bae731e59e
Works around the fact that Chrome doesnt have AEC on streams that dont go through PCs
112 lines
3.1 KiB
JavaScript
112 lines
3.1 KiB
JavaScript
import MediaStreamUtils from '/imports/utils/media-stream-utils';
|
|
|
|
export default class LocalPCLoopback {
|
|
constructor(constraints) {
|
|
this.constraints = constraints;
|
|
this.inputStream = null;
|
|
this.localPC = null;
|
|
this.loopbackPC = null;
|
|
this.loopbackStream = new MediaStream();
|
|
}
|
|
|
|
_initializeLocalPC() {
|
|
this.localPC = new RTCPeerConnection();
|
|
this.localPC.onicecandidate = (({ candidate }) => {
|
|
if (candidate && this.loopbackPC) {
|
|
this.loopbackPC.addIceCandidate(new RTCIceCandidate(candidate));
|
|
}
|
|
});
|
|
this.inputStream.getTracks().forEach((track) => this.localPC.addTrack(track, this.inputStream));
|
|
}
|
|
|
|
_initializeLoopbackPC() {
|
|
this.loopbackPC = new RTCPeerConnection();
|
|
this.loopbackPC.onicecandidate = (({ candidate }) => {
|
|
if (candidate && this.localPC) {
|
|
this.localPC.addIceCandidate(new RTCIceCandidate(candidate));
|
|
}
|
|
});
|
|
this.loopbackPC.ontrack = (({ streams }) => {
|
|
streams.forEach((stream) => {
|
|
stream.getTracks().forEach((track) => this.loopbackStream.addTrack(track));
|
|
});
|
|
});
|
|
}
|
|
|
|
_replaceInputStream(inputStream) {
|
|
let replaced = false;
|
|
|
|
if (this.localPC == null || inputStream == null || !inputStream?.active) {
|
|
return Promise.resolve(this.loopbackStream);
|
|
}
|
|
|
|
const newTracks = {
|
|
audio: inputStream.getAudioTracks(),
|
|
video: inputStream.getVideoTracks(),
|
|
};
|
|
this.localPC.getSenders().forEach((sender, index) => {
|
|
if (sender.track) {
|
|
const { kind } = sender.track;
|
|
if (this.constraints[kind]) {
|
|
const newTrack = newTracks[kind][index];
|
|
if (newTrack == null) return;
|
|
sender.replaceTrack(newTrack);
|
|
replaced = true;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (replaced) this.inputStream = inputStream;
|
|
|
|
return Promise.resolve(this.loopbackStream);
|
|
}
|
|
|
|
async start(inputStream) {
|
|
if (inputStream == null || !inputStream?.active) throw (new TypeError('Invalid input stream'));
|
|
|
|
if (this.localPC && this.loopbackPC) return this._replaceInputStream(inputStream);
|
|
|
|
this.inputStream = inputStream;
|
|
const nOptions = {
|
|
offerAudio: this.constraints.audio,
|
|
offerVideo: this.constraints.video,
|
|
offerToReceiveAudio: false,
|
|
offerToReceiveVideo: false,
|
|
};
|
|
|
|
try {
|
|
this._initializeLocalPC();
|
|
this._initializeLoopbackPC();
|
|
const offer = await this.localPC.createOffer(nOptions);
|
|
await this.localPC.setLocalDescription(offer);
|
|
await this.loopbackPC.setRemoteDescription(offer);
|
|
const answer = await this.loopbackPC.createAnswer();
|
|
await this.loopbackPC.setLocalDescription(answer);
|
|
await this.localPC.setRemoteDescription(answer);
|
|
|
|
return this.loopbackStream;
|
|
} catch (error) {
|
|
// Rollback
|
|
this.stop();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
stop() {
|
|
if (this.localPC) {
|
|
this.localPC.close();
|
|
this.localPC = null;
|
|
}
|
|
|
|
if (this.loopbackPC) {
|
|
this.loopbackPC.close();
|
|
this.loopbackPC = null;
|
|
}
|
|
|
|
if (this.loopbackStream) {
|
|
MediaStreamUtils.stopMediaStreamTracks(this.loopbackStream);
|
|
this.loopbackStream = null;
|
|
}
|
|
}
|
|
}
|