svc support in whip.html; pixel preview freezing fix; whep improvements; motion recorder

This commit is contained in:
steveseguin
2023-10-20 23:54:13 -04:00
parent 782ccda08f
commit 0a53b23195
6 changed files with 594 additions and 238 deletions

View File

@@ -90,9 +90,9 @@
<link itemprop="url" href="./media/vdoNinja_logo_full.png" />
</span>
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/CodecsHandler.js?ver=49"></script>
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/CodecsHandler.js?ver=6"></script>
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/aes.js"></script>
<script type="text/javascript" crossorigin="anonymous" src="./webrtc.js?ver=703"></script>
<script type="text/javascript" crossorigin="anonymous" src="./webrtc.js?ver=706"></script>
<input id="zoomSlider" type="range" style="display: none;" />
<span id="electronDragZone" style="pointer-events: none; z-index:-10; position:absolute;top:0;left:0;width:100%;height:2%;-webkit-app-region: drag;min-height:20px;"></span>
<div id="header">
@@ -2638,11 +2638,11 @@
// session.record = false; // uncomment to block users from being able to record via vdo.ninja's built in recording function
// session.whipServerURL = "wss://whip.vdo.ninja"; // If you deploy your own whip websocket service
</script>
<script type="text/javascript" crossorigin="anonymous" id="lib-js" src="./lib.js?ver=929"></script>
<script type="text/javascript" crossorigin="anonymous" id="lib-js" src="./lib.js?ver=939"></script>
<!--
// If you wish to change branding, blank offers a good clean start.
<script type="text/javascript" id="main-js" src="./main.js" data-translation="blank"></script>
-->
<script type="text/javascript" crossorigin="anonymous" id="main-js" src="./main.js?ver=747"></script>
<script type="text/javascript" crossorigin="anonymous" id="main-js" src="./main.js?ver=751"></script>
</body>
</html>

498
lib.js
View File

@@ -539,6 +539,8 @@ try{
}
}
} catch(e){errorlog(e);}
if (session.audioCtx && session.audioCtx.sampleRate && (session.audioCtx.sampleRate > 192000)){
@@ -6632,6 +6634,14 @@ function sleep(ms = 0) {
return new Promise(r => setTimeout(r, ms)); // LOLz!
}
function sleepCancellable(ms = 0) {
let resolve;
const promise = new Promise(r => {
resolve = r;
setTimeout(resolve, ms);
});
return { promise, resolve };
}
async function changeAvatarImage(ev, ele, set=false){
log("changeAvatarImage() triggered");
@@ -7363,7 +7373,7 @@ function motionDetection(video, threshold = 15, sensitivity=75){
}
if (matches >= threshold){
log("MOTION DETECTED: "+matches);
if (window.obsstudio && window.obsstudio["setCurrentScene"]){
if (session.motionSwitch && window.obsstudio && window.obsstudio["setCurrentScene"]){
if (!changeSceneEnabled){ // the bit cut scene change is already active.
if (session.obsState && session.obsState.details && session.obsState.details.thisScene && session.obsState.details.currentScene){
if (session.obsState.details.thisScene !== session.obsState.details.currentScene.name){ // don't trigger it multiple times; makes it hard to prep next scene
@@ -7380,6 +7390,16 @@ function motionDetection(video, threshold = 15, sensitivity=75){
updateMixer();
}
}
if (session.motionRecord){
if (!session.motionRecordTimeout){
session.motionRecordTimeout = setTimeout(function(){
session.motionRecordTimeout = null;
},1000);
saveVideoFrameToClipboard(video);
}
}
}
}
@@ -9306,11 +9326,9 @@ function processStats(UUID){
}
}
}
} catch (e){};
} catch (e){};
if (session.rpcs[UUID].whep){
processMeshcastStats(UUID);
}
try {
@@ -9320,6 +9338,7 @@ function processStats(UUID){
var node = session.rpcs[UUID];
}
var validTrackIds = [];
if (session.rpcs[UUID].streamSrc){
session.rpcs[UUID].streamSrc.getTracks().forEach(trk=>{
@@ -9327,6 +9346,13 @@ function processStats(UUID){
});
}
if (session.rpcs[UUID].whep){
processMeshcastStats(UUID);
if (!node.getStats){
setTimeout(processStats, session.statsInterval, UUID); // no p2p, so lets do WHEP again manually.
}
}
if (node.getStats){
node.getStats().then(function(stats){
if (!(UUID in session.rpcs)){return;}
@@ -9853,38 +9879,41 @@ function playoutdelay(UUID){ // applies a delay to all videos
var sync_offset = 0.0;
if (session.rpcs[UUID].stats[tid]._sync_offset){
sync_offset = session.rpcs[UUID].stats[tid]._sync_offset;
sync_offset = session.rpcs[UUID].stats[tid]._sync_offset || 0;
} else {
session.rpcs[UUID].stats[tid]._sync_offset = 0;
}
sync_offset += target_buffer;
sync_offset -= session.rpcs[UUID].stats[tid].Jitter_Buffer_ms;
sync_offset -= session.rpcs[UUID].stats[tid].Jitter_Buffer_ms || 0;
if (session.includeRTT){
sync_offset -= parseInt(session.rpcs[UUID].stats['Peer-to-Peer_Connection'].Round_Trip_Time_ms/2); // I can't be sure what the actual one-way delay is
if (session.includeRTT && session.rpcs[UUID].stats['Peer-to-Peer_Connection']){
sync_offset -= parseInt(session.rpcs[UUID].stats['Peer-to-Peer_Connection'].Round_Trip_Time_ms/2) || 0; // I can't be sure what the actual one-way delay is
}
if (sync_offset>target_buffer){
sync_offset=target_buffer;
if (sync_offset > target_buffer){
sync_offset = target_buffer || 0;
}
if (sync_offset<0){sync_offset=0;}
session.rpcs[UUID].stats[tid].Added_Buffer_Delay_ms = sync_offset;
session.rpcs[UUID].stats[tid].Added_Buffer_Delay_ms = sync_offset || 0;
session.rpcs[UUID].stats[tid].Total_Playout_Delay_ms = sync_offset + parseInt(session.rpcs[UUID].stats['Peer-to-Peer_Connection'].Round_Trip_Time_ms/2) + session.rpcs[UUID].stats[tid].Jitter_Buffer_ms;
if (session.rpcs[UUID].stats['Peer-to-Peer_Connection']){
session.rpcs[UUID].stats[tid].Total_Playout_Delay_ms = (sync_offset + parseInt(session.rpcs[UUID].stats['Peer-to-Peer_Connection'].Round_Trip_Time_ms/2) + session.rpcs[UUID].stats[tid].Jitter_Buffer_ms) || 0;
}
if (session.rpcs[UUID].stats[tid]._type=="audio"){
session.rpcs[UUID].stats[tid]._sync_offset = sync_offset;
session.rpcs[UUID].stats[tid]._sync_offset = sync_offset || 0;
receiver.playoutDelayHint = parseFloat(sync_offset/1000);
receiver.playoutDelayHint = parseFloat(sync_offset/1000) || 0;
// receiver.jitterBufferDelayhint = parseFloat(sync_offset/1000); // This is deprecated I believe
if (session.sync!==false){
var audio_delay = session.sync || 0; // video is typically showing greater delay than audio.
audio_delay += target_buffer - session.rpcs[UUID].stats[tid].Jitter_Buffer_ms;
audio_delay += (target_buffer - session.rpcs[UUID].stats[tid].Jitter_Buffer_ms) || 0;
if ((receiver.track.kind=="audio") && (receiver.track.id in session.rpcs[UUID].inboundAudioPipeline)){
if (session.rpcs[UUID].inboundAudioPipeline[receiver.track.id] && session.rpcs[UUID].inboundAudioPipeline[receiver.track.id].delayNode){
@@ -9894,14 +9923,14 @@ function playoutdelay(UUID){ // applies a delay to all videos
} catch(e){
session.rpcs[UUID].inboundAudioPipeline[receiver.track.id].delayNode.delayTime.setValueAtTime(parseFloat(audio_delay/1000.0), session.audioCtx.currentTime+1);
}
session.rpcs[UUID].stats[tid].Audio_Sync_Delay_ms = audio_delay;
session.rpcs[UUID].stats[tid].Audio_Sync_Delay_ms = audio_delay || 0;
}
}
}
} else if (session.rpcs[UUID].stats[tid]._type=="video"){
session.rpcs[UUID].stats[tid]._sync_offset = sync_offset;
receiver.playoutDelayHint = parseFloat(sync_offset/1000); // Chrome seems to somewhat sync audio and video when using the delay
session.rpcs[UUID].stats[tid]._sync_offset = sync_offset || 0;
receiver.playoutDelayHint = parseFloat(sync_offset/1000) || 0; // Chrome seems to somewhat sync audio and video when using the delay
// receiver.jitterBufferDelayhint = parseFloat(sync_offset/1000); // This is deprecated I believe
}
@@ -10163,8 +10192,8 @@ function processMeshcastStats(UUID){
session.rpcs[UUID].whep.getStats().then(function(stats){
if (!(UUID in session.rpcs)){return;}
if (!session.rpcs[UUID].stats['Meshcast_Connection']){
session.rpcs[UUID].stats['Meshcast_Connection'] = {};
if (!session.rpcs[UUID].stats['WHEP_Connection']){ // meshcast
session.rpcs[UUID].stats['WHEP_Connection'] = {};
}
// var qos = false;]
@@ -10221,21 +10250,21 @@ function processMeshcastStats(UUID){
}
} else if (stat.type == "transport"){
if ("bytesReceived" in stat) {
if ("_bytesReceived" in session.rpcs[UUID].stats['Meshcast_Connection']){
if (session.rpcs[UUID].stats['Meshcast_Connection']._timestamp){
if ("_bytesReceived" in session.rpcs[UUID].stats['WHEP_Connection']){
if (session.rpcs[UUID].stats['WHEP_Connection']._timestamp){
if (stat.timestamp){
session.rpcs[UUID].stats['Meshcast_Connection'].total_recv_bitrate_kbps = parseInt(8*(stat.bytesReceived - session.rpcs[UUID].stats['Meshcast_Connection']._bytesReceived)/(stat.timestamp - session.rpcs[UUID].stats['Meshcast_Connection']._timestamp));
session.rpcs[UUID].stats['WHEP_Connection'].total_recv_bitrate_kbps = parseInt(8*(stat.bytesReceived - session.rpcs[UUID].stats['WHEP_Connection']._bytesReceived)/(stat.timestamp - session.rpcs[UUID].stats['WHEP_Connection']._timestamp));
}
}
}
session.rpcs[UUID].stats['Meshcast_Connection']._bytesReceived = stat.bytesReceived;
session.rpcs[UUID].stats['WHEP_Connection']._bytesReceived = stat.bytesReceived;
}
if ("timestamp" in stat) {
session.rpcs[UUID].stats['Meshcast_Connection']._timestamp = stat.timestamp;
if (!session.rpcs[UUID].stats['Meshcast_Connection']._timestampStart){
session.rpcs[UUID].stats['Meshcast_Connection']._timestampStart = stat.timestamp;
session.rpcs[UUID].stats['WHEP_Connection']._timestamp = stat.timestamp;
if (!session.rpcs[UUID].stats['WHEP_Connection']._timestampStart){
session.rpcs[UUID].stats['WHEP_Connection']._timestampStart = stat.timestamp;
} else {
session.rpcs[UUID].stats['Meshcast_Connection'].time_active_minutes = parseInt((stat.timestamp - session.rpcs[UUID].stats['Meshcast_Connection']._timestampStart)/600)/100;
session.rpcs[UUID].stats['WHEP_Connection'].time_active_minutes = parseInt((stat.timestamp - session.rpcs[UUID].stats['WHEP_Connection']._timestampStart)/600)/100;
}
}
@@ -10424,7 +10453,7 @@ function processMeshcastStats(UUID){
if (nominatedCandidate){
if ("currentRoundTripTime" in nominatedCandidate){
session.rpcs[UUID].stats['Meshcast_Connection'].Round_Trip_Time_ms = nominatedCandidate.currentRoundTripTime*1000;
session.rpcs[UUID].stats['WHEP_Connection'].Round_Trip_Time_ms = nominatedCandidate.currentRoundTripTime*1000;
}
}
@@ -10432,20 +10461,20 @@ function processMeshcastStats(UUID){
if (candidates[nominatedCandidate.remoteCandidateId]){
var candidate = candidates[nominatedCandidate.remoteCandidateId];
if ("candidateType" in candidate){
session.rpcs[UUID].stats['Meshcast_Connection'].remote_candidateType = candidate.candidateType;
session.rpcs[UUID].stats['WHEP_Connection'].remote_candidateType = candidate.candidateType;
if (candidate.candidateType === "relay"){
if ("relayProtocol" in candidate){
session.rpcs[UUID].stats['Meshcast_Connection'].remote_relay_protocol = candidate.relayProtocol;
session.rpcs[UUID].stats['WHEP_Connection'].remote_relay_protocol = candidate.relayProtocol;
}
if ("ip" in candidate){session.rpcs[UUID].stats['Meshcast_Connection'].remote_relay_IP = candidate.ip;}
if ("ip" in candidate){session.rpcs[UUID].stats['WHEP_Connection'].remote_relay_IP = candidate.ip;}
} else {
try {
delete session.rpcs[UUID].stats['Meshcast_Connection'].local_relay_IP;
delete session.rpcs[UUID].stats['Meshcast_Connection'].local_relay_protocol;
delete session.rpcs[UUID].stats['WHEP_Connection'].local_relay_IP;
delete session.rpcs[UUID].stats['WHEP_Connection'].local_relay_protocol;
} catch(e){}
}
if ("networkType" in candidate){
session.rpcs[UUID].stats['Meshcast_Connection'].remote_networkType = candidate.networkType;
session.rpcs[UUID].stats['WHEP_Connection'].remote_networkType = candidate.networkType;
}
}
}
@@ -10454,22 +10483,22 @@ function processMeshcastStats(UUID){
if (candidates[nominatedCandidate.localCandidateId]){
var candidate = candidates[nominatedCandidate.localCandidateId];
if ("candidateType" in candidate){
session.rpcs[UUID].stats['Meshcast_Connection'].local_candidateType = candidate.candidateType;
session.rpcs[UUID].stats['WHEP_Connection'].local_candidateType = candidate.candidateType;
if (candidate.candidateType === "relay"){
if ("relayProtocol" in candidate){
session.rpcs[UUID].stats['Meshcast_Connection'].local_relay_protocol = candidate.relayProtocol;
session.rpcs[UUID].stats['WHEP_Connection'].local_relay_protocol = candidate.relayProtocol;
}
if ("ip" in candidate){session.rpcs[UUID].stats['Meshcast_Connection'].local_relay_IP = candidate.ip;}
if ("ip" in candidate){session.rpcs[UUID].stats['WHEP_Connection'].local_relay_IP = candidate.ip;}
} else {
try {
delete session.rpcs[UUID].stats['Meshcast_Connection'].local_relay_IP;
delete session.rpcs[UUID].stats['Meshcast_Connection'].local_relay_protocol;
delete session.rpcs[UUID].stats['WHEP_Connection'].local_relay_IP;
delete session.rpcs[UUID].stats['WHEP_Connection'].local_relay_protocol;
} catch(e){}
}
}
if ("networkType" in candidate){
session.rpcs[UUID].stats['Meshcast_Connection'].local_networkType = candidate.networkType;
session.rpcs[UUID].stats['WHEP_Connection'].local_networkType = candidate.networkType;
}
}
}
@@ -14433,7 +14462,7 @@ function publishWebcam(btn = false, miconly=false) {
}, 200);
};
if ((session.roomid === "") && ((!(session.view)) || (session.view === ""))) {
if ((session.roomid === "") && ((!session.view) || (session.view === ""))) {
// no room, no viewing, viewing disabled
if (session.manual===null){
session.manual = true;
@@ -15117,7 +15146,7 @@ session.publishIFrame = function(iframeURL){
}
if (session.roomid!==false){
if ((session.roomid==="") && ((!(session.view)) || (session.view===""))){
if ((session.roomid==="") && ((!session.view) || (session.view===""))){
} else {
log("ROOMID EANBLED");
@@ -15159,7 +15188,7 @@ session.publishIFrame = function(iframeURL){
updateMixer();
} else if (session.roomid!==false){
if (session.roomid===""){
if (!(session.view) || (session.view==="")){
if (!session.view || (session.view==="")){
session.windowed = true;
container.classList.add("vidcon");
@@ -16597,13 +16626,18 @@ function saveVideoFrameToClipboard(videoElement, e=false) {
canvas.toBlob(function(blob) {
var link = document.createElement("a");
link.download = (videoElement.id||"video")+"_"+parseInt(performance.now())+".png";
if (e){
link.download = (videoElement.id||"video")+"_"+parseInt(performance.now())+".png";
} else {
link.download = (videoElement.id||"video")+"_"+parseInt(Date.now())+".png";
}
link.href = URL.createObjectURL(blob);
link.click();
URL.revokeObjectURL(link.href);
}, 'image/png');
popupMessage(e, "Saving current frame to disk");
if (e){
popupMessage(e, "Saving current frame to disk");
}
} catch(e){
errorlog(e);
}
@@ -21835,16 +21869,6 @@ async function grabScreen(quality = 0, audio = true, videoOnEnd = false) {
}
function toggleBufferSettings(UUID){
//bufferSliderValue
/* try {
session.rpcs[taskItemInContext.dataset.UUID].buffer = parseInt(inputElement.value);
inputElement.title = session.rpcs[taskItemInContext.dataset.UUID].buffer + " ms";
getById("bufferSliderValue").innerText = session.rpcs[taskItemInContext.dataset.UUID].buffer + " ms";
playoutdelay(taskItemInContext.dataset.UUID); // trigger
} catch(e){
errorlog(e);
*/
toggle(getById('bufferSettings'));
if (getById('bufferSettings').style.display=="none"){
@@ -21879,6 +21903,19 @@ function toggleBufferSettings(UUID){
});
playoutdelay(UUID); // trigger
};
ele.onkeyup = function(e){
if (e.key === "Enter") {
session.rpcs[UUID].buffer = parseInt(e.target.value);
//getById("bufferSliderValue").innerText = session.rpcs[UUID].buffer + " ms";
getById('bufferSettings').querySelectorAll("input").forEach(ele2=>{
if (ele2!==e.target){
ele2.value = parseInt(e.target.value);
}
ele2.title = parseInt(e.target.value) + " ms";
});
playoutdelay(UUID); // trigger
}
};
ele.oninput = function(e){
getById('bufferSettings').querySelectorAll("input").forEach(ele2=>{
if (ele2!==e.target){
@@ -22126,6 +22163,30 @@ function checkBasicStreamsExist(){
resetupAudioOut(session.videoElement, true);
}, { once: true });
session.videoElement.onpause = (event) => { // prevent things from pausing; human or other
if (!((event.ctrlKey) || (event.metaKey) )){
log("Video paused; auto playing");
event.currentTarget.play().then(_ => {
log("playing 10");
}).catch(warnlog);
}
};
session.videoElement.addEventListener('error', function(event) {
errorlog("video error");
errorlog(event);
setTimeout(function(){
if (session.videoElement){
log("Trying to re-load local preview, as it may have crashed");
session.videoElement.load();
}
},1200);
});
//session.videoElement.addEventListener('loadedmetadata', function(event) {
// log("loadedmetadata");
// log(event);
//});
}
session.videoElement.srcObject = outboundAudioPipeline();
toggleMute(true);
@@ -22621,9 +22682,9 @@ async function grabVideo(quality = 0, eleName = 'previewWebcam', selector = "sel
applyMirror(session.mirrorExclude);
session.videoElement.play().then(() => {
log("start play doublecheck");
log("play doublecheck completed");
});
if ((eleName == "previewWebcam") && document.getElementById("previewWebcam")){
if (session.autostart) {
publishWebcam();
@@ -22686,6 +22747,7 @@ async function grabVideo(quality = 0, eleName = 'previewWebcam', selector = "sel
if (getUserMediaRequestID !== gumid) { // new camera selected in this time.
return;
}
makeImages(true);
if (getById("popupSelector_constraints_loading")) {
@@ -23040,7 +23102,12 @@ async function grabAudio(selector = "#audioSource", trackid = null, override = f
log("TRACK EXCLUDED:" + trackid);
try {
var audioSelect = document.querySelector(selector).querySelectorAll("input");
var baseTest = document.querySelector(selector);
if (!baseTest){
errorlog("No audio source menu");
return;
}
var audioSelect = baseTest.querySelectorAll("input");
var audioExcludeList = [];
for (var i = 0; i < audioSelect.length; i++) {
try {
@@ -24238,15 +24305,6 @@ session.publishStream = function(v){ // stream is used to generated an SDP
}
v.onpause = (event) => { // prevent things from pausing; human or other
if (!((event.ctrlKey) || (event.metaKey) )){
log("Video paused; auto playing");
event.currentTarget.play().then(_ => {
log("playing 10");
}).catch(warnlog);
}
};
v.addEventListener('click', function(e) {
log("click");
try {
@@ -24424,6 +24482,14 @@ session.postPublish = async function(){
whepOut();
}
if (session.motionRecord && session.videoElement && !session.motionDetectionInterval){
session.motionDetectionInterval = setTimeout(function(){
setInterval(function(){
motionDetection(session.videoElement, session.motionRecord);
},400);
},2000);
}
}
@@ -24575,7 +24641,7 @@ async function publishScreen2(constraints, audioList=[], audio=true, overrideFra
// OR, jump right in, and let user change from there
if (session.roomid!==false){
if ((session.roomid==="") && ((!(session.view)) || (session.view===""))){
if ((session.roomid==="") && ((!session.view) || (session.view===""))){
} else {
getById("head3").classList.add('hidden');
@@ -24673,7 +24739,7 @@ async function publishScreen2(constraints, audioList=[], audio=true, overrideFra
setTimeout(function(){updateMixer();},1);
} else if (session.roomid!==false){
if (session.roomid===""){
if (!(session.view) || (session.view==="")){
if (!session.view || (session.view==="")){
getById("mutespeakerbutton").classList.add("hidden");
@@ -25108,7 +25174,7 @@ session.hostFile = function(ele, event){ // webcam stream is used to generated a
}
if (session.roomid!==false){
if ((session.roomid==="") && ((!(session.view)) || (session.view===""))){
if ((session.roomid==="") && ((!session.view) || (session.view===""))){
} else {
log("ROOMID EANBLED");
@@ -25279,7 +25345,7 @@ session.publishFile = function(ele, event){ // webcam stream is used to generate
if (session.roomid!==false){
if ((session.roomid==="") && ((!(session.view)) || (session.view===""))){
if ((session.roomid==="") && ((!session.view) || (session.view===""))){
} else {
log("ROOMID EANBLED");
@@ -25375,7 +25441,7 @@ session.publishFile = function(ele, event){ // webcam stream is used to generate
} else if (session.roomid!==false){
if (session.roomid===""){
if (!(session.view) || (session.view==="")){
if (!session.view || (session.view==="")){
if (session.fullscreen){
session.windowed = false;
} else {
@@ -29610,8 +29676,8 @@ function setupWebcamSelection(miconly=false) {
activatedPreview = false;
if (session.quality !== false) {
grabVideo(session.quality);
} else {
session.quality_wb = parseInt(getById("webcamquality").elements.namedItem("resolution").value);
} else if (document.getElementById("webcamquality")){
session.quality_wb = parseInt(document.getElementById("webcamquality").elements.namedItem("resolution").value);
grabVideo(session.quality_wb);
}
};
@@ -29732,15 +29798,13 @@ function setupWebcamSelection(miconly=false) {
if (session.autostart) {
publishWebcam(); // no need to mirror as there is no video...
return;
} else {
if (document.getElementById("gowebcam")) {
document.getElementById("gowebcam").dataset.ready = "true";
if (document.getElementById("gowebcam").dataset.audioready == "true"){
document.getElementById("gowebcam").disabled = false;
miniTranslate(document.getElementById("gowebcam"),"start");
document.getElementById("gowebcam").focus();
//document.getElementById("gowebcam").innerHTML = getTranslation("start");
}
} else if (document.getElementById("gowebcam")) {
document.getElementById("gowebcam").dataset.ready = "true";
if (document.getElementById("gowebcam").dataset.audioready == "true"){
document.getElementById("gowebcam").disabled = false;
miniTranslate(document.getElementById("gowebcam"),"start");
document.getElementById("gowebcam").focus();
//document.getElementById("gowebcam").innerHTML = getTranslation("start");
}
}
} else {
@@ -29748,7 +29812,7 @@ function setupWebcamSelection(miconly=false) {
activatedPreview = false;
if (session.quality !== false) {
grabVideo(session.quality);
} else {
} else if (document.getElementById("webcamquality")){
session.quality_wb = parseInt(getById("webcamquality").elements.namedItem("resolution").value);
grabVideo(session.quality_wb);
}
@@ -33824,10 +33888,10 @@ function updateIncomingVideoElement(UUID, video=true, audio=true){
}
});
if (session.motionSwitch && !session.rpcs[UUID].motionDetectionInterval){
if ((session.motionSwitch || session.motionRecord) && !session.rpcs[UUID].motionDetectionInterval){
session.rpcs[UUID].motionDetectionInterval = setTimeout(function(){
setInterval(function(){
motionDetection(session.rpcs[UUID].videoElement, session.motionSwitch);
motionDetection(session.rpcs[UUID].videoElement, session.motionSwitch || session.motionRecord);
},400);
},2000);
}
@@ -36048,7 +36112,37 @@ function configureWhipOutSDP(description){ // THIS IS FOR WHIP-OUTPUT; it has
return description;
}
const scalabilityModes = [
'L1T1',
'L1T2',
'L1T3',
'L2T1',
'L2T2',
'L2T3',
'L3T1',
'L3T2',
'L3T3',
'L2T1h',
'L2T2h',
'L2T3h',
'S2T1',
'S2T2',
'S2T3',
'S2T1h',
'S2T2h',
'S2T3h',
'S3T1',
'S3T2',
'S3T3',
'S3T1h',
'S3T2h',
'S3T3h',
'L2T2_KEY',
'L2T3_KEY',
'L3T2_KEY',
'L3T3_KEY'
];
function whipOut(){
log("whipOut");
@@ -36079,59 +36173,64 @@ function whipOut(){
}
}
try {
var tracks = false;
if (session.videoElement && session.videoElement.srcObject){
tracks = session.videoElement.srcObject.getAudioTracks();
}
var streamsource = false;
if (!tracks || !tracks.length){
var audioCtx = new AudioContext();
warnlog("No audio track; using a webaudio node instead");
var destination = audioCtx.createMediaStreamDestination();
streamsource = destination.stream;
destination.stream.getAudioTracks().forEach(trk=>{
tracks = trk;
});
} else {
tracks = tracks[0];
streamsource = session.videoElement.srcObject;
}
if (session.audioContentHint && (tracks.kind === "audio")){
try {
tracks.contentHint = session.audioContentHint;
} catch(e){
errorlog(e);
}
}
if (tracks){
try {
session.whipOut.addTransceiver(tracks, {
streams: [ streamsource ],
direction: 'sendonly'
});
} catch(e){
errorlog(e);
session.whipOut.addTrack(tracks);
}
}
//}
///////
try {
/// audio tracks
//// video tracks
var tracks = false;
if (session.videoElement && session.videoElement.srcObject){
tracks = session.videoElement.srcObject.getVideoTracks();
}
////
var tracks = false;
if (session.videoElement && session.videoElement.srcObject){
tracks = session.videoElement.srcObject.getAudioTracks();
}
var streamsource = false;
if (!tracks || !tracks.length){
var audioCtx = new AudioContext();
warnlog("No audio track; using a webaudio node instead");
var destination = audioCtx.createMediaStreamDestination();
streamsource = destination.stream;
destination.stream.getAudioTracks().forEach(trk=>{
tracks = trk;
});
} else {
tracks = tracks[0];
streamsource = session.videoElement.srcObject;
}
if (session.audioContentHint && (tracks.kind === "audio")){
try {
tracks.contentHint = session.audioContentHint;
} catch(e){
errorlog(e);
}
}
if (tracks){
try {
session.whipOut.addTransceiver(tracks, {
streams: [ streamsource ],
direction: 'sendonly'
});
} catch(e){
errorlog(e);
session.whipOut.addTrack(tracks);
}
}
} catch(e){errorlog(e);}
try {
//// video tracks ///
var tracks = false;
if (session.videoElement && session.videoElement.srcObject){
tracks = session.videoElement.srcObject.getVideoTracks();
}
//if (!tracks || !tracks.length){
// tracks = getMeshcastCanvasTrack();
//} else {
// tracks = tracks[0];
//}
if (tracks && tracks.length){
//if (!tracks || !tracks.length){
// tracks = getMeshcastCanvasTrack();
//} else {
tracks = tracks[0];
//}
tracks = tracks[0];
if (session.screenShareState && session.screenshareContentHint && (tracks.kind === "video")){
try {
@@ -36146,18 +36245,56 @@ function whipOut(){
errorlog(e);
}
}
if (tracks){
try {
session.whipOut.addTransceiver(tracks, {
try {
var transceiverSetup = {
streams: [ session.videoElement.srcObject ],
direction: 'sendonly'
});
};
if (session.scalabilityMode){
// might be a good time to validate the scalabilityMode at this point; check to see if requested codec is available,etc.
try {
transceiverSetup.sendEncodings = [{scalabilityMode: session.scalabilityMode}];
} catch(e){
errorlog("Invalid scalability mode provided");
}
if (session.whipOutCodec && session.whipOutCodec.length){
var svcCodecPref = [];
for (var i = 0;i<session.whipOutCodec.length;i++){
svcCodecPref.push({mimeType:"video/"+session.whipOutCodec[i].toUpperCase(), clockRate: 90000}); // is it ever not 90000?
}
try{
session.whipOut.addTransceiver(tracks, transceiverSetup);
let transceiver = session.whipOut.getTransceivers().find(t => t.sender && t.sender.track === tracks);
transceiver.setCodecPreferences(svcCodecPref);
} catch(e){
errorlog(e);
warnUser("Could not configure WHIP output based on specified video encoder/SVC settings.", 5000);
var transceiverSetup = {
streams: [ session.videoElement.srcObject ],
direction: 'sendonly'
};
session.whipOut.addTransceiver(tracks, transceiverSetup);
}
} else {
session.whipOut.addTransceiver(tracks, transceiverSetup);
}
} else {
session.whipOut.addTransceiver(tracks, transceiverSetup);
}
} catch(e){
errorlog(e);
try {
session.whipOut.addTrack(tracks);
} catch(e){
errorlog(e);
session.whipOut.addTrack(tracks);
}
}
//}
}
session.whipOut.onnegotiationneeded = publish; // bug: https://groups.google.com/forum/#!topic/discuss-webrtc/3-TmyjQ2SeE
@@ -36174,7 +36311,6 @@ function whipOut(){
}
var publishing = false;
function publish(event){
if (publishing){
log(event);
@@ -36423,7 +36559,7 @@ function whipClient(){ // publish to whip.vdo.ninja with obs, to use. experimen
var data = JSON.parse(event.data);
if ("sdp" in data){
var resp = await processWHIP(data);
var resp = await processWhipIn(data);
if (resp){
var ret = {};
var get = data.get;
@@ -36446,7 +36582,7 @@ function whipClient(){ // publish to whip.vdo.ninja with obs, to use. experimen
connect();
}
async function processWHIP(data){ // LISTEN FOR REMOTE WHIP
async function processWhipIn(data){ // LISTEN FOR REMOTE WHIP (from OBS?)
var msg = {};
msg.description = {};
msg.description.type = "offer";
@@ -36515,8 +36651,11 @@ async function processWHIP(data){ // LISTEN FOR REMOTE WHIP
await promise;
session.rpcs[msg.UUID].whipCallback = null;
sdpAnswer = session.rpcs[msg.UUID].localDescription.sdp;
// sdpAnswer = processSDPFromServer(sdpAnswer); // the sending server doesn't give a damn if we're in stereo or not.
var insertIce = "";
iceBundle.forEach(ice=>{
if (ice.candidate){
@@ -36525,9 +36664,6 @@ async function processWHIP(data){ // LISTEN FOR REMOTE WHIP
});
sdpAnswer = sdpAnswer.replace("a=ice-ufrag", insertIce+"a=ice-ufrag");
//if (session.stereo){
// sdpAnswer = CodecsHandler.setOpusAttributes(sdpAnswer, {stereo:1}, true);
//}
if (sdpAnswer.includes("sendrecv")){
errorlog("Should not include sendrecv");
@@ -36541,6 +36677,23 @@ async function processWHIP(data){ // LISTEN FOR REMOTE WHIP
return sdpAnswer; // return SDP answer for the remote WHIP request
}
function processSDPFromServer(sdp){ // not the description package; just the sdp
try {
if (session.mono && Firefox){ // chrome defaults to mono already, but we can force Firefox mono if needed
sdp = CodecsHandler.setOpusAttributes(sdp, {stereo:0}, true);
} else if (Firefox){
// Let Firefox be Firefox.. else it might break the server. mediamtx err 400 response if mono
} else if (session.stereo && (session.stereo==4)){ // pro audio only when viewing streams
sdp = CodecsHandler.setOpusAttributes(sdp, {stereo:2}, true);
} else if (session.stereo && !session.mono && (session.stereo!=3)){
sdp = CodecsHandler.setOpusAttributes(sdp, {stereo:1}, true);
}
} catch(e){
errorlog(e);
}
return sdp;
}
async function whepIn(whepInput=false, whepInputToken=false, UUID=false){ // PLAY WHEP
var candidates = [];
var responseLocation = false;
@@ -36574,11 +36727,12 @@ async function whepIn(whepInput=false, whepInputToken=false, UUID=false){ // PLA
session.rpcs[UUID].lockedAudioBitrate = false;
session.rpcs[UUID].manualBandwidth = false; // doesn't do anything, except maybe help keep track of pause/play states
session.rpcs[UUID].motionDetectionInterval = false;
session.rpcs[UUID].buffer = false;
}
var config = {...session.configuration};
if (whepInput.includes("cloudflare")){
config.iceTransportPolicy = "relay"; // oof. Doesn't work with Cloudflare without this?
config.iceTransportPolicy = "relay"; // oof. Doesn't work with Cloudflare without this? REVISIT
}
try {
@@ -36642,7 +36796,11 @@ async function whepIn(whepInput=false, whepInputToken=false, UUID=false){ // PLA
session.rpcs[UUID].whep.onicecandidate = function(event){ //event
if (event.candidate==null){
log("END OF ICE CANDIDATES");
warnlog("END OF ICE CANDIDATES");
if (session.rpcs[UUID].whep.iceCompletedCallback){
console.log("Fully collected all ice candidates in time. woohoo!");
session.rpcs[UUID].whep.iceCompletedCallback();
}
return;
}
//log(event.candidate);
@@ -36665,11 +36823,26 @@ async function whepIn(whepInput=false, whepInputToken=false, UUID=false){ // PLA
try {
session.rpcs[UUID].whep.createOffer().then(async function(offer){
offer.sdp = processSDPFromServer(offer.sdp);
//offer.sdp = CodecsHandler.setOpusAttributes(offer.sdp, {'stereo': 1});
return session.rpcs[UUID].whep.setLocalDescription(offer);
}).then(async function() {
//log(session.rpcs[UUID].whep.localDescription);
await sleep(6000);
console.log("Waiting up to 2-seconds for ice candidates to collect. At least 300ms recommended; at most 30-seconds.");
try {
let startTime = Date.now();
const { promise, resolve } = sleepCancellable(2000); // I want to give the ICE / STUN / TURN time to collect.
session.rpcs[UUID].whep.iceCompletedCallback = resolve; // Can complete earlier if possible.
await promise; // pausing for a moment
console.log("Finished waiting for ice candidates. Waited " + ((Date.now() - startTime)/1000)+ "-seconds");
delete session.rpcs[UUID].whep.iceCompletedCallback;
} catch(e){errorlog(e);}
var sdp = session.rpcs[UUID].whep.localDescription.sdp;
sdp = processSDPFromServer(sdp);
//description.sdp = processSDPFromServer(description.sdp); // cause server to not send
if (sdp.includes("sendrecv")){
errorlog("Should not include sendrecv");
sdp = sdp.replace("a=sendrecv","a=recvonly");
@@ -36697,8 +36870,11 @@ async function whepIn(whepInput=false, whepInputToken=false, UUID=false){ // PLA
jsep.type = "answer";
warnlog("Processing answer:");
jsep.sdp = processSDPFromServer(jsep.sdp); // setup stereo/mono
session.rpcs[UUID].whep.setRemoteDescription(jsep).then(function(){
warnlog("SHOULD BE CONNECTED?");
/* var content = "";
while (candidates.length){
@@ -36730,7 +36906,7 @@ async function whepIn(whepInput=false, whepInputToken=false, UUID=false){ // PLA
}
} else {
xhttp.open("POST", whepInput, true);
}
}
xhttp.setRequestHeader('Content-Type', 'application/'+type);
if (whepInputToken){
@@ -38156,10 +38332,14 @@ function getReceivers2(UUID){
ssTracks = session.rpcs[UUID].screenIndexes;
}
var receivers = session.rpcs[UUID].getReceivers();
if (session.rpcs[UUID].getReceivers){
var receivers = session.rpcs[UUID].getReceivers();
} else {
var receivers = [];
}
try {
if (session.rpcs[UUID].whep){ // used to be "mc", not "whep"
if (session.rpcs[UUID].whep && session.rpcs[UUID].whep.getReceivers){ // used to be "mc", not "whep"
try{
receivers = receivers.concat(session.rpcs[UUID].whep.getReceivers());
} catch(e){errorlog(e);}

37
main.js
View File

@@ -408,7 +408,17 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
}
}
if (urlParams.has('svc') || urlParams.has('scalabilitymode')) { // Experiment with this feature here: https://webrtc.github.io/samples/src/content/extensions/svc/
session.scalabilityMode = urlParams.get('svc') || urlParams.get('scalabilitymode') || "L1T3";
if (!scalabilityModes.includes(session.scalabilityMode)){
scalabilityModes.forEach(sca=>{
if (sca.toLowerCase() === session.scalabilityMode.toLowerCase()){
session.scalabilityMode = sca;
log("Corrected the capitalization of the SVC value. just in case thats important");
}
});
}
}
if (urlParams.has('whepplay')) { // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case
if (urlParams.get('whepplay')){
@@ -748,6 +758,11 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
session.motionSwitch = parseInt(urlParams.get('motionswitch')) || parseInt(urlParams.get('motiondetection')) || 15; // threshold of motion needed to trigger
}
if (urlParams.has('motionrecord') || urlParams.has('recordmotion')) { // switch OBS to this scene when there is motion, and "solo view" this video in the VDO.Ninja auto-mixer, if used
session.motionRecord = parseInt(urlParams.get('motionrecord')) || parseInt(urlParams.get('recordmotion')) || 15; // threshold of motion needed to trigger
}
if (urlParams.has('locked')) {
session.locked = urlParams.get('locked');
@@ -1831,6 +1846,10 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
session.stereo = 4;
} else if (session.stereo === "multi") {
session.stereo = 4;
} else if (session.stereo === "8") {
session.stereo = 8;
} else if (session.stereo === "surround") {
session.stereo = 8;
} else if (session.stereo === "2") {
session.stereo = 2;
} else if (session.stereo === "6") {
@@ -2144,7 +2163,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
fakeElement.id = parseInt(Math.random() * 10000000000);
session.fakeFeeds.push(fakeElement);
}
if (session.view!==false || session.scene!==false){
if ((session.view!==false) || (session.scene!==false) || session.whepInput){
setTimeout(function(){updateMixer();},1000);
}
}
@@ -4435,7 +4454,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
getById("saveRoom").style.display = "none"; // don't let the user save the room if in OBS
}
if (session.cleanViewer){
if (session.view && !session.director && session.permaid===false){
if ((session.view || session.whepInput) && !session.director && session.permaid===false){
session.cleanOutput = true;
}
}
@@ -4708,7 +4727,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
getById("head3").classList.remove('hidden');
getById("head3a").classList.remove('hidden');
}
} else if ((window.obsstudio) && (session.permaid === false) && (session.director === false) && (session.view) &&(session.roomid.length>0)) { // we already know roomid !== false
} else if (window.obsstudio && (session.permaid === false) && (session.director === false) && (session.view || session.whepInput) && (session.roomid.length>0)) { // we already know roomid !== false
updateURL("scene", true, false); // we also know it's not a scene, but we will assume it is in this specific case.
}
@@ -4753,7 +4772,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
} else if (session.chatbutton === false) {
getById("chatbutton").classList.add("hidden");
}
} else if (session.view && (session.permaid === false)) {
} else if ((session.view || session.whepInput) && (session.permaid === false)) {
//if (!session.activeSpeaker){
session.audioMeterGuest = false;
//}
@@ -4814,7 +4833,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
getById("header").style.opacity = 0;
}
if (session.view) {
if (session.view || session.whepInput) {
getById("main").className = "";
getById("credits").style.display = 'none';
try {
@@ -4839,7 +4858,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
session.waitImage = urlParams.get('waitimage') || false;
}
if (((session.view) && (session.roomid === false)) || (session.waitImage && (session.scene!==false))) {
if (((session.view || session.whepInput) && (session.roomid === false)) || (session.waitImage && (session.scene!==false))) {
getById("container-4").className = 'column columnfade';
getById("container-3").className = 'column columnfade';
@@ -4870,7 +4889,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
session.waitImageTimeoutObject = setTimeout(function() {
session.waitImageTimeoutObject = true;
try {
if ((session.view)) {
if ((session.view || session.whepInput)) {
if (document.getElementById("mainmenu")) {
if (session.waitImage){
getById("mainmenu").innerHTML += '<img id="retryimage"/>';
@@ -6077,7 +6096,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
// Warns user about network going down
window.addEventListener("offline", function (e) {
warnlog("connection lost");
if ((session.view) && (session.permaid === false)) {
if ((session.view || session.whepInput) && (session.permaid === false)) {
log("VDO.Ninja has no network connectivity and can't work properly." );
} else if (session.scene !== false) {
log("VDO.Ninja has no network connectivity and can't work properly." );

View File

@@ -533,14 +533,52 @@ var CodecsHandler = (function() {
}
if (typeof params.stereo != 'undefined'){
if (params.stereo==0){
appendOpusNext += ';stereo=0;sprop-stereo=0'; // defaults to 0
} else if (params.stereo==1){
appendOpusNext += ';stereo=1;sprop-stereo=1'; // defaults to 0
} else if (params.stereo==2){
if (params.stereo==0){ // &stereo=0
sdpLines[opusFmtpLineIndex] = sdpLines[opusFmtpLineIndex].replace(";stereo=1", "");
sdpLines[opusFmtpLineIndex] = sdpLines[opusFmtpLineIndex].replace(";sprop-stereo=1", "");
sdpLines[opusFmtpLineIndex] = sdpLines[opusFmtpLineIndex].replace(";stereo=0", "");
sdpLines[opusFmtpLineIndex] = sdpLines[opusFmtpLineIndex].replace(";sprop-stereo=0", "");
if (sdpLines[opusFmtpLineIndex].split(";stereo=0").length==1){
appendOpusNext += ';stereo=0'; // defaults to 0
}
if (sdpLines[opusFmtpLineIndex].split(";sprop-stereo=0").length==1){
appendOpusNext += ';sprop-stereo=0'; // defaults to 0
}
} else if (params.stereo==1){ // &stereo=1
sdpLines[opusFmtpLineIndex] = sdpLines[opusFmtpLineIndex].replace(";stereo=1", "");
sdpLines[opusFmtpLineIndex] = sdpLines[opusFmtpLineIndex].replace(";sprop-stereo=1", "");
sdpLines[opusFmtpLineIndex] = sdpLines[opusFmtpLineIndex].replace(";stereo=0", "");
sdpLines[opusFmtpLineIndex] = sdpLines[opusFmtpLineIndex].replace(";sprop-stereo=0", "");
if (sdpLines[opusFmtpLineIndex].split(";stereo=1").length==1){
appendOpusNext += ';stereo=1'; // defaults to 0
}
if (sdpLines[opusFmtpLineIndex].split(";sprop-stereo=1").length==1){
appendOpusNext += ';sprop-stereo=1'; // defaults to 0
}
} else if (params.stereo==2){ // &stereo=4
sdpLines[opusFmtpLineIndex] = sdpLines[opusFmtpLineIndex].replace(";stereo=1", "");
sdpLines[opusFmtpLineIndex] = sdpLines[opusFmtpLineIndex].replace(";sprop-stereo=1", "");
sdpLines[opusFmtpLineIndex] = sdpLines[opusFmtpLineIndex].replace(";stereo=0", "");
sdpLines[opusFmtpLineIndex] = sdpLines[opusFmtpLineIndex].replace(";sprop-stereo=0", "");
sdpLines[opusIndex] = sdpLines[opusIndex].replace("opus/48000/2", "multiopus/48000/6");
appendOpusNext += ';channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2'; // Multi-channel 5.1 audio
if (sdpLines[opusFmtpLineIndex].split(";channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2").length==1){
appendOpusNext += ';channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2'; // Multi-channel 5.1 audio
}
} else if (params.stereo==3){ // &stereo=8
sdpLines[opusFmtpLineIndex] = sdpLines[opusFmtpLineIndex].replace(";stereo=1", "");
sdpLines[opusFmtpLineIndex] = sdpLines[opusFmtpLineIndex].replace(";sprop-stereo=1", "");
sdpLines[opusFmtpLineIndex] = sdpLines[opusFmtpLineIndex].replace(";stereo=0", "");
sdpLines[opusFmtpLineIndex] = sdpLines[opusFmtpLineIndex].replace(";sprop-stereo=0", "");
sdpLines[opusIndex] = sdpLines[opusIndex].replace("opus/48000/2", "multiopus/48000/8");
if (sdpLines[opusFmtpLineIndex].split(";channel_mapping=0,6,1,2,3,4,5,7;num_streams=5;coupled_streams=3").length==1){
appendOpusNext += ';channel_mapping=0,6,1,2,3,4,5,7;num_streams=5;coupled_streams=3'; // Multi-channel 5.1 audio
}
}
}
if (typeof params.maxaveragebitrate != 'undefined') {
@@ -577,10 +615,6 @@ var CodecsHandler = (function() {
}
}
if (debug){
console.log();
}
sdpLines[opusFmtpLineIndex] = sdpLines[opusFmtpLineIndex].concat(appendOpusNext);
if (debug){

File diff suppressed because one or more lines are too long

233
whip.html
View File

@@ -33,6 +33,8 @@
border:0;
margin:0;
outline:0;
overflow-y: auto;
overflow-x: hidden;
}
button.glyphicon-button:focus,
@@ -59,6 +61,7 @@
box-sizing: border-box;
align-items: center;
padding: 0 1em;
min-width: 50px;
}
.details{
font-size: 14px;
@@ -74,6 +77,7 @@
box-sizing: border-box;
align-items: center;
padding: 0 1em;
min-width: 50px;
}
#header{
width:100%;
@@ -104,7 +108,7 @@
}
.container{
font-size: min(14px, 2vh);
font-size: 16px;
align-self:center;
max-width: 100%;
width: 720px;
@@ -164,7 +168,7 @@
flex-direction: row;
flex-wrap: nowrap;
justify-content: center;
margin: 4em;
margin: 2em;
}
@media only screen and (max-width: 1030px) {
@@ -186,50 +190,14 @@
}
div.urlInput {
padding: 0 0 4vh 0;
}
@media only screen and (max-height: 839px) {
body{
zoom: 0.74;
-moz-transform: scale(0.74);
-moz-transform-origin: 0 0;
}
}
@media only screen and (max-width: 940px) {
body{
zoom: 0.74;
-moz-transform: scale(0.74);
-moz-transform-origin: 0 0;
}
.container{
max-width:99%;
}
div.urlInput {
}
div#audioOutputContainer, #history {
margin: 2em;
}
}
@media only screen and (max-width: 840px) {
body{
zoom: 0.64;
-moz-transform: scale(0.64);
-moz-transform-origin: 0 0;
}
padding: 0 0 1vh 0;
}
@media only screen and (max-height: 639px) {
div.urlInput {
}
div#audioOutputContainer, #history {
margin: 2em;
margin: 1em;
}
}
@@ -336,26 +304,26 @@
</div>
<div class="inputCombo" id="advanced" style="margin: 10px 0px 10px 10px;">
<select style="border-radius:10px;margin-right:5px;width:unset!important;" class="changeText" id="whipoutaudiobitrate" title="Which audio bitrate target would you prefer?" >
<select style="border-radius:10px;margin-right:5px;width:unset!important;" class="changeText" id="whipoutaudiobitrate" title="Which audio bitrate target would you prefer? 128-kbps is fine for music." >
<option value="0" selected>🎙Default Audio Bitrate</option>
<option value="32">🎙32-kbps</option>
<option value="64">🎙64-kbps</option>
<option value="128">🎙128-kbps</option>
<option value="256">🎙256-kbps</option>
</select >
<select style="border-radius:10px;margin-right:5px;width:unset!important;" class="changeText" id="vbrcbr" title="Which audio bitrate target would you prefer?" >
<select style="border-radius:10px;margin-right:5px;width:unset!important;" class="changeText" id="vbrcbr" title="Whether the audio bitrate with be constant or variable" >
<option value="cbr" selected>🎙CBR</option>
<option value="vbr">🎙VBR</option>
</select >
<select style="border-radius:10px;margin-right:5px;width:unset!important;" class="changeText" id="denoise" title="Which audio bitrate target would you prefer?" >
<option value="1" selected>🎙Denoise On</option>
<option value="0">🎙Denoise Off</option>
<select style="border-radius:10px;margin-right:5px;width:unset!important;" class="changeText" id="denoise" onchange="checkStereo()" title="Turn off to improve clarity, but you'll hear any background noise" >
<option value="0" selected>🎙Denoise Off</option>
<option value="1">🎙Denoise On</option>
</select >
<select style="border-radius:10px;margin-right:5px;width:unset!important;" class="changeText" id="autogain" title="Which audio bitrate target would you prefer?" >
<option value="1" selected>🎙Auto Gain On</option>
<option value="0">🎙Auto Gain Off</option>
<select style="border-radius:10px;margin-right:5px;width:unset!important;" class="changeText" id="autogain" onchange="checkStereo()" title="Auto-controls the input volume; turn off to manage that yourself." >
<option value="0" selected>🎙Auto Gain Off</option>
<option value="1">🎙Auto Gain On</option>
</select >
<select style="border-radius:10px;margin-right:5px;width:unset!important;" class="changeText" id="stereo" title="Which audio bitrate target would you prefer?" >
<select style="border-radius:10px;margin-right:5px;width:unset!important;" class="changeText" id="stereo" title="Stereo is available only if auto-gain and noise-reduction is off." >
<option value="1" selected>🎙Stereo</option>
<option value="0">🎙Mono</option>
</select >
@@ -368,12 +336,15 @@
<option value="6000">🎦6000-kbps</option>
<option value="20000">🎦20000-kbps</option>
</select >
<select style="border-radius:10px;margin-right:5px;width:unset!important;" class="changeText" id="codecGroupFlag" title="Which video codec would you prefer to be used if available?" >
<select style="border-radius:10px;margin-right:5px;width:unset!important;" class="changeText" id="codecGroupFlag" onchange="updateSVC();" title="Which video codec would you prefer to be used if available?" >
<option value="default" selected>🎦OpenH264</option>
<option value="vp9">🎦VP9</option>
<option value="h264">🎦H264</option>
<option value="vp8">🎦VP8</option>
<option value="av1">🎦AV1</option>
<option value="vp9">🎦VP9</option>
<option value="vp8">🎦VP8</option>
<option value="h264">🎦H264</option>
</select >
<select style="border-radius:10px;margin-right:5px;width:unset!important;" class="changeText" id="svcGroupFlag" title="Which scalable video coding do you want to use?" >
<option value="0" selected>🎦 SVC Off</option>
</select >
</div>
@@ -452,6 +423,38 @@ document.querySelector("#changeText1a").value = localStorage.getItem('changeText
document.querySelector("#changeText2").value = localStorage.getItem('changeText2') || "";
document.querySelector("#changeText3").value = localStorage.getItem('changeText3') || "";
const scalabilityModes = [
'L1T1',
'L1T2',
'L1T3',
'L2T1',
'L2T2',
'L2T3',
'L3T1',
'L3T2',
'L3T3',
'L2T1h',
'L2T2h',
'L2T3h',
'S2T1',
'S2T2',
'S2T3',
'S2T1h',
'S2T2h',
'S2T3h',
'S3T1',
'S3T2',
'S3T3',
'S3T1h',
'S3T2h',
'S3T3h',
'L2T2_KEY',
'L2T3_KEY',
'L3T2_KEY',
'L3T3_KEY'
];
function gohere1(){
if (document.getElementById('changeText1').value){
localStorage.setItem('changeText1', document.getElementById('changeText1').value);
@@ -459,6 +462,7 @@ function gohere1(){
localStorage.setItem('bitrateGroupFlag', document.getElementById('bitrateGroupFlag').value);
localStorage.setItem('codecGroupFlag', document.getElementById('codecGroupFlag').value);
localStorage.setItem('svcGroupFlag', document.getElementById('svcGroupFlag').value);
localStorage.setItem('whipoutaudiobitrate', document.getElementById('whipoutaudiobitrate').value);
localStorage.setItem('vbrcbr', document.getElementById('vbrcbr').value);
@@ -486,15 +490,59 @@ function gohere1(){
if (document.getElementById('codecGroupFlag').value!=="default"){
codec = "&whipoutcodec="+document.getElementById('codecGroupFlag').value;
}
var svc = "";
if (document.getElementById('svcGroupFlag').value!=="0"){
svc = "&svc="+document.getElementById('svcGroupFlag').value;
}
if (document.getElementById('changeText1a').value){
window.location = domain + "?push&whippush=" + encodeURIComponent(document.getElementById('changeText1').value) + "&whippushtoken=" + document.getElementById('changeText1a').value + codec + bitrate+whipoutaudiobitrate+vbrcbr+autogain+stereo+denoise;
window.location = domain + "?push&whippush=" + encodeURIComponent(document.getElementById('changeText1').value) + "&whippushtoken=" + document.getElementById('changeText1a').value + codec + bitrate+whipoutaudiobitrate+vbrcbr+autogain+stereo+denoise+svc;
} else {
window.location = domain + "?push&whippush=" + encodeURIComponent(document.getElementById('changeText1').value) + codec + bitrate+whipoutaudiobitrate+vbrcbr+autogain+stereo+denoise;
window.location = domain + "?push&whippush=" + encodeURIComponent(document.getElementById('changeText1').value) + codec + bitrate+whipoutaudiobitrate+vbrcbr+autogain+stereo+denoise+svc;;
}
}
}
function checkStereo(){
if (parseInt(document.getElementById('autogain').value) || parseInt(document.getElementById('denoise').value)){
document.getElementById('stereo').disabled = true;
document.getElementById('stereo').title = "Noise reduction and auto-gain will prevent stereo audio from working";
} else {
document.getElementById('stereo').disabled = false;
delete document.getElementById('stereo').disabled;
document.getElementById('stereo').title = "Enable stereo 2.0 audio if available. Must be enabled on the viewer's end as well.";
}
}
function updateSVC(){
var codecName = document.getElementById('codecGroupFlag').value;
var select = document.getElementById("svcGroupFlag");
var selectedValue = "0";
if (select.options && select.selectedIndex && select.options[select.selectedIndex]){
selectedValue = select.options[select.selectedIndex].value;
}
select.innerHTML = "";
var option = document.createElement("option");
option.text = "🎦 SVC Off";
option.value = "0";
select.add(option);
select.selectedIndex = 0;
if (svcLUT[codecName]){
svcLUT[codecName].forEach(opt=>{
option = document.createElement("option");
option.text = "🎦 "+opt;
option.value = opt;
select.add(option);
if (opt == selectedValue){
select.value = opt;
}
});
}
}
function gohere1t(){
if (document.getElementById('changeText1t').value){
localStorage.setItem('changeText1t', document.getElementById('changeText1t').value);
@@ -527,6 +575,7 @@ function resetHistory(){
document.querySelector("#changeText2").value = "";
document.querySelector("#changeText3").value = "";
document.querySelector("#changeText1t").value = "";
checkStereo();
}
(function (w) {
@@ -555,12 +604,86 @@ function enterPressed(event, callback){
}
}
checkStereo();
var isMobile = false;
if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)){ // does not detect iPad Pros.
isMobile=true; // if iOS, default to H264? meh. let's not.
}
var Firefox = navigator.userAgent.indexOf("Firefox")>=0;
if (Firefox){
Firefox = parseInt(navigator.userAgent.split("irefox/").pop()) || true;
}
var capabilityType = Firefox ? "transmission" : "webrtc";
var codecs = RTCRtpSender.getCapabilities('video').codecs;
var svcLUT = {};
var svcDefault = {};
function getCommonValues(obj) {
if (obj.default){
delete obj.default;
}
let commonValues = [];
let firstKey = Object.keys(obj)[0];
let firstArray = obj[firstKey];
for (let i = 0; i < firstArray.length; i++) {
let currentValue = firstArray[i];
let isCommonValue = true;
for (let key in obj) {
if (!obj[key].includes(currentValue)) {
isCommonValue = false;
break;
}
}
if (isCommonValue) {
commonValues.push(currentValue);
}
}
return commonValues
}
async function processCodecs(){
await codecs.forEach(async codec => {
var codecName = codec.mimeType.replace("video/","").toLowerCase();
if (['video/red', 'video/ulpfec', 'video/rtx'].includes(codec.mimeType)) {
return;
} else if (svcLUT[codecName]){ // already done
return;
}
svcLUT[codecName] = [];
var capabilityPromises = [];
for (const mode of scalabilityModes) {
capabilityPromises.push(navigator.mediaCapabilities.encodingInfo({
type: capabilityType,
video: {
contentType: codec.mimeType,
width: 1920,
height: 1080,
bitrate: 10000,
framerate: 29.97,
scalabilityMode: mode
}
}));
}
var capabilityResults = await Promise.all(capabilityPromises);
for (var i = 0;i<capabilityResults.length;i++){
if (capabilityResults[i].supported){
svcLUT[codecName].push(scalabilityModes[i]);
}
}
svcLUT['default'] = getCommonValues(svcLUT);
updateSVC();
});
console.log("available codecs");
console.log(svcLUT);
}
processCodecs();
</script>
</body>