1
Fork 0

Factor out stream creation, implement replaceUpStream.

This avoids code duplication in the various addUpMedia functions,
and makes it possible to replace a stream without reopening the
stream's source.  This will be required in order to change the
simulcast envelope.
This commit is contained in:
Juliusz Chroboczek 2021-05-06 23:39:27 +02:00
parent ed88a7a4b5
commit f1a15f07db
1 changed files with 209 additions and 157 deletions

View File

@ -392,12 +392,6 @@ getButtonElement('unpresentbutton').onclick = function(e) {
resizePeers(); resizePeers();
}; };
function changePresentation() {
let c = findUpMedia('camera');
if(c)
addLocalMedia(c.localId);
}
/** /**
* @param {string} id * @param {string} id
* @param {boolean} visible * @param {boolean} visible
@ -466,7 +460,7 @@ getSelectElement('videoselect').onchange = function(e) {
if(!(this instanceof HTMLSelectElement)) if(!(this instanceof HTMLSelectElement))
throw new Error('Unexpected type for this'); throw new Error('Unexpected type for this');
updateSettings({video: this.value}); updateSettings({video: this.value});
changePresentation(); replaceCameraStream();
}; };
getSelectElement('audioselect').onchange = function(e) { getSelectElement('audioselect').onchange = function(e) {
@ -474,7 +468,7 @@ getSelectElement('audioselect').onchange = function(e) {
if(!(this instanceof HTMLSelectElement)) if(!(this instanceof HTMLSelectElement))
throw new Error('Unexpected type for this'); throw new Error('Unexpected type for this');
updateSettings({audio: this.value}); updateSettings({audio: this.value});
changePresentation(); replaceCameraStream();
}; };
getInputElement('mirrorbox').onchange = function(e) { getInputElement('mirrorbox').onchange = function(e) {
@ -482,7 +476,8 @@ getInputElement('mirrorbox').onchange = function(e) {
if(!(this instanceof HTMLInputElement)) if(!(this instanceof HTMLInputElement))
throw new Error('Unexpected type for this'); throw new Error('Unexpected type for this');
updateSettings({mirrorView: this.checked}); updateSettings({mirrorView: this.checked});
changePresentation(); // no need to reopen the camera
replaceUpStreams('camera');
}; };
getInputElement('blackboardbox').onchange = function(e) { getInputElement('blackboardbox').onchange = function(e) {
@ -490,7 +485,7 @@ getInputElement('blackboardbox').onchange = function(e) {
if(!(this instanceof HTMLInputElement)) if(!(this instanceof HTMLInputElement))
throw new Error('Unexpected type for this'); throw new Error('Unexpected type for this');
updateSettings({blackboardMode: this.checked}); updateSettings({blackboardMode: this.checked});
changePresentation(); replaceCameraStream();
}; };
document.getElementById('mutebutton').onclick = function(e) { document.getElementById('mutebutton').onclick = function(e) {
@ -509,7 +504,8 @@ getSelectElement('filterselect').onchange = async function(e) {
if(!(this instanceof HTMLSelectElement)) if(!(this instanceof HTMLSelectElement))
throw new Error('Unexpected type for this'); throw new Error('Unexpected type for this');
updateSettings({filter: this.value}); updateSettings({filter: this.value});
changePresentation(); // no need to reopen the camera
replaceUpStreams('camera');
}; };
/** @returns {number} */ /** @returns {number} */
@ -944,31 +940,37 @@ Filter.prototype.stop = function() {
}; };
/** /**
* Removes any filter set on c.
*
* @param {Stream} c * @param {Stream} c
* @param {Filter} f
*/ */
function setFilter(c, f) { function removeFilter(c) {
if(!f) { let old = c.userdata.filter;
let filter = c.userdata.filter; if(!old)
if(!filter) return;
return null;
if(!(filter instanceof Filter)) if(!(old instanceof Filter))
throw new Error('userdata.filter is not a filter'); throw new Error('userdata.filter is not a filter');
if(c.userdata.filter) {
c.stream = c.userdata.filter.inputStream; c.stream = old.inputStream;
c.userdata.filter.stop(); old.stop();
c.userdata.filter = null; c.userdata.filter = null;
} }
return
}
if(c.userdata.filter) /**
setFilter(c, null); * Sets the filter described by c.userdata.filterDefinition on c.
*
* @param {Stream} c
*/
function setFilter(c) {
removeFilter(c);
if(f.inputStream != c.stream) if(!c.userdata.filterDefinition)
throw new Error('Setting filter for wrong stream'); return;
c.stream = f.outputStream;
c.userdata.filter = f; let filter = new Filter(c.stream, c.userdata.filterDefinition);
c.stream = filter.outputStream;
c.userdata.filter = filter;
} }
/** /**
@ -1021,11 +1023,56 @@ function isSafari() {
} }
/** /**
* Sets up c to send the given stream. Some extra parameters are stored
* in c.userdata.
*
* @param {Stream} c * @param {Stream} c
* @param {MediaStreamTrack} t
* @param {MediaStream} stream * @param {MediaStream} stream
*/ */
function addUpTrack(c, t, stream) { function setUpStream(c, stream) {
if(c.stream != null)
throw new Error("Setting nonempty stream");
c.stream = stream;
try {
setFilter(c);
} catch(e) {
displayWarning("Couldn't set filter: " + e);
}
c.onclose = replace => {
removeFilter(c);
if(!replace) {
stopStream(c.stream);
if(c.userdata.onclose)
c.userdata.onclose.call(c);
delMedia(c.localId);
}
}
/**
* @param {MediaStreamTrack} t
*/
function addUpTrack(t) {
if(c.label === 'camera') {
let settings = getSettings();
if(t.kind == 'audio') {
if(settings.localMute)
t.enabled = false;
} else if(t.kind == 'video') {
if(settings.blackboardMode) {
/** @ts-ignore */
t.contentHint = 'detail';
}
}
}
t.onended = e => {
stream.onaddtrack = null;
stream.onremovetrack = null;
c.close();
};
let encodings = [{}]; let encodings = [{}];
if(t.kind === 'video') { if(t.kind === 'video') {
let bps = getMaxVideoThroughput(); let bps = getMaxVideoThroughput();
@ -1039,6 +1086,94 @@ function addUpTrack(c, t, stream) {
}); });
} }
// c.stream might be different from stream if there's a filter
c.stream.getTracks().forEach(addUpTrack);
stream.onaddtrack = function(e) {
addUpTrack(e.track);
};
stream.onremovetrack = function(e) {
let t = e.track;
/** @type {RTCRtpSender} */
let sender;
c.pc.getSenders().forEach(s => {
if(s.track === t)
sender = s;
});
if(sender) {
c.pc.removeTrack(sender);
} else {
console.warn('Removing unknown track');
}
let found = false;
c.pc.getSenders().forEach(s => {
if(s.track)
found = true;
});
if(!found) {
stream.onaddtrack = null;
stream.onremovetrack = null;
c.close();
}
};
c.onstats = gotUpStats;
c.setStatsInterval(2000);
}
/**
* Replaces c with a freshly created stream, duplicating any relevant
* parameters in c.userdata.
*
* @param {Stream} c
* @returns {Promise<Stream>}
*/
async function replaceUpStream(c) {
removeFilter(c);
let cn = newUpStream(c.localId);
cn.label = c.label;
if(c.userdata.filterDefinition)
cn.userdata.filterDefinition = c.userdata.filterDefinition;
if(c.userdata.onclose)
cn.userdata.onclose = c.userdata.onclose;
let media = /** @type{HTMLVideoElement} */
(document.getElementById('media-' + c.localId));
setUpStream(cn, c.stream);
await setMedia(c, true,
c.label == 'camera' && getSettings().mirrorView,
c.label == 'video' && media);
return cn;
}
/**
* Replaces all up streams with the given label. If label is null,
* replaces all up stream.
*
* @param {string} label
*/
async function replaceUpStreams(label) {
let promises = [];
for(let id in serverConnection.up) {
let c = serverConnection.up[id];
if(label && c.label !== label)
continue
promises.push(replaceUpStream(c));
}
await Promise.all(promises);
}
/**
* Closes and reopens the camera then replaces the camera stream.
*/
function replaceCameraStream() {
let c = findUpMedia('camera');
if(c)
addLocalMedia(c.localId);
}
/** /**
* @param {string} [localId] * @param {string} [localId]
*/ */
@ -1048,14 +1183,6 @@ async function addLocalMedia(localId) {
let audio = settings.audio ? {deviceId: settings.audio} : false; let audio = settings.audio ? {deviceId: settings.audio} : false;
let video = settings.video ? {deviceId: settings.video} : false; let video = settings.video ? {deviceId: settings.video} : false;
let filter = null;
if(settings.filter) {
filter = filters[settings.filter];
if(!filter) {
displayWarning(`Unknown filter ${settings.filter}`);
}
}
if(video) { if(video) {
let resolution = settings.resolution; let resolution = settings.resolution;
if(resolution) { if(resolution) {
@ -1095,51 +1222,17 @@ async function addLocalMedia(localId) {
return; return;
} }
c.stream = stream;
c.label = 'camera'; c.label = 'camera';
if(filter) { if(settings.filter) {
try { let filter = filters[settings.filter];
let f = new Filter(stream, filter); if(filter)
setFilter(c, f); c.userdata.filterDefinition = filter;
c.onclose = replace => { else
stopStream(stream); displayWarning(`Unknown filter ${settings.filter}`);
setFilter(c, null);
if(!replace)
delMedia(c.localId);
}
} catch(e) {
displayWarning(e);
c.onclose = replace => {
stopStream(c.stream);
if(!replace)
delMedia(c.localId);
}
}
} else {
c.onclose = replace => {
stopStream(c.stream);
if(!replace)
delMedia(c.localId);
}
} }
let mute = getSettings().localMute; setUpStream(c, stream);
c.stream.getTracks().forEach(t => {
if(t.kind == 'audio') {
if(mute)
t.enabled = false;
} else if(t.kind == 'video') {
if(settings.blackboardMode) {
/** @ts-ignore */
t.contentHint = 'detail';
}
}
addUpTrack(c, t, stream);
});
c.onstats = gotUpStats;
c.setStatsInterval(2000);
await setMedia(c, true, settings.mirrorView); await setMedia(c, true, settings.mirrorView);
setButtonsVisibility(); setButtonsVisibility();
} }
@ -1168,19 +1261,8 @@ async function addShareMedia() {
} }
let c = newUpStream(); let c = newUpStream();
c.stream = stream;
c.label = 'screenshare'; c.label = 'screenshare';
c.onclose = replace => { setUpStream(c, stream);
stopStream(stream);
if(!replace)
delMedia(c.localId);
}
stream.getTracks().forEach(t => {
addUpTrack(c, t, stream)
t.onended = e => c.close();
});
c.onstats = gotUpStats;
c.setStatsInterval(2000);
await setMedia(c, true); await setMedia(c, true);
setButtonsVisibility(); setButtonsVisibility();
} }
@ -1208,60 +1290,24 @@ async function addFileMedia(file) {
} }
let c = newUpStream(); let c = newUpStream();
c.stream = stream;
c.label = 'video'; c.label = 'video';
c.onclose = function(replace) { c.userdata.onclose = function() {
stopStream(c.stream);
let media = /** @type{HTMLVideoElement} */ let media = /** @type{HTMLVideoElement} */
(document.getElementById('media-' + this.localId)); (document.getElementById('media-' + this.localId));
if(media && media.src) { if(media && media.src) {
URL.revokeObjectURL(media.src); URL.revokeObjectURL(media.src);
media.src = null; media.src = null;
} }
if(!replace)
delMedia(c.localId);
}; };
await setUpStream(c, stream);
stream.onaddtrack = function(e) {
let t = e.track;
if(t.kind === 'audio') {
let presenting = !!findUpMedia('camera'); let presenting = !!findUpMedia('camera');
let muted = getSettings().localMute; let muted = getSettings().localMute;
if(presenting && !muted) { if(presenting && !muted) {
setLocalMute(true, true); setLocalMute(true, true);
displayWarning('You have been muted'); displayWarning('You have been muted');
} }
}
addUpTrack(c, t, stream);
c.onstats = gotUpStats;
c.setStatsInterval(2000);
};
stream.onremovetrack = function(e) {
let t = e.track;
/** @type {RTCRtpSender} */
let sender;
c.pc.getSenders().forEach(s => {
if(s.track === t)
sender = s;
});
if(sender) {
c.pc.removeTrack(sender);
} else {
console.warn('Removing unknown track');
}
let found = false;
c.pc.getSenders().forEach(s => {
if(s.track)
found = true;
});
if(!found) {
stream.onaddtrack = null;
stream.onremovetrack = null;
c.close();
}
};
await setMedia(c, true, false, video); await setMedia(c, true, false, video);
c.userdata.play = true; c.userdata.play = true;
setButtonsVisibility(); setButtonsVisibility();
@ -1352,11 +1398,7 @@ async function setMedia(c, isUp, mirror, video) {
let media = /** @type {HTMLVideoElement} */ let media = /** @type {HTMLVideoElement} */
(document.getElementById('media-' + c.localId)); (document.getElementById('media-' + c.localId));
if(media) { if(!media) {
if(video) {
throw new Error("Duplicate video");
}
} else {
if(video) { if(video) {
media = video; media = video;
} else { } else {
@ -1464,7 +1506,7 @@ function addCustomControls(media, container, c, toponly) {
topcontrols.id = 'topcontrols-' + c.localId; topcontrols.id = 'topcontrols-' + c.localId;
container.appendChild(topcontrols); container.appendChild(topcontrols);
} }
registerControlHandlers(media, container, c); registerControlHandlers(c.localId, media, container);
} }
/** /**
@ -1495,11 +1537,11 @@ function setVolumeButton(muted, button, slider) {
} }
/** /**
* @param {string} localId
* @param {HTMLVideoElement} media * @param {HTMLVideoElement} media
* @param {HTMLElement} container * @param {HTMLElement} container
* @param {Stream} c
*/ */
function registerControlHandlers(media, container, c) { function registerControlHandlers(localId, media, container) {
let play = getVideoButton(container, 'video-play'); let play = getVideoButton(container, 'video-play');
if(play) { if(play) {
play.onclick = function(event) { play.onclick = function(event) {
@ -1513,6 +1555,9 @@ function registerControlHandlers(media, container, c) {
stop.onclick = function(event) { stop.onclick = function(event) {
event.preventDefault(); event.preventDefault();
try { try {
let c = serverConnection.findByLocalId(localId);
if(!c)
throw new Error('Closing unknown stream');
c.close(); c.close();
} catch(e) { } catch(e) {
console.error(e); console.error(e);
@ -2243,6 +2288,13 @@ commands.renegotiate = {
} }
}; };
commands.replace = {
f: (c, r) => {
replaceUpStreams(null);
}
};
/** /**
* parseCommand splits a string into two space-separated parts. The first * parseCommand splits a string into two space-separated parts. The first
* part may be quoted and may include backslash escapes. * part may be quoted and may include backslash escapes.