A WebRTC based tool to support low latency audio conferencing. This is targeted to allow musicians to be able to jam together.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

230 lines
5.6 KiB

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