913 lines
28 KiB
JavaScript
913 lines
28 KiB
JavaScript
mistplayers.webrtc = {
|
|
name: "WebRTC player",
|
|
mimes: ["webrtc"],
|
|
priority: MistUtil.object.keys(mistplayers).length + 1,
|
|
isMimeSupported: function (mimetype) {
|
|
return (this.mimes.indexOf(mimetype) == -1 ? false : true);
|
|
},
|
|
isBrowserSupported: function (mimetype,source,MistVideo) {
|
|
|
|
if ((!("WebSocket" in window)) || (!("RTCPeerConnection" in window) || (!("RTCRtpReceiver" in window)))) { return false; }
|
|
|
|
//check for http/https mismatch
|
|
if (location.protocol.replace(/^http/,"ws") != MistUtil.http.url.split(source.url.replace(/^http/,"ws")).protocol) {
|
|
MistVideo.log("HTTP/HTTPS mismatch for this source");
|
|
return false;
|
|
}
|
|
|
|
|
|
//check if both audio and video have at least one playable track
|
|
//gather track types and codec strings
|
|
var playabletracks = {};
|
|
var hassubtitles = false;
|
|
for (var i in MistVideo.info.meta.tracks) {
|
|
if (MistVideo.info.meta.tracks[i].type == "meta") {
|
|
if (MistVideo.info.meta.tracks[i].codec == "subtitle") { hassubtitles = true; }
|
|
continue;
|
|
}
|
|
if (!(MistVideo.info.meta.tracks[i].type in playabletracks)) {
|
|
playabletracks[MistVideo.info.meta.tracks[i].type] = {};
|
|
}
|
|
playabletracks[MistVideo.info.meta.tracks[i].type][MistVideo.info.meta.tracks[i].codec] = 1;
|
|
}
|
|
|
|
var tracktypes = [];
|
|
for (var type in playabletracks) {
|
|
var playable = false;
|
|
|
|
for (var codec in playabletracks[type]) {
|
|
var supported = RTCRtpReceiver.getCapabilities(type).codecs;
|
|
for (var i in supported) {
|
|
if (supported[i].mimeType.toLowerCase() == (type+"/"+codec).toLowerCase()) {
|
|
playable = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (playable) {
|
|
tracktypes.push(type);
|
|
}
|
|
}
|
|
if (hassubtitles) {
|
|
//there is a subtitle track, check if there is a webvtt source
|
|
for (var i in MistVideo.info.source) {
|
|
if (MistVideo.info.source[i].type == "html5/text/vtt") {
|
|
tracktypes.push("subtitle");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return tracktypes.length ? tracktypes : false;
|
|
|
|
//return true;
|
|
},
|
|
player: function(){}
|
|
};
|
|
var p = mistplayers.webrtc.player;
|
|
p.prototype = new MistPlayer();
|
|
p.prototype.build = function (MistVideo,callback) {
|
|
var me = this;
|
|
|
|
if ((typeof WebRTCBrowserEqualizerLoaded == "undefined") || (!WebRTCBrowserEqualizerLoaded)) {
|
|
//load it
|
|
var scripttag = document.createElement("script");
|
|
scripttag.src = MistVideo.urlappend(MistVideo.options.host+"/webrtc.js");
|
|
MistVideo.log("Retrieving webRTC browser equalizer code from "+scripttag.src);
|
|
document.head.appendChild(scripttag);
|
|
scripttag.onerror = function(){
|
|
MistVideo.showError("Failed to load webrtc browser equalizer",{nextCombo:5});
|
|
}
|
|
scripttag.onload = function(){
|
|
me.build(MistVideo,callback);
|
|
}
|
|
return;
|
|
}
|
|
|
|
var video = document.createElement("video");
|
|
video.setAttribute("playsinline",""); //iphones. effin' iphones.
|
|
|
|
//apply options
|
|
var attrs = ["autoplay","loop","poster"];
|
|
for (var i in attrs) {
|
|
var attr = attrs[i];
|
|
if (MistVideo.options[attr]) {
|
|
video.setAttribute(attr,(MistVideo.options[attr] === true ? "" : MistVideo.options[attr]));
|
|
}
|
|
}
|
|
if (MistVideo.options.muted) {
|
|
video.muted = true; //don't use attribute because of Chrome bug
|
|
}
|
|
if (MistVideo.info.type == "live") {
|
|
video.loop = false;
|
|
}
|
|
if (MistVideo.options.controls == "stock") {
|
|
video.setAttribute("controls","");
|
|
}
|
|
video.setAttribute("crossorigin","anonymous");
|
|
this.setSize = function(size){
|
|
video.style.width = size.width+"px";
|
|
video.style.height = size.height+"px";
|
|
};
|
|
MistUtil.event.addListener(video,"loadeddata",correctSubtitleSync);
|
|
MistUtil.event.addListener(video,"seeked",correctSubtitleSync);
|
|
|
|
if (!MistVideo.options.autoplay) {
|
|
MistUtil.event.addListener(video,"canplay",function(){
|
|
var onplay = MistUtil.event.addListener(video,"play",function(){
|
|
MistVideo.log("Pausing because autoplay is disabled");
|
|
var onpause = MistUtil.event.addListener(video,"pause",function(){
|
|
MistVideo.options.autoplay = false;
|
|
MistUtil.event.removeListener(onpause);
|
|
});
|
|
me.api.pause();
|
|
MistUtil.event.removeListener(onplay);
|
|
});
|
|
});
|
|
}
|
|
|
|
var seekoffset = 0;
|
|
var hasended = false;
|
|
var currenttracks = [];
|
|
this.listeners = {
|
|
on_connected: function() {
|
|
seekoffset = 0;
|
|
hasended = false;
|
|
this.webrtc.play();
|
|
MistUtil.event.send("webrtc_connected",null,video);
|
|
},
|
|
on_disconnected: function() {
|
|
MistUtil.event.send("webrtc_disconnected",null,video);
|
|
MistVideo.log("Websocket sent on_disconnect");
|
|
/*
|
|
If a VoD file ends, we receive an on_stop, but no on_disconnect
|
|
If a live stream ends, we receive an on_disconnect, but no on_stop
|
|
If MistOutWebRTC crashes, we receive an on_stop and then an on_disconnect
|
|
*/
|
|
if (!hasended) {
|
|
//MistVideo.showError("Connection to media server ended unexpectedly.");
|
|
video.pause();
|
|
}
|
|
|
|
//this.webrtc.signaling.ws.close();
|
|
},
|
|
on_answer_sdp: function (ev) {
|
|
if (!ev.result) {
|
|
MistVideo.showError("Failed to open stream.");
|
|
this.on_disconnected();
|
|
return;
|
|
}
|
|
MistVideo.log("SDP answer received");
|
|
},
|
|
on_time: function(ev) {
|
|
//timeupdate
|
|
var oldoffset = seekoffset;
|
|
seekoffset = ev.current*1e-3 - video.currentTime;
|
|
if (Math.abs(oldoffset - seekoffset) > 1) { correctSubtitleSync(); }
|
|
|
|
if ((!("paused" in ev) || (!ev.paused)) && (video.paused)) {
|
|
video.play();
|
|
}
|
|
|
|
var d = (ev.end == 0 ? Infinity : ev.end*1e-3);
|
|
if (d != duration) {
|
|
duration = d;
|
|
MistUtil.event.send("durationchange",d,video);
|
|
}
|
|
|
|
MistVideo.info.meta.buffer_window = ev.end - ev.begin;
|
|
|
|
if ((ev.tracks) && (currenttracks != ev.tracks)) {
|
|
var tracks = MistVideo.info ? MistUtil.tracks.parse(MistVideo.info.meta.tracks) : [];
|
|
for (var i in ev.tracks) {
|
|
if (currenttracks.indexOf(ev.tracks[i]) < 0) {
|
|
//find track type
|
|
var type;
|
|
for (var j in tracks) {
|
|
if (ev.tracks[i] in tracks[j]) {
|
|
type = j;
|
|
break;
|
|
}
|
|
}
|
|
if (!type) {
|
|
//track type not found, this should not happen
|
|
continue;
|
|
}
|
|
if (type == "subtitle") { continue; }
|
|
|
|
//create an event to pass this to the skin
|
|
MistUtil.event.send("playerUpdate_trackChanged",{
|
|
type: type,
|
|
trackid: ev.tracks[i]
|
|
},MistVideo.video);
|
|
}
|
|
}
|
|
|
|
currenttracks = ev.tracks;
|
|
}
|
|
|
|
if (MistVideo.reporting && ev.tracks) {
|
|
MistVideo.reporting.stats.d.tracks = ev.tracks.join(",");
|
|
}
|
|
},
|
|
seek: function(e){
|
|
var thisPlayer = this;
|
|
MistUtil.event.send("seeked",seekoffset,video);
|
|
|
|
//set playback rate to auto if seek was to live point
|
|
if (e.live_point) {
|
|
thisPlayer.webrtc.playbackrate("auto");
|
|
}
|
|
|
|
if ("seekPromise" in this.webrtc.signaling){
|
|
video.play().then(function(){
|
|
if ("seekPromise" in thisPlayer.webrtc.signaling) {
|
|
thisPlayer.webrtc.signaling.seekPromise.resolve("Play promise resolved");
|
|
}
|
|
}).catch(function(){
|
|
if ("seekPromise" in thisPlayer.webrtc.signaling) {
|
|
thisPlayer.webrtc.signaling.seekPromise.reject("Play promise rejected");
|
|
}
|
|
});
|
|
}
|
|
else { video.play(); }
|
|
},
|
|
set_speed: function(e){
|
|
this.webrtc.play_rate = e.play_rate_curr;
|
|
MistUtil.event.send("ratechange",e,video);
|
|
},
|
|
on_stop: function(){
|
|
MistVideo.log("Websocket sent on_stop");
|
|
video.pause();
|
|
MistUtil.event.send("ended",null,video);
|
|
hasended = true;
|
|
}
|
|
};
|
|
|
|
|
|
function WebRTCPlayer(){
|
|
this.peerConn = null;
|
|
this.localOffer = null;
|
|
this.isConnected = false;
|
|
this.isConnecting = false;
|
|
this.play_rate = "auto";
|
|
var thisWebRTCPlayer = this;
|
|
|
|
this.on_event = function(ev) {
|
|
switch (ev.type) {
|
|
case "on_connected": {
|
|
thisWebRTCPlayer.isConnected = true;
|
|
thisWebRTCPlayer.isConnecting = false;
|
|
break;
|
|
}
|
|
case "on_answer_sdp": {
|
|
thisWebRTCPlayer.peerConn
|
|
.setRemoteDescription({ type: "answer", sdp: ev.answer_sdp })
|
|
.then(function(){}, function(err) {
|
|
console.error(err);
|
|
});
|
|
break;
|
|
}
|
|
case "on_disconnected": {
|
|
thisWebRTCPlayer.isConnected = false;
|
|
break;
|
|
}
|
|
case "on_error": {
|
|
MistVideo.showError("WebRTC error: "+MistUtil.format.ucFirst(ev.message));
|
|
return;
|
|
break;
|
|
}
|
|
}
|
|
if (ev.type in me.listeners) {
|
|
return me.listeners[ev.type].call(me,("data" in ev)?ev.data:ev);
|
|
}
|
|
MistVideo.log("Unhandled WebRTC event "+ev.type+": "+JSON.stringify(ev));
|
|
return false;
|
|
};
|
|
|
|
this.connect = function(callback){
|
|
thisWebRTCPlayer.isConnecting = true;
|
|
MistVideo.container.setAttribute("data-loading","connecting"); //show loading icon while setting up the connection
|
|
|
|
//chrome on android has a bug where H264 is not available immediately after the tab is opened: https://bugs.chromium.org/p/webrtc/issues/detail?id=11620
|
|
//this workaround tries 5x with 100ms intervals before continuing
|
|
function checkH264(n){
|
|
var p = new Promise(function(resolve,reject){
|
|
function promise_body(n){
|
|
try {
|
|
var r = RTCRtpReceiver.getCapabilities("video");
|
|
for (var i = 0; i < r.codecs.length; i++) {
|
|
if (r.codecs[i].mimeType == "video/H264") {
|
|
resolve("H264 found :)");
|
|
return;
|
|
}
|
|
}
|
|
if (n > 0) {
|
|
setTimeout(function(){
|
|
promise_body(n-1);
|
|
},100);
|
|
}
|
|
else {
|
|
reject("H264 not found :(");
|
|
}
|
|
} catch (e) { resolve("Checker unavailable"); }
|
|
}
|
|
promise_body(n);
|
|
});
|
|
|
|
return p;
|
|
};
|
|
checkH264(5).catch(function(){
|
|
MistVideo.log("Beware: this device does not seem to be able to play H264.");
|
|
}).finally(function(){
|
|
thisWebRTCPlayer.signaling = new WebRTCSignaling(thisWebRTCPlayer.on_event);
|
|
var opts = {};
|
|
if (MistVideo.options.RTCIceServers) {
|
|
opts.iceServers = MistVideo.options.RTCIceServers;
|
|
}
|
|
else if (MistVideo.source.RTCIceServers) {
|
|
opts.iceServers = MistVideo.source.RTCIceServers;
|
|
}
|
|
thisWebRTCPlayer.peerConn = new RTCPeerConnection(opts);
|
|
thisWebRTCPlayer.MetaDataTrack = thisWebRTCPlayer.peerConn.createDataChannel("*",{protocol:"JSON"});
|
|
|
|
thisWebRTCPlayer.peerConn.ontrack = function(ev) {
|
|
video.srcObject = ev.streams[0];
|
|
if (callback) { callback(); }
|
|
};
|
|
thisWebRTCPlayer.peerConn.ondatachannel = function(){
|
|
console.warn("ondatachannel",arguments);
|
|
};
|
|
thisWebRTCPlayer.peerConn.onconnectionstatechange = function(e){
|
|
if (MistVideo.destroyed) { return; } //the player doesn't exist any more
|
|
switch (this.connectionState) {
|
|
case "failed": {
|
|
//WebRTC will never work (firewall maybe?)
|
|
MistVideo.log("UDP connection failed, trying next combo.","error");
|
|
MistVideo.nextCombo();
|
|
break;
|
|
}
|
|
case "connected": {
|
|
MistVideo.container.removeAttribute("data-loading");
|
|
}
|
|
case "disconnected":
|
|
case "closed":
|
|
case "new":
|
|
case "connecting":
|
|
default: {
|
|
MistVideo.log("WebRTC connection state changed to "+this.connectionState);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
thisWebRTCPlayer.peerConn.oniceconnectionstatechange = function(e){
|
|
if (MistVideo.destroyed) { return; } //the player doesn't exist any more
|
|
switch (this.iceConnectionState) {
|
|
case "failed": {
|
|
MistVideo.showError("ICE connection "+this.iceConnectionState);
|
|
break;
|
|
}
|
|
case "disconnected":
|
|
case "closed":
|
|
case "new":
|
|
case "checking":
|
|
case "connected":
|
|
case "completed":
|
|
default: {
|
|
MistVideo.log("WebRTC ICE connection state changed to "+this.iceConnectionState);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
MistUtil.event.send("webrtc_ready",null,video);
|
|
});
|
|
};
|
|
|
|
this.play = function(){
|
|
if (!this.isConnected) {
|
|
throw "Not connected, cannot play";
|
|
}
|
|
|
|
this.peerConn
|
|
.createOffer({
|
|
offerToReceiveAudio: true,
|
|
offerToReceiveVideo: true,
|
|
})
|
|
.then(function(offer){
|
|
thisWebRTCPlayer.localOffer = offer;
|
|
thisWebRTCPlayer.peerConn
|
|
.setLocalDescription(offer)
|
|
.then(function(){
|
|
thisWebRTCPlayer.signaling.sendOfferSDP(thisWebRTCPlayer.localOffer.sdp);
|
|
}, function(err){console.error(err)});
|
|
}, function(err){ throw err; });
|
|
};
|
|
|
|
this.stop = function(){
|
|
if (!this.isConnected) { throw "Not connected, cannot stop." }
|
|
this.signaling.send({type: "stop"});
|
|
};
|
|
this.seek = function(seekTime){
|
|
var p = new Promise(function(resolve,reject){
|
|
if (!thisWebRTCPlayer.isConnected || !thisWebRTCPlayer.signaling) {
|
|
if (thisWebRTCPlayer.isConnecting) {
|
|
|
|
var listener = MistUtil.event.addListener(MistVideo.video,"loadstart",function(){
|
|
thisWebRTCPlayer.seek(seekTime);
|
|
MistUtil.event.removeListener(listener);
|
|
});
|
|
return reject("Not connected yet, will seek once connected");
|
|
}
|
|
else {
|
|
return reject("Failed seek: not connected");
|
|
}
|
|
}
|
|
thisWebRTCPlayer.signaling.send({type: "seek", "seek_time": (seekTime == "live" ? "live" : seekTime*1e3)});
|
|
if ("seekPromise" in thisWebRTCPlayer.signaling) {
|
|
thisWebRTCPlayer.signaling.seekPromise.reject("Doing new seek");
|
|
}
|
|
|
|
thisWebRTCPlayer.signaling.seekPromise = {
|
|
resolve: function(msg){
|
|
resolve("seeked");
|
|
delete thisWebRTCPlayer.signaling.seekPromise;
|
|
},
|
|
reject: function(msg) {
|
|
reject("Failed to seek: "+msg);
|
|
delete thisWebRTCPlayer.signaling.seekPromise;
|
|
}
|
|
};
|
|
});
|
|
return p;
|
|
};
|
|
this.pause = function(){
|
|
if (!this.isConnected) { throw "Not connected, cannot pause." }
|
|
this.signaling.send({type: "hold"});
|
|
};
|
|
this.setTrack = function(obj){
|
|
if (!this.isConnected) { throw "Not connected, cannot set track." }
|
|
obj.type = "tracks";
|
|
this.signaling.send(obj);
|
|
};
|
|
this.playbackrate = function(value) {
|
|
if (typeof value == "undefined") {
|
|
return (me.webrtc.play_rate == "auto" ? 1 : me.webrtc.play_rate);
|
|
}
|
|
|
|
if (!this.isConnected) { throw "Not connected, cannot change playback rate." }
|
|
|
|
this.signaling.send({
|
|
type: "set_speed",
|
|
play_rate: value
|
|
});
|
|
|
|
};
|
|
this.getStats = function(callback){
|
|
this.peerConn.getStats().then(function(d){
|
|
var output = {};
|
|
var entries = Array.from(d.entries());
|
|
for (var i in entries) {
|
|
var value = entries[i];
|
|
if (value[1].type == "inbound-rtp") {
|
|
output[value[0]] = value[1];
|
|
}
|
|
}
|
|
callback(output);
|
|
});
|
|
};
|
|
//input only
|
|
/*
|
|
this.sendVideoBitrate = function(bitrate) {
|
|
this.send({type: "video_bitrate", video_bitrate: bitrate});
|
|
};
|
|
*/
|
|
|
|
this.connect();
|
|
}
|
|
function WebRTCSignaling(onEvent){
|
|
this.ws = null;
|
|
|
|
this.ws = new WebSocket(MistVideo.source.url.replace(/^http/,"ws"));
|
|
|
|
var ignoreopen = false;
|
|
|
|
this.ws.onopen = function() {
|
|
onEvent({type: "on_connected"});
|
|
};
|
|
|
|
this.ws.timeOut = MistVideo.timers.start(function(){
|
|
if (MistVideo.player.webrtc.signaling.ws.readyState == 0) {
|
|
MistVideo.log("WebRTC: socket timeout - try next combo");
|
|
MistVideo.nextCombo();
|
|
}
|
|
},5e3);
|
|
|
|
|
|
this.ws.onmessage = function(e) {
|
|
try {
|
|
var cmd = JSON.parse(e.data);
|
|
onEvent(cmd);
|
|
}
|
|
catch (err) {
|
|
console.error("Failed to parse a response from MistServer",err,e.data);
|
|
}
|
|
};
|
|
|
|
/* See http://tools.ietf.org/html/rfc6455#section-7.4.1 */
|
|
this.ws.onclose = function(ev) {
|
|
switch (ev.code) {
|
|
case 1006: {
|
|
//MistVideo.showError("WebRTC websocket closed unexpectedly");
|
|
}
|
|
default: {
|
|
onEvent({type: "on_disconnected", code: ev.code});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.sendOfferSDP = function(sdp) {
|
|
this.send({type: "offer_sdp", offer_sdp: sdp});
|
|
};
|
|
this.send = function(cmd) {
|
|
if (!this.ws) {
|
|
throw "Not initialized, cannot send "+JSON.stringify(cmd);
|
|
}
|
|
this.ws.send(JSON.stringify(cmd));
|
|
}
|
|
};
|
|
this.webrtc = new WebRTCPlayer();
|
|
|
|
this.api = {};
|
|
|
|
//override video duration
|
|
var duration;
|
|
Object.defineProperty(this.api,"duration",{
|
|
get: function(){ return duration; }
|
|
});
|
|
|
|
//override seeking
|
|
Object.defineProperty(this.api,"currentTime",{
|
|
get: function(){
|
|
return seekoffset + video.currentTime;
|
|
},
|
|
set: function(value){
|
|
seekoffset = value - video.currentTime;
|
|
video.pause();
|
|
var promise = me.webrtc.seek(value);
|
|
MistUtil.event.send("seeking",value,video);
|
|
if (promise) {
|
|
promise.catch(function(e){
|
|
//do nothing
|
|
//keep this code because not handling this shows an error message in the console:
|
|
// (Uncaught (in promise) Failed seek: not connected)
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
//override playbackrate
|
|
Object.defineProperty(this.api,"playbackRate",{
|
|
get: function(){
|
|
return me.webrtc.playbackrate();
|
|
},
|
|
set: function(value){
|
|
return me.webrtc.playbackrate(value);
|
|
//TODO send playbackrate changed event?
|
|
}
|
|
});
|
|
|
|
//redirect properties
|
|
//using a function to make sure the "item" is in the correct scope
|
|
function reroute(item) {
|
|
Object.defineProperty(me.api,item,{
|
|
get: function(){ return video[item]; },
|
|
set: function(value){
|
|
return video[item] = value;
|
|
}
|
|
});
|
|
}
|
|
var list = [
|
|
"volume"
|
|
,"muted"
|
|
,"loop"
|
|
,"paused",
|
|
,"error"
|
|
,"textTracks"
|
|
,"webkitDroppedFrameCount"
|
|
,"webkitDecodedFrameCount"
|
|
];
|
|
for (var i in list) {
|
|
reroute(list[i]);
|
|
}
|
|
|
|
//redirect methods
|
|
function redirect(item) {
|
|
if (item in video) {
|
|
me.api[item] = function(){
|
|
return video[item].call(video,arguments);
|
|
};
|
|
}
|
|
}
|
|
var list = ["load","getVideoPlaybackQuality"];
|
|
for (var i in list) {
|
|
redirect(list[i]);
|
|
}
|
|
|
|
//redirect play
|
|
me.api.play = function(){
|
|
var seekto;
|
|
if (me.api.currentTime) {
|
|
seekto = me.api.currentTime;
|
|
}
|
|
if ((MistVideo.info) && (MistVideo.info.type == "live")) {
|
|
seekto = "live";
|
|
}
|
|
if (seekto) {
|
|
var p = new Promise(function(resolve,reject){
|
|
if ((!me.webrtc.isConnected) && (me.webrtc.peerConn.iceConnectionState != "completed")) {
|
|
if (!me.webrtc.isConnecting) {
|
|
MistVideo.log("Received call to play while not connected, connecting "+me.webrtc.peerConn.iceConnectionState);
|
|
me.webrtc.connect(function(){
|
|
me.webrtc.seek(seekto).then(function(msg){
|
|
resolve("played "+msg);
|
|
}).catch(function(msg){
|
|
reject(msg);
|
|
});
|
|
});
|
|
}
|
|
else {
|
|
reject("Still connecting");
|
|
}
|
|
}
|
|
else {
|
|
me.webrtc.seek(seekto).then(function(msg){
|
|
resolve("played "+msg);
|
|
}).catch(function(msg){
|
|
reject(msg);
|
|
});
|
|
}
|
|
});
|
|
|
|
return p;
|
|
}
|
|
else {
|
|
return video.play();
|
|
}
|
|
};
|
|
|
|
me.api.getStats = function(){
|
|
if (me.webrtc && me.webrtc.isConnected) {
|
|
return new Promise(function(resolve,reject) {
|
|
me.webrtc.peerConn.getStats().then(function(a){
|
|
var r = {
|
|
audio: null,
|
|
video: null
|
|
};
|
|
var obj = Object.fromEntries(a);
|
|
for (var i in obj) {
|
|
if (obj[i].type == "track") {
|
|
//average jitter buffer in seconds
|
|
r[obj[i].kind] = obj[i];
|
|
}
|
|
}
|
|
resolve(r);
|
|
})
|
|
});
|
|
}
|
|
};
|
|
me.api.getLatency = function() {
|
|
var p = MistVideo.player.api.getStats();
|
|
if (p) {
|
|
return new Promise(function(resolve,reject){
|
|
p.then(function(first){
|
|
setTimeout(function(){
|
|
var p = me.api.getStats();
|
|
if (!p) { reject(); return; }
|
|
p.then(function(last){
|
|
var r = {};
|
|
for (var i in first) {
|
|
r[i] = first[i] && last[i] ? (last[i].jitterBufferDelay - first[i].jitterBufferDelay) / (last[i].jitterBufferEmittedCount - first[i].jitterBufferEmittedCount) : null;
|
|
}
|
|
resolve(r);
|
|
},reject);
|
|
},1e3);
|
|
},reject);
|
|
});
|
|
}
|
|
}
|
|
|
|
//redirect pause
|
|
me.api.pause = function(){
|
|
video.pause();
|
|
try {
|
|
me.webrtc.pause();
|
|
}
|
|
catch (e) {}
|
|
MistUtil.event.send("paused",null,video);
|
|
};
|
|
|
|
me.api.setTracks = function(obj){
|
|
if (me.webrtc.isConnected) {
|
|
me.webrtc.setTrack(obj);
|
|
}
|
|
else {
|
|
var f = function(){
|
|
me.webrtc.setTrack(obj);
|
|
MistUtil.event.removeListener({type: "webrtc_connected", callback: f, element: video});
|
|
};
|
|
MistUtil.event.addListener(video,"webrtc_connected",f);
|
|
}
|
|
};
|
|
function correctSubtitleSync() {
|
|
if (!me.api.textTracks[0]) { return; }
|
|
var currentoffset = me.api.textTracks[0].currentOffset || 0;
|
|
if (Math.abs(seekoffset - currentoffset) < 1) { return; } //don't bother if the change is small
|
|
var newCues = [];
|
|
for (var i = me.api.textTracks[0].cues.length-1; i >= 0; i--) {
|
|
var cue = me.api.textTracks[0].cues[i];
|
|
me.api.textTracks[0].removeCue(cue);
|
|
if (!("orig" in cue)) {
|
|
cue.orig = {start:cue.startTime,end:cue.endTime};
|
|
}
|
|
cue.startTime = cue.orig.start - seekoffset;
|
|
cue.endTime = cue.orig.end - seekoffset;
|
|
newCues.push(cue);
|
|
}
|
|
for (var i in newCues) {
|
|
me.api.textTracks[0].addCue(newCues[i]);
|
|
}
|
|
me.api.textTracks[0].currentOffset = seekoffset;
|
|
}
|
|
me.api.setSubtitle = function(trackmeta) {
|
|
//remove previous subtitles
|
|
var tracks = video.getElementsByTagName("track");
|
|
for (var i = tracks.length - 1; i >= 0; i--) {
|
|
video.removeChild(tracks[i]);
|
|
}
|
|
if (trackmeta) { //if the chosen track exists
|
|
//add the new one
|
|
var track = document.createElement("track");
|
|
video.appendChild(track);
|
|
track.kind = "subtitles";
|
|
track.label = trackmeta.label;
|
|
track.srclang = trackmeta.lang;
|
|
track.src = trackmeta.src;
|
|
track.setAttribute("default","");
|
|
|
|
//correct timesync
|
|
track.onload = correctSubtitleSync;
|
|
}
|
|
};
|
|
|
|
me.api.metaTrackSocket = function(){
|
|
//console.warn("new metaTrackSocket");
|
|
|
|
this.origin = {};
|
|
this.CONNECTING = 0;
|
|
this.OPEN = 1;
|
|
this.CLOSING = 2;
|
|
this.CLOSED = 3;
|
|
|
|
this.readyState = 0;
|
|
//follow readystate of origin, except when self is asked to close, then pretend to close and remove event listeners.
|
|
|
|
this.listeners = [];
|
|
var me = this;
|
|
|
|
MistUtil.event.addListener(MistVideo.video,"webrtc_ready",function(){
|
|
me.init();
|
|
});
|
|
this.init = function(){
|
|
this.origin = MistVideo.player.webrtc && MistVideo.player.webrtc.MetaDataTrack ? MistVideo.player.webrtc.MetaDataTrack : {};
|
|
|
|
//console.warn("init",this.origin);
|
|
if ("readyState" in this.origin) {
|
|
//console.warn("origin readystate",this.origin.readyState);
|
|
function onopen() {
|
|
me.readyState = me.OPEN;
|
|
me.onopen();
|
|
}
|
|
|
|
this.origin.addEventListener("open",onopen);
|
|
this.origin.onmessage = function(e){
|
|
//console.warn("metadata message",e);
|
|
};
|
|
this.origin.addEventListener("close",function(){
|
|
me.readyState = me.CLOSED;
|
|
me.onclose();
|
|
});
|
|
if (this.origin.readyState == "open") { onopen(); }
|
|
|
|
return true;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
this.open = function(){
|
|
//should be open once webrtc is active
|
|
|
|
if (this.readyState == this.OPEN) return; //already open
|
|
|
|
switch (this.origin.readyState) {
|
|
case "connecting": { this.readyState = this.CONNECTING; break; }
|
|
case "open": { this.readyState = this.OPEN; break; }
|
|
case "closing": { this.readyState = this.CLOSING; break; }
|
|
case "closed": { this.readyState = this.CLOSED; break; }
|
|
}
|
|
|
|
for (var i in this.listeners) {
|
|
this.origin.addEventListener.apply(this.origin,this.listeners[i]);
|
|
}
|
|
};
|
|
this.close = function(){
|
|
//don't actually close, but pretend
|
|
if (this.readyState >= this.CLOSING) return; //already closed
|
|
|
|
this.readyState = this.CLOSED;
|
|
|
|
//remove listeners
|
|
for (var i in this.listeners) {
|
|
this.removeEventListener.apply(this,this.listeners[i]);
|
|
}
|
|
};
|
|
this.send = function(){
|
|
if (this.origin.readyState == "open") return this.origin.send.apply(this,arguments);
|
|
return false;
|
|
};
|
|
this.onopen = function(){};
|
|
this.onclose = function(){};
|
|
this.addEventListener = function(){
|
|
this.listeners.push(arguments);
|
|
return this.origin.addEventListener.apply(this.origin,arguments);
|
|
};
|
|
this.removeEventListener = function(name,func){
|
|
//remove them from the listeners array and the origin
|
|
for (var i = this.listeners.length-1; i >= 0; i--) {
|
|
if ((name == this.listeners[i][0]) && (func == this.listeners[i][1])) {
|
|
this.listeners.splice(i,1);
|
|
break;
|
|
}
|
|
}
|
|
return this.origin.removeEventListener.apply(this.origin,arguments);
|
|
};
|
|
|
|
this.init();
|
|
|
|
return this;
|
|
};
|
|
|
|
//loop
|
|
MistUtil.event.addListener(video,"ended",function(){
|
|
if (me.api.loop) {
|
|
if (MistVideo.state == "Stream is online") {
|
|
me.webrtc.connect();
|
|
}
|
|
}
|
|
});
|
|
|
|
if ("decodingIssues" in MistVideo.skin.blueprints) {
|
|
//get additional dev stats
|
|
var vars = ["nackCount","pliCount","packetsLost","packetsReceived","bytesReceived"];
|
|
for (var j in vars) {
|
|
me.api[vars[j]] = 0;
|
|
}
|
|
var f = function() {
|
|
MistVideo.timers.start(function(){
|
|
me.webrtc.getStats(function(d){
|
|
for (var i in d) {
|
|
for (var j in vars) {
|
|
if (vars[j] in d[i]) {
|
|
me.api[vars[j]] = d[i][vars[j]];
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
f();
|
|
},1e3);
|
|
};
|
|
f();
|
|
}
|
|
|
|
me.api.ABR_resize = function(size){
|
|
MistVideo.log("Requesting the video track with the resolution that best matches the player size");
|
|
me.api.setTracks({video:"~"+[size.width,size.height].join("x")});
|
|
};
|
|
|
|
me.api.unload = function(){
|
|
try {
|
|
me.webrtc.stop();
|
|
me.webrtc.signaling.ws.close();
|
|
me.webrtc.peerConn.close();
|
|
} catch (e) {
|
|
//console.log(e);
|
|
}
|
|
};
|
|
|
|
callback(video);
|
|
|
|
};
|