pc_test_swap.html (382 lines of code) (raw):

<html><head> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <title>Simple RTCPeerConnection Video Test</title> <style> #player1 { position:relative; width:640px; height:480px; float:right; } #player2 { position:relative; width:640px; height:480px; float:left; } #mainvideo { position:absolute; top:0; left:0; z-index:0; } #localvideo1, #localvideo2 { position:absolute; top:0; left:0; z-index:1; } .hidden { display: none; } </style> </head> <body> <h1>Simple swap via RTPSender Video Test</h1> Ugly proof-of-concept. Ignore the disabled portions of pc_test lying around. <div> <button id="tehbutton" onclick="start();">Start!</button> <button id="swap" onclick="replace_track(pc1)">Swap video</button> <div id="controls" class="hidden"> <input checked="checked" id="oneway" value="one-way call" type="checkbox"> <label for="oneway">One-way call</label> <input id="audio_only" value="Audio-only call" type="checkbox"> <label for="audio_only">Audio-only call</label> <input id="disable_nack" value="Disable NACK" type="checkbox"> <label for="disable_nack">Disable NACK</label> <input id="disable_video" value="Disable video" onclick="disable_media(pc1, 1, 0);" type="checkbox"> <label for="disable_video">Disable video</label> <input id="disable_audio" value="Disable audio" onclick="disable_media(pc1, 0, 0);" type="checkbox"> <label for="disable_audio">Disable audio</label> <input id="requireh264" value="Require H.264" onclick="h264StateChange(this);" type="checkbox"> <label for="requireh264">Require H.264 video</label> <div id="divH264" class="hidden"> <br> <input id="prefermode0" value="Prefer Mode 0" type="checkbox"> <label for="prefermode0">Prefer Mode 0</label> </div> </div> </div><br> <div> <div id="player1"> <video tabindex="0" id="pc1video" class="mainvideo" controls="controls" muted autoplay width="320" height="240"></video> <video tabindex="0" id="localvideo1" controls="controls" muted autoplay width="160" height="120"></video></div> <div id="player2"> <video tabindex="0" id="pc2video" class="mainvideo" controls="controls" muted autoplay width="320" height="240"></video> <video tabindex="0" id="localvideo2" controls="controls" muted autoplay width="160" height="120"></video></div><br> <br style="clear: left;"> </div> <div id="log"></div> <script type="application/javascript"> // Lame clone function. Not efficient but we don't have jQuery. function copy_dictionary (obj) { return JSON.parse(JSON.stringify(obj)); } function log(msg) { let div = document.getElementById("log"); div.innerHTML = div.innerHTML + "<p>" + msg + "</p>"; } let pc1video = document.getElementById("pc1video"); let pc2video = document.getElementById("pc2video"); pc1video.onplay = function() {log("Play for pc1");}; pc2video.onplay = function() {log("Play for pc2");}; let button = document.getElementById("tehbutton"); let localvideo1 = document.getElementById("localvideo1"); let localvideo2 = document.getElementById("localvideo2"); let swapped = false; let oneway = document.getElementById("oneway"); oneway.checked = true; let disablenack = document.getElementById("disable_nack"); disablenack.checked = false; let audio_only = document.getElementById("audio_only"); audio_only.checked = false; let video_disable = document.getElementById("disable_video"); let audio_disable = document.getElementById("disable_audio"); let requireh264 = document.getElementById("requireh264"); requireh264.checked = false; let prefermode0 = document.getElementById("prefermode0"); requireh264.checked = false; let pc1; let pc2; let pc1_offer; let pc2_answer; let offer_constraints; let answer_constraints; let fake_stream; let real_stream; let new_stream; var senders1 = [ ]; var senders2 = [ ]; function failed(code) { log("Failure callback: " + JSON.stringify(code)); } function disable_media(pc, type, array_index) { log("disable_media"); var stream; if (pc == pc1) stream = localvideo1.mozSrcObject; else if (pc == pc2) stream = localvideo2.mozSrcObject; else log("bad pc " + pc); if (stream) { log("track[" + array_index + "] = " + stream.getVideoTracks()[array_index]); if (type == 1) stream.getVideoTracks()[array_index].enabled = !video_disable.checked; else if (type == 0) stream.getAudioTracks()[array_index].enabled = !audio_disable.checked; else log("bad type"); } else log("no stream"); } function replace_track(pc) { swapped = !swapped; log("replace_track = " + swapped); //senders1['audio'].replaceTrack(fake_stream.getAudioTracks()[0], function() {}, failed); localvideo1.mozSrcObject = fake_stream; let old_video = senders1['video'].track; log(old_video); senders1['video'].replaceTrack(fake_stream.getVideoTracks()[0], function() { log("Replaced video with fake"); old_video.stop(); log("stopped old_video" + old_video); if (old_video === senders1['video'].track) { log("?? .track didn't change?"); } old_video = null; fake_stream.getVideoTracks()[0].enabled = false; log("Disabled fake"); navigator.mozGetUserMedia({ video: { facingMode: (swapped? "environment" : "user") } }, function(stream) { new_stream = stream; localvideo1.mozSrcObject = stream; localvideo1.play(); senders1['video'].replaceTrack(stream.getVideoTracks()[0], function() { log("Replaced video with new video"); }, failed); }, failed); }, failed); } // pc1.createOffer finished, call pc1.setLocal function step1(offer) { // log("Offer: " + sdpPrettyPrint(offer.sdp)); pc1_offer = offer; if (requireh264.checked) { // to enforce the usage of H264 we remove the VP8 codec from the offer offer.sdp = removeVP8(offer.sdp); if (prefermode0.checked) { offer.sdp = preferMode0(offer.sdp); } pc1_offer = offer; // log("No VP8 Offer: " + sdpPrettyPrint(offer.sdp)); if (!offer.sdp.match(/a=rtpmap:[0-9]+ H264/g)) { log("No H264 found in the offer!!!"); return; } } if (disablenack.checked) { offer.sdp = removeNACK(offer.sdp); } pc1.setLocalDescription(offer, step2, failed); } // replace CR NL with HTML breaks function sdpPrettyPrint(sdp) { return sdp.replace(/[\r\n]+/g, '<br>'); } function h264StateChange(cb) { var h264opts = document.getElementById('divH264'); if(cb.checked){ h264opts.style.display = 'block'; } else { h264opts.style.display = 'none'; } } // Also remove mode 0 if it's offered // Note, we don't bother removing the fmtp lines, which makes a good test // for some SDP parsing issues. function removeVP8(sdp) { updated_sdp = sdp.replace("a=rtpmap:120 VP8/90000\r\n",""); updated_sdp = updated_sdp.replace(/m=video ([0-9]+) RTP\/SAVPF ([0-9 ]*) 120/g, "m=video $1 RTP\/SAVPF $2"); updated_sdp = updated_sdp.replace(/m=video ([0-9]+) RTP\/SAVPF 120([0-9 ]*)/g, "m=video $1 RTP\/SAVPF$2"); updated_sdp = updated_sdp.replace("a=rtcp-fb:120 nack\r\n",""); updated_sdp = updated_sdp.replace("a=rtcp-fb:120 nack pli\r\n",""); updated_sdp = updated_sdp.replace("a=rtcp-fb:120 ccm fir\r\n",""); return updated_sdp; } function preferMode0(sdp) { updated_sdp = updated_sdp.replace("126 97", "97 126"); return updated_sdp; } function removeNACK(sdp) { updated_sdp = sdp.replace("a=rtcp-fb:120 nack\r\n",""); updated_sdp = updated_sdp.replace("a=rtcp-fb:120 nack pli\r\n","a=rtcp-fb:120 pli\r\n"); updated_sdp = updated_sdp.replace("a=rtcp-fb:126 nack\r\n",""); updated_sdp = updated_sdp.replace("a=rtcp-fb:126 nack pli\r\n","a=rtcp-fb:126 pli\r\n"); updated_sdp = updated_sdp.replace("a=rtcp-fb:97 nack\r\n",""); updated_sdp = updated_sdp.replace("a=rtcp-fb:97 nack pli\r\n","a=rtcp-fb:97 pli\r\n"); return updated_sdp; } // pc1.setLocal finished, call pc2.setRemote function step2() { pc1.onicecandidate = function(obj) { if (obj.candidate) { // log("pc1 found ICE candidate: " + JSON.stringify(obj.candidate)); pc2.addIceCandidate(obj.candidate); } else { // log("pc1 got end-of-candidates signal"); } } pc2.setRemoteDescription(pc1_offer, step3, failed); }; // pc2.setRemote finished, call pc2.createAnswer function step3() { pc2.didSetRemote = true; while (pc2.ice_queued.length > 0) { pc2.addIceCandidate(pc2.ice_queued.shift()); } pc2.createAnswer(step4, failed, answer_constraints); } // pc2.createAnswer finished, call pc2.setLocal function step4(answer) { // log("Answer: " + sdpPrettyPrint(answer.sdp)); pc2_answer = answer; if (requireh264.checked && !(answer.sdp.match(/a=rtpmap:[0-9]+ H264/g))) { log("No H264 found in the answer!!!"); return; } pc2.setLocalDescription(answer, step5, failed); } // pc2.setLocal finished, call pc1.setRemote function step5() { pc2.onicecandidate = function(obj) { if (obj.candidate) { // log("pc2 found ICE candidate: " + JSON.stringify(obj.candidate)); pc1.addIceCandidate(obj.candidate); } else { // log("pc2 got end-of-candidates signal"); } } pc1.setRemoteDescription(pc2_answer, step6, failed); } // pc1.setRemote finished, media should be running! function step6() { pc1.didSetRemote = true; while (pc1.ice_queued.length > 0) { pc1.addIceCandidate(pc1.ice_queued.shift()); } log("HIP HIP HOORAY"); } function start() { button.innerHTML = "Stop!"; button.onclick = stop; pc1 = new RTCPeerConnection(); pc2 = new RTCPeerConnection(); pc1.didSetRemote = false; pc2.didSetRemote = false; pc1.ice_queued = []; pc2.ice_queued = []; pc2.onicecandidate = function(obj) { if (obj.candidate) { // log("pc2 found ICE candidate: " + JSON.stringify(obj.candidate)); if (pc1.didSetRemote) { pc1.addIceCandidate(obj.candidate); } else { pc1.ice_queued.push(obj.candidate); } } else { // log("pc2 got end-of-candidates signal"); } } pc1.onicecandidate = function(obj) { if (obj.candidate) { // log("pc1 found ICE candidate: " + JSON.stringify(obj.candidate)); if (pc2.didSetRemote) { pc2.addIceCandidate(obj.candidate); } else { pc2.ice_queued.push(obj.candidate); } } else { // log("pc1 got end-of-candidates signal"); } } pc1.onaddstream = function(obj) { log("pc1 got remote stream from pc2 " + obj.type); pc1video.mozSrcObject = obj.stream; //setTimeout(pc1video.play, 1000); } pc2.onaddstream = function(obj) { log("pc2 got remote stream from pc1 " + obj.type); pc2video.mozSrcObject = obj.stream; //setTimeout(pc2video.play, 1000); } var myrequest = { audio: true }; if (!(audio_only.checked)) { myrequest = { audio: true, video: {facingMode: "user"} }; } myrequest_reverse = copy_dictionary(myrequest); if (oneway.checked) { offer_constraints = { mandatory: { OfferToReceiveVideo : false, OfferToReceiveAudio: false } }; answer_constraints = { mandatory: { OfferToReceiveVideo : true, OfferToReceiveAudio: true } }; } navigator.mozGetUserMedia({ fake:true, audio: true, video: true }, function(stream) { fake_stream = stream; navigator.mozGetUserMedia(myrequest, function(video1) { // Add stream obtained from gUM to <video> to start media flow. localvideo1.mozSrcObject = video1; real_stream = video1; if (video_disable.checked) localvideo1.mozSrcObject.getVideoTracks()[0].enabled = false; if (audio_disable.checked) localvideo1.mozSrcObject.getAudioTracks()[0].enabled = false; localvideo1.play(); video1.getTracks().forEach(function(track) { senders1[track.kind] = pc1.addTrack(track, video1); }); //pc1.addStream(video1); if (!oneway.checked) { navigator.mozGetUserMedia(myrequest_reverse, function(video2) { localvideo2.mozSrcObject = video2; localvideo2.play(); video2.getTracks().forEach(function(track) { senders2[track.kind] = pc2.addTrack(track, video2); }); //pc2.addStream(video2); // Start the signaling. pc1.createOffer(step1, failed, offer_constraints ); }, failed); } else { pc1.createOffer(step1, failed, offer_constraints ); } }, failed); }, failed); } function stop() { pc1.close(); pc2.close(); pc1 = null; pc2 = null; if (localvideo1.mozSrcObject) { localvideo1.mozSrcObject.stop(); localvideo1.mozSrcObject = null; } if (localvideo2.mozSrcObject) { localvideo2.mozSrcObject.stop(); localvideo2.mozSrcObject = null; } fake_stream.stop(); fake_stream.stop(); real_stream.stop(); fake_stream = null; real_stream = null; senders1 = []; senders2 = []; button.innerHTML = "Start!"; button.onclick = start; } </script> </body></html>