cduss commited on
Commit
94a9cda
·
1 Parent(s): 9174608

feat: bidirectional audio

Browse files
Files changed (1) hide show
  1. index.html +93 -2
index.html CHANGED
@@ -520,6 +520,22 @@
520
  </svg>
521
  <span id="muteText">Unmute</span>
522
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
523
  </div>
524
  </div>
525
  </div>
@@ -619,6 +635,10 @@
619
  // Audio mute state (for robot's audio playback)
620
  let isMuted = true; // Default to muted
621
 
 
 
 
 
622
  // Export functions
623
  window.loginToHuggingFace = loginToHuggingFace;
624
  window.logout = logout;
@@ -628,6 +648,7 @@
628
  window.playSound = playSound;
629
  window.playSoundPreset = playSoundPreset;
630
  window.toggleMute = toggleMute;
 
631
 
632
  document.addEventListener('DOMContentLoaded', () => {
633
  initAuth();
@@ -814,10 +835,28 @@
814
  isMuted = true;
815
  updateMuteButton();
816
 
 
 
 
 
 
 
 
 
 
 
 
817
  peerConnection = new RTCPeerConnection({
818
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
819
  });
820
 
 
 
 
 
 
 
 
821
  peerConnection.ontrack = (e) => {
822
  if (e.track.kind === 'video') {
823
  const video = document.getElementById('remoteVideo');
@@ -868,8 +907,13 @@
868
  if (!peerConnection) return;
869
  try {
870
  if (msg.sdp) {
871
- await peerConnection.setRemoteDescription(new RTCSessionDescription(msg.sdp));
872
- if (msg.sdp.type === 'offer') {
 
 
 
 
 
873
  const answer = await peerConnection.createAnswer();
874
  await peerConnection.setLocalDescription(answer);
875
  await sendToServer({ type: 'peer', sessionId: currentSessionId, sdp: { type: 'answer', sdp: answer.sdp } });
@@ -913,6 +957,13 @@
913
  await sendToServer({ type: 'endSession', sessionId: currentSessionId });
914
  }
915
 
 
 
 
 
 
 
 
916
  if (peerConnection) peerConnection.close();
917
  if (dataChannel) dataChannel.close();
918
 
@@ -930,6 +981,7 @@
930
  function enableControls(enabled) {
931
  document.getElementById('btnPlaySound').disabled = !enabled;
932
  document.getElementById('muteBtn').disabled = !enabled;
 
933
  }
934
 
935
  function toggleMute() {
@@ -957,6 +1009,45 @@
957
  }
958
  }
959
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
960
  // ===================== Robot State =====================
961
  function handleRobotMessage(data) {
962
  if (data.state) updateStateDisplay(data.state);
 
520
  </svg>
521
  <span id="muteText">Unmute</span>
522
  </button>
523
+ <button class="btn btn-mute muted" id="micBtn" onclick="toggleMic()" disabled>
524
+ <svg id="micOffIcon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
525
+ <line x1="1" y1="1" x2="23" y2="23"></line>
526
+ <path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"></path>
527
+ <path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2c0 .76-.13 1.49-.35 2.17"></path>
528
+ <line x1="12" y1="19" x2="12" y2="23"></line>
529
+ <line x1="8" y1="23" x2="16" y2="23"></line>
530
+ </svg>
531
+ <svg id="micOnIcon" class="hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
532
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
533
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
534
+ <line x1="12" y1="19" x2="12" y2="23"></line>
535
+ <line x1="8" y1="23" x2="16" y2="23"></line>
536
+ </svg>
537
+ <span id="micText">Mic Off</span>
538
+ </button>
539
  </div>
540
  </div>
541
  </div>
 
635
  // Audio mute state (for robot's audio playback)
636
  let isMuted = true; // Default to muted
637
 
638
+ // Microphone state (for speaking through the robot)
639
+ let localMicStream = null;
640
+ let isMicMuted = true; // Default mic off
641
+
642
  // Export functions
643
  window.loginToHuggingFace = loginToHuggingFace;
644
  window.logout = logout;
 
648
  window.playSound = playSound;
649
  window.playSoundPreset = playSoundPreset;
650
  window.toggleMute = toggleMute;
651
+ window.toggleMic = toggleMic;
652
 
653
  document.addEventListener('DOMContentLoaded', () => {
654
  initAuth();
 
835
  isMuted = true;
836
  updateMuteButton();
837
 
838
+ // Capture microphone for bidirectional audio (speak through robot)
839
+ try {
840
+ localMicStream = await navigator.mediaDevices.getUserMedia({ audio: true });
841
+ localMicStream.getAudioTracks().forEach(t => t.enabled = false); // Start muted
842
+ isMicMuted = true;
843
+ updateMicButton();
844
+ } catch (e) {
845
+ console.warn('Microphone not available, speak-through-robot disabled:', e);
846
+ localMicStream = null;
847
+ }
848
+
849
  peerConnection = new RTCPeerConnection({
850
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
851
  });
852
 
853
+ // Add mic track to PeerConnection (before SDP exchange)
854
+ if (localMicStream) {
855
+ for (const track of localMicStream.getAudioTracks()) {
856
+ peerConnection.addTrack(track, localMicStream);
857
+ }
858
+ }
859
+
860
  peerConnection.ontrack = (e) => {
861
  if (e.track.kind === 'video') {
862
  const video = document.getElementById('remoteVideo');
 
907
  if (!peerConnection) return;
908
  try {
909
  if (msg.sdp) {
910
+ let sdp = msg.sdp;
911
+ // If we have a mic, ensure audio is sendrecv for bidirectional audio
912
+ if (sdp.type === 'offer' && localMicStream) {
913
+ sdp = { ...sdp, sdp: makeAudioBidirectional(sdp.sdp) };
914
+ }
915
+ await peerConnection.setRemoteDescription(new RTCSessionDescription(sdp));
916
+ if (sdp.type === 'offer') {
917
  const answer = await peerConnection.createAnswer();
918
  await peerConnection.setLocalDescription(answer);
919
  await sendToServer({ type: 'peer', sessionId: currentSessionId, sdp: { type: 'answer', sdp: answer.sdp } });
 
957
  await sendToServer({ type: 'endSession', sessionId: currentSessionId });
958
  }
959
 
960
+ // Stop microphone
961
+ if (localMicStream) {
962
+ localMicStream.getTracks().forEach(t => t.stop());
963
+ localMicStream = null;
964
+ }
965
+ isMicMuted = true;
966
+
967
  if (peerConnection) peerConnection.close();
968
  if (dataChannel) dataChannel.close();
969
 
 
981
  function enableControls(enabled) {
982
  document.getElementById('btnPlaySound').disabled = !enabled;
983
  document.getElementById('muteBtn').disabled = !enabled;
984
+ document.getElementById('micBtn').disabled = !enabled || !localMicStream;
985
  }
986
 
987
  function toggleMute() {
 
1009
  }
1010
  }
1011
 
1012
+ function toggleMic() {
1013
+ if (!localMicStream) return;
1014
+ isMicMuted = !isMicMuted;
1015
+ localMicStream.getAudioTracks().forEach(t => t.enabled = !isMicMuted);
1016
+ updateMicButton();
1017
+ }
1018
+
1019
+ function updateMicButton() {
1020
+ const btn = document.getElementById('micBtn');
1021
+ const micOffIcon = document.getElementById('micOffIcon');
1022
+ const micOnIcon = document.getElementById('micOnIcon');
1023
+ const micText = document.getElementById('micText');
1024
+
1025
+ if (isMicMuted) {
1026
+ btn.classList.add('muted');
1027
+ micOffIcon.classList.remove('hidden');
1028
+ micOnIcon.classList.add('hidden');
1029
+ micText.textContent = 'Mic Off';
1030
+ } else {
1031
+ btn.classList.remove('muted');
1032
+ micOffIcon.classList.add('hidden');
1033
+ micOnIcon.classList.remove('hidden');
1034
+ micText.textContent = 'Mic On';
1035
+ }
1036
+ }
1037
+
1038
+ function makeAudioBidirectional(sdp) {
1039
+ // Change audio section direction from sendonly to sendrecv
1040
+ // so the browser can send microphone audio to the robot
1041
+ const lines = sdp.split('\r\n');
1042
+ let inAudioSection = false;
1043
+ return lines.map(line => {
1044
+ if (line.startsWith('m=audio')) inAudioSection = true;
1045
+ else if (line.startsWith('m=')) inAudioSection = false;
1046
+ if (inAudioSection && line === 'a=sendonly') return 'a=sendrecv';
1047
+ return line;
1048
+ }).join('\r\n');
1049
+ }
1050
+
1051
  // ===================== Robot State =====================
1052
  function handleRobotMessage(data) {
1053
  if (data.state) updateStateDisplay(data.state);