From dc6420049100931fd616808d024fdf626cf8114e Mon Sep 17 00:00:00 2001 From: rafern Date: Wed, 7 Dec 2022 17:39:22 +0000 Subject: [PATCH 1/4] WIP MediaStream support (...) - Need to find a way to build the project and test it; `sed` isn't working for some reason, even though I have it installed, and it's used in the build script --- src/howler.core.js | 44 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/howler.core.js b/src/howler.core.js index aa60cdbe..75d2a3d3 100644 --- a/src/howler.core.js +++ b/src/howler.core.js @@ -588,7 +588,7 @@ self._preload = (typeof o.preload === 'boolean' || o.preload === 'metadata') ? o.preload : true; self._rate = o.rate || 1; self._sprite = o.sprite || {}; - self._src = (typeof o.src !== 'string') ? o.src : [o.src]; + self._src = (typeof o.src !== 'string' && !isMediaStream(o.src)) ? o.src : [o.src]; self._volume = o.volume !== undefined ? o.volume : 1; self._xhr = { method: o.xhr && o.xhr.method ? o.xhr.method : 'GET', @@ -603,6 +603,7 @@ self._endTimers = {}; self._queue = []; self._playLock = false; + self._isMediaStream = false; // Setup event listeners. self._onend = o.onend ? [{fn: o.onend}] : []; @@ -664,13 +665,22 @@ } // Make sure our source is in an array. - if (typeof self._src === 'string') { + if (typeof self._src === 'string' || isMediaStream(self._src)) { self._src = [self._src]; } // Loop through the sources and pick the first one that is compatible. for (var i=0; i= 0; - if (!node.bufferSource) { + if (self._isMediaStream || !node.bufferSource) { return self; } @@ -2270,7 +2282,12 @@ self._node.addEventListener('ended', self._endFn, false); // Setup the new audio node. - self._node.src = parent._src; + if (parent._isMediaStream) { + self._node.srcObject = parent._src; + } else { + self._node.src = parent._src; + } + self._node.preload = parent._preload === true ? 'auto' : parent._preload; self._node.volume = volume * Howler.volume(); @@ -2375,6 +2392,7 @@ /***************************************************************************/ var cache = {}; + var mediaStreamsSupported = 'MediaStream' in globalThis; /** * Buffer a sound from URL, Data URI or cache and decode to audio source (Web Audio API). @@ -2556,6 +2574,16 @@ Howler._setup(); }; + /** + * Helper function for checking whether a source is a MediaStream. Doesn't + * throw an error when MediaStreams aren't supported. + * @param {String | MediaStream | Array} src The source to check + * @return {Boolean} Returns true if the source is a MediaStream instance + */ + var isMediaStream = function(src) { + return mediaStreamsSupported && src instanceof MediaStream; + } + // Add support for AMD (Asynchronous Module Definition) libraries such as require.js. if (typeof define === 'function' && define.amd) { define([], function() { From ce955741b6c73d3b3adb048c8ffdccc7ac7ea9c3 Mon Sep 17 00:00:00 2001 From: rafern Date: Thu, 8 Dec 2022 13:17:44 +0000 Subject: [PATCH 2/4] Fix exception and MediaStreams not playing --- src/howler.core.js | 48 +++++++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/howler.core.js b/src/howler.core.js index 75d2a3d3..2e4722c1 100644 --- a/src/howler.core.js +++ b/src/howler.core.js @@ -678,7 +678,13 @@ // Sound object needs to be created if (isMediaStream(src)) { self._isMediaStream = true; + self._src = src; + self._duration = Infinity; + self._sprite = {__default: [0, Infinity]}; new Sound(self); + self._state = 'loaded'; + self._emit('load'); + self._loadQueue(); return self; } @@ -873,11 +879,13 @@ node.gain.setValueAtTime(vol, Howler.ctx.currentTime); sound._playStart = Howler.ctx.currentTime; - // Play the sound using the supported method. - if (typeof node.bufferSource.start === 'undefined') { - sound._loop ? node.bufferSource.noteGrainOn(0, seek, 86400) : node.bufferSource.noteGrainOn(0, seek, duration); - } else { - sound._loop ? node.bufferSource.start(0, seek, 86400) : node.bufferSource.start(0, seek, duration); + if (!self._isMediaStream) { + // Play the sound using the supported method. + if (typeof node.bufferSource.start === 'undefined') { + sound._loop ? node.bufferSource.noteGrainOn(0, seek, 86400) : node.bufferSource.noteGrainOn(0, seek, duration); + } else { + sound._loop ? node.bufferSource.start(0, seek, 86400) : node.bufferSource.start(0, seek, duration); + } } // Start a new timer if none is present. @@ -2148,9 +2156,15 @@ _refreshBuffer: function(sound) { var self = this; - // Setup the buffer source for playback. - sound._node.bufferSource = Howler.ctx.createBufferSource(); - sound._node.bufferSource.buffer = cache[self._src]; + if (self._isMediaStream) { + // Special case for streams. Make a MediaStreamSource and set it as the + // bufferSource. + sound._node.bufferSource = Howler.ctx.createMediaStreamSource(self._src); + } else { + // Setup the buffer source for playback. + sound._node.bufferSource = Howler.ctx.createBufferSource(); + sound._node.bufferSource.buffer = cache[self._src]; + } // Connect to the correct node. if (sound._panner) { @@ -2159,13 +2173,17 @@ sound._node.bufferSource.connect(sound._node); } - // Setup looping and playback rate. - sound._node.bufferSource.loop = sound._loop; - if (sound._loop) { - sound._node.bufferSource.loopStart = sound._start || 0; - sound._node.bufferSource.loopEnd = sound._stop || 0; + // MediaStreams can't have custom playback rates or loop, so don't set + // that up + if (!self._isMediaStream) { + // Setup looping and playback rate. + sound._node.bufferSource.loop = sound._loop; + if (sound._loop) { + sound._node.bufferSource.loopStart = sound._start || 0; + sound._node.bufferSource.loopEnd = sound._stop || 0; + } + sound._node.bufferSource.playbackRate.setValueAtTime(sound._rate, Howler.ctx.currentTime); } - sound._node.bufferSource.playbackRate.setValueAtTime(sound._rate, Howler.ctx.currentTime); return self; }, @@ -2179,7 +2197,7 @@ var self = this; var isIOS = Howler._navigator && Howler._navigator.vendor.indexOf('Apple') >= 0; - if (self._isMediaStream || !node.bufferSource) { + if (!node.bufferSource) { return self; } From 1a1a7bbbc9aec9d21f55db2d8bd290aaa1858378 Mon Sep 17 00:00:00 2001 From: rafern Date: Thu, 8 Dec 2022 14:32:57 +0000 Subject: [PATCH 3/4] Fix MediaStreams web audio playback on Chromium, fix pause/stop --- src/howler.core.js | 45 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/src/howler.core.js b/src/howler.core.js index 2e4722c1..fe41f019 100644 --- a/src/howler.core.js +++ b/src/howler.core.js @@ -1061,14 +1061,16 @@ if (sound._node) { if (self._webAudio) { // Make sure the sound has been created. - if (!self._isMediaStream && !sound._node.bufferSource) { + if (!sound._node.bufferSource) { continue; } - if (typeof sound._node.bufferSource.stop === 'undefined') { - sound._node.bufferSource.noteOff(0); - } else { - sound._node.bufferSource.stop(0); + if (!self._isMediaStream) { + if (typeof sound._node.bufferSource.stop === 'undefined') { + sound._node.bufferSource.noteOff(0); + } else { + sound._node.bufferSource.stop(0); + } } // Clean up the buffer source. @@ -1133,10 +1135,12 @@ if (self._webAudio) { // Make sure the sound's AudioBufferSourceNode has been created. if (sound._node.bufferSource) { - if (typeof sound._node.bufferSource.stop === 'undefined') { - sound._node.bufferSource.noteOff(0); - } else { - sound._node.bufferSource.stop(0); + if (!self._isMediaStream) { + if (typeof sound._node.bufferSource.stop === 'undefined') { + sound._node.bufferSource.noteOff(0); + } else { + sound._node.bufferSource.stop(0); + } } // Clean up the buffer source. @@ -2159,6 +2163,29 @@ if (self._isMediaStream) { // Special case for streams. Make a MediaStreamSource and set it as the // bufferSource. + + // XXX There is a Chromium bug + // (https://bugs.chromium.org/p/chromium/issues/detail?id=933677) where + // remote MediaStreams don't play unless they are assigned to a media + // element. Workaround: + var ua = Howler._navigator ? Howler._navigator.userAgent : ''; + if (ua.indexOf('Chrome') !== -1) { + var tmpAudio = new Audio(); + + var tmpAudioCallback = function() { + if (tmpAudio) { + tmpAudio.removeEventListener('error', tmpAudioCallback); + tmpAudio.removeEventListener('canplaythrough', tmpAudioCallback); + tmpAudio = null; + } + }; + + tmpAudio.muted = true; + tmpAudio.addEventListener('error', tmpAudioCallback); + tmpAudio.addEventListener('canplaythrough', tmpAudioCallback); + tmpAudio.srcObject = self._src; + } + sound._node.bufferSource = Howler.ctx.createMediaStreamSource(self._src); } else { // Setup the buffer source for playback. From bc291feee573adaeb7e32f74aadf697e75f97763 Mon Sep 17 00:00:00 2001 From: rafern Date: Fri, 16 Dec 2022 14:08:18 +0000 Subject: [PATCH 4/4] Fix crash when unloading a Howl instance --- src/howler.core.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/howler.core.js b/src/howler.core.js index fe41f019..2e02e9df 100644 --- a/src/howler.core.js +++ b/src/howler.core.js @@ -1809,7 +1809,7 @@ // Delete this sound from the cache (if no other Howl is using it). var remCache = true; for (i=0; i= 0) { + if (Howler._howls[i]._src === self._src || (!self._isMediaStream && self._src.indexOf(Howler._howls[i]._src) >= 0)) { remCache = false; break; }