A WebRTC based tool to support low latency audio conferencing. This is targeted to allow musicians to be able to jam together.
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 

194 řádky
4.7 KiB

  1. import jamming from './jamming';
  2. import { v4 as uuidv4 } from 'uuid';
  3. import WebSocketAsPromised from 'websocket-as-promised';
  4. function sendServer(obj) {
  5. let lclobj = Object.assign({}, obj);
  6. lclobj.uuid = uuid;
  7. return fetch('/offer', {
  8. body: JSON.stringify(lclobj),
  9. headers: {
  10. 'Content-Type': 'application/json'
  11. },
  12. method: 'POST'
  13. });
  14. }
  15. async function runPage() {
  16. const uuid = uuidv4();
  17. const constatus = document.getElementById('constatus');
  18. const audioSink = document.getElementById('audioSink');
  19. var stream;
  20. var wsp;
  21. var pc;
  22. const constraints = {
  23. audio: {
  24. latency: .005, /* 5ms latency */
  25. channelCount: 1,
  26. noiseSuppression: false,
  27. autoGainControl: false,
  28. sampleRate: { min: 22050, max: 48000, ideal: 32000 },
  29. }
  30. };
  31. /* setup local media */
  32. try {
  33. stream = await navigator.mediaDevices.getUserMedia(constraints);
  34. } catch(err) {
  35. constatus.textContent = 'Unable to open microphone';
  36. return
  37. }
  38. /* setup server messages */
  39. wsp = new WebSocketAsPromised('ws://' + window.location.host + '/ws', {
  40. createWebSocket: url => new WebSocket(url),
  41. extractMessageData: event => event,
  42. });
  43. wsp.onError.addListener((err) => {
  44. constatus.textContent = 'connection to server lost';
  45. });
  46. wsp.onMessage.addListener((message) => {
  47. var msg = JSON.parse(message.data);
  48. console.log('got message via ws:', msg);
  49. if (msg.uuid == uuid) return;
  50. if (msg.sdp) {
  51. pc.setRemoteDescription(new RTCSessionDescription(msg));
  52. } else if (msg.ice) {
  53. pc.addIceCandidate(new RTCIceCandidate(msg.ice));
  54. }
  55. });
  56. await wsp.open();
  57. function sendServer(obj) {
  58. var lclobj = Object.assign({}, obj);
  59. lclobj.uuid = uuid;
  60. console.log('send:', lclobj);
  61. wsp.send(JSON.stringify(lclobj));
  62. }
  63. /* we are initiator */
  64. const configuration = {
  65. iceServers: [ {
  66. urls: [
  67. 'stun:stun3.l.google.com:19302',
  68. /* reduce number of stun servers
  69. 'stun:stun.l.google.com:19302',
  70. 'stun:stun1.l.google.com:19302',
  71. 'stun:stun2.l.google.com:19302',
  72. 'stun:stun4.l.google.com:19302',
  73. */
  74. 'stun:stun.services.mozilla.com',
  75. ]
  76. } ]
  77. };
  78. pc = new RTCPeerConnection(configuration);
  79. pc.onicecandidate = (event) => {
  80. if (event.candidate != null) {
  81. console.log(event.candidate)
  82. sendServer({ ice: event.candidate });
  83. }
  84. };
  85. pc.ontrack = (event) => {
  86. audioSink.srcObject = event.streams[0];
  87. };
  88. pc.addStream(stream);
  89. try {
  90. var desc = await pc.createOffer()
  91. } catch(err) {
  92. constatus.textContent = 'failed to create offer for server: ' + err;
  93. return
  94. }
  95. /* do description filtering here */
  96. await pc.setLocalDescription(desc);
  97. var ld = pc.localDescription;
  98. sendServer({ sdp: ld.sdp, type: ld.type });
  99. }
  100. runPage()
  101. // #4 of https://stackoverflow.com/questions/37656592/define-global-variable-with-webpack
  102. global.runPage = runPage;
  103. async function foo() {
  104. var cert = await RTCPeerConnection.generateCertificate({
  105. name: "ECDSA", namedCurve: "P-256",
  106. hash: 'SHA-256'
  107. });
  108. // global.pc = new RTCPeerConnection({certificates: [cert]});
  109. }
  110. async function bar() {
  111. const signaling = new SignalingChannel(); // handles JSON.stringify/parse
  112. const configuration = {
  113. iceServers: [ {
  114. urls: [
  115. 'stun.l.google.com:19302',
  116. 'stun1.l.google.com:19302',
  117. 'stun2.l.google.com:19302',
  118. 'stun3.l.google.com:19302',
  119. 'stun4.l.google.com:19302',
  120. 'stun:stun.services.mozilla.com',
  121. ]
  122. } ]
  123. };
  124. let pc, channel;
  125. // call start() to initiate
  126. function start() {
  127. pc = new RTCPeerConnection(configuration);
  128. // send any ice candidates to the other peer
  129. pc.onicecandidate = ({candidate}) => signaling.send({candidate});
  130. // let the "negotiationneeded" event trigger offer generation
  131. pc.onnegotiationneeded = async () => {
  132. try {
  133. await pc.setLocalDescription();
  134. // send the offer to the other peer
  135. signaling.send({description: pc.localDescription});
  136. } catch (err) {
  137. console.error(err);
  138. }
  139. };
  140. // create data channel and setup chat using "negotiated" pattern
  141. channel = pc.createDataChannel('chat', {negotiated: true, id: 0});
  142. channel.onopen = () => input.disabled = false;
  143. channel.onmessage = ({data}) => showChatMessage(data);
  144. input.onkeypress = ({keyCode}) => {
  145. // only send when user presses enter
  146. if (keyCode != 13) return;
  147. channel.send(input.value);
  148. }
  149. }
  150. signaling.onmessage = async ({data: {description, candidate}}) => {
  151. if (!pc) start(false);
  152. try {
  153. if (description) {
  154. await pc.setRemoteDescription(description);
  155. // if we got an offer, we need to reply with an answer
  156. if (description.type == 'offer') {
  157. await pc.setLocalDescription();
  158. signaling.send({description: pc.localDescription});
  159. }
  160. } else if (candidate) {
  161. await pc.addIceCandidate(candidate);
  162. }
  163. } catch (err) {
  164. console.error(err);
  165. }
  166. };
  167. }