Skip to content

Instantly share code, notes, and snippets.

@kwindla
Created January 7, 2020 23:56
Show Gist options
  • Save kwindla/f273f9afce17cb1715288a208c5d1853 to your computer and use it in GitHub Desktop.
Save kwindla/f273f9afce17cb1715288a208c5d1853 to your computer and use it in GitHub Desktop.
merge audio tracks partial sample
setupAudioContext () {
this.audioContext = new window.AudioContext();
// create an output stream node. have to use the constructor form
// because we want to make the stream mono
this.audioOut = new MediaStreamAudioDestinationNode(this.audioContext,
{ channelCount: 1 });
// connecting a single gain node to the audioOut node suffices for
// the pipeline to work. a MediaStreamDestination with nothing
// connected to it seems to hang the MediaRecorder
this.localGain = this.audioContext.createGain();
this.localGain.connect(this.audioOut);
}
startRecording () {
this.setupAudioContext();
this.localAudioInfo = { streamId: null, gainNode: null };
this.remoteAudioInfo = {}; // [participantId]: {streamId, gainNode}
let audioTrack = this.audioOut.stream.getAudioTracks()[0];
let mediaStream = new MediaStream([
audioTrack
]);
// setup MediaRecorder, etc ...
}
// set up an audio gain node so we can control stream volume
createAudioGainNode (stream, muted=false, gainValue=1.0) {
try {
if (stream.getAudioTracks().length === 0) {
return null;
}
let source = this.audioContext.createMediaStreamSource(stream),
gainNode = this.audioContext.createGain();
gainNode._dailyInitialGainValue = gainValue;
gainNode._dailySourceNode = source;
gainNode.gain.value = muted ? 0 : gainValue;
source.connect(gainNode);
gainNode.connect(this.audioOut);
return gainNode;
} catch (e) {
console.error('error setting up gain node', e);
}
}
removeAudioGainNode (gainNode) {
if (gainNode) {
gainNode.disconnect(this.audioOut);
// this cleanup may not be necessary, but given that we don't
// have much control over object lifecycle let's be extra careful
gainNode._dailySourceNode.disconnect(gainNode);
gainNode._dailySourceNode = null;
}
}
updateLocalAudioStream (localParticipant) {
if (!(localParticipant && localParticipant.stream &&
localParticipant.stream.stream &&
localParticipant.stream.id)) {
return;
}
if (this.localAudioInfo.streamId !== localParticipant.stream.id) {
console.log('recorder updating local audio stream');
// create/update local audio track that's feeding into the
// recording stream. we need to clone the stream so we can
// control whether the local audio track is enabled (in
// audioconf mode the local audio track is disabled so that we
// don't send webrtc audio to Daily peers)
let stream = localParticipant.stream.stream.clone(),
audioTrack = stream.getAudioTracks()[0],
isMuted = isUserMuted(localParticipant.public.audioState);
if (audioTrack) {
audioTrack.enabled = true;
this.localAudioInfo.streamId = localParticipant.stream.id;
this.removeAudioGainNode(this.localAudioInfo.gainNode);
this.localAudioInfo.gainNode = this.createAudioGainNode(stream,isMuted);
}
} else {
// mute/unmute local audio stream if necessary
if (this.localAudioInfo.gainNode) {
let localMicMuted = isUserMuted(localParticipant.public.audioState),
gainNodeMuted = this.localAudioInfo.gainNode.gain.value === 0;
if (localMicMuted && !gainNodeMuted) {
this.localAudioInfo.gainNode.gain.value = 0;
} else if (!localMicMuted && gainNodeMuted) {
this.localAudioInfo.gainNode.gain.value =
this.localAudioInfo.gainNode._dailyInitialGainValue;
}
}
}
}
updateRemoteAudioStreams (camParticipants) {
// loop through our remote audio streams and remove any for which
// we don't have a camParticipants entry
Object.keys(this.remoteAudioInfo).forEach((participantId) => {
if (!find(camParticipants, (p) => p.public.id === participantId)) {
console.log('recording removing audio stream for', participantId);
this.removeAudioStream(participantId);
}
});
// loop through camParticipants list. add/update track for any
// with an audio track. we let muting be handled on the remote end.
camParticipants.forEach((p) => {
if (p.public && p.audioTrack) {
this.updateAudioStream(p.public.id, p.audioTrack.pendingTrack);
}
});
}
removeAudioStream (participantId) {
let streamInfo = this.remoteAudioInfo[participantId];
if (streamInfo) {
if (streamInfo.gainNode) {
this.removeAudioGainNode(streamInfo.gainNode);
}
delete this.remoteAudioInfo[participantId];
}
}
updateAudioStream (participantId, track, gainValue=1.0) {
if (!(track && participantId)) {
return;
}
let currentInfo = this.remoteAudioInfo[participantId];
// make a new info record if we don't have one
if (!currentInfo) {
currentInfo = { trackId: null, gainNode: null };
this.remoteAudioInfo[participantId] = currentInfo;
}
// if stream id doesn't match, remove current gain node and make a new one
if (track.id !== currentInfo.trackId) {
console.log("recording replacing audio stream for", participantId);
this.removeAudioGainNode(currentInfo.gainNode);
currentInfo.trackId = track.id;
currentInfo.gainNode = this.createAudioGainNode(
new MediaStream([track]), false, gainValue
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment