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>