Skip to content

Instantly share code, notes, and snippets.

@AndrewMeadows
Last active November 4, 2021 15:00
Show Gist options
  • Save AndrewMeadows/80c50549559a98e5cfb7890893991276 to your computer and use it in GitHub Desktop.
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
// 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