Last active
November 4, 2021 15:00
-
-
Save AndrewMeadows/80c50549559a98e5cfb7890893991276 to your computer and use it in GitHub Desktop.
Workaround for second phenotype of Chrome bug #1264539: insertable-streams fails on simple audio MediaStream
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// LoopbackTransform is a DOUBLE WORKAROUND helper for Chrome BUG #1264539 | |
// https://bugs.chromium.org/p/chromium/issues/detail?id=1264539 | |
// e.g. it allows insertable-streams on simple local audio stream | |
// | |
class LoopbackTransform { | |
// ctor() takes 'transformer' | |
// which is required to have a getTransform() method | |
// which supplies a transform function for audio MediaStream via insertable-streams | |
// 'audio' argument is optional AudioElement | |
// for when you just want to play the final outputStream | |
// | |
constructor(transformer, audio = null) { | |
// Note: the LoopbackTransform does NOT own the 'transformer'. | |
// e.g. if 'transformer' is some WASM object with Module memory | |
// then it is the duty of external logic to clean it up. | |
// | |
this.transformer = transformer; | |
this.audio = audio; | |
// initialize the RTC connections | |
this.pc0 = new RTCPeerConnection(); | |
this.pc1 = new RTCPeerConnection(); | |
this.pc0.onicecandidate = e => | |
e.candidate && this.pc1.addIceCandidate(new RTCIceCandidate(e.candidate)); | |
this.pc1.onicecandidate = e => | |
e.candidate && this.pc0.addIceCandidate(new RTCIceCandidate(e.candidate)); | |
// pc1's ontrack callback applies insertable-streams to the inbound | |
// end of the audio stream after it has passed through the RTCPeerConnection | |
let self = this; | |
this.pc1.ontrack = (e) => { | |
// e = { receiver, streams, track, transceiver, target } | |
let track = e.track; | |
let workaroundStream = e.streams[0]; | |
// make the insertable-stream pipes | |
let processor = new MediaStreamTrackProcessor({track: track}); | |
let generator = new MediaStreamTrackGenerator({kind: 'audio'}); | |
const source = processor.readable; | |
const sink = generator.writable; | |
// supply the actual transform operation which will do the interesting work | |
let transformStream = new TransformStream({transform: self.transformer.getTransform()}); | |
// abortController handles failures during pipeThrough | |
let abortController = new AbortController(); | |
const signal = abortController.signal; | |
// connect the pipes | |
let promise = source.pipeThrough(transformStream, {signal}).pipeTo(sink); | |
promise.catch((e) => { | |
if (signal.aborted) { | |
console.log(`Shutting down adder stream id=${stream.id} after abort`); | |
} else { | |
console.error(`Error from adder transform: error='${e.message}'`); | |
} | |
source.cancel(e); | |
sink.abort(e); | |
}); | |
// WORKAROUND for first phenotype of BUG #1264539 | |
// keep bogusAudio running but with zero volume | |
let bogusAudio = new Audio(); | |
bogusAudio.srcObject = workaroundStream; | |
bogusAudio.volume = 0; | |
bogusAudio.play(); | |
// final pipe connection | |
let stream = new MediaStream(); | |
stream.addTrack(generator); | |
// remember these for later | |
self.abortController = abortController; | |
self.outputStream = stream; | |
self.bogusAudio = bogusAudio; | |
if (self.audio) { | |
// attach the outputStream to the supplied AudioElement | |
audio.loop = true; | |
audio.srcObject = stream; | |
audio.play(); | |
} | |
}; | |
} | |
// startLoopback() takes the 'stream' to which the insterable-stream will operate | |
// after it has passed through the RTCPeerConnection loopback | |
// | |
async startLoopback(stream) { | |
let track = stream.getAudioTracks()[0]; | |
this.pc0.addTrack(track, stream); | |
const offerOptions = { | |
offerAudio: true, | |
offerVideo: false, | |
offerToReceiveAudio: false, | |
offerToReceiveVideo: false | |
}; | |
let offer = await this.pc0.createOffer(offerOptions); | |
await this.pc0.setLocalDescription(offer); | |
await this.pc1.setRemoteDescription(offer); | |
let answer = await this.pc1.createAnswer(); | |
await this.pc1.setLocalDescription(answer); | |
await this.pc0.setRemoteDescription(answer); | |
} | |
// abort() for safe shutdown of stream workaround | |
// | |
abort() { | |
if (this.abortController) { | |
this.bogusAudio.pause(); | |
if (this.audio) { | |
this.audio.pause(); | |
} | |
this.abortController.abort(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment