Embed: Support for subtitles and metadata tracks over websocket

LSP: Preview tab: Show meta information for metadata tracks
LSP/embed: use new inclzero flag to receive sources that are enabled, but that don't currently have active tracks; and bugfix for metadata update for tracks not firing if sources have also changed
Embed: dashjs: disable stock subtitles unless activated
This commit is contained in:
Cat 2022-11-09 10:41:21 +01:00 committed by Thulinma
parent f3ba13d6bb
commit c337fff614
11 changed files with 514 additions and 85 deletions

File diff suppressed because one or more lines are too long

View file

@ -67,6 +67,10 @@ svg.icon .spin,svg.icon.spin{animation:mistvideo-spin 1.5s infinite linear;trans
.mistvideo{line-height:1.2;font-size:14.5px}
.mistvideo svg{margin:2.5px}
.mistvideo-video{display:flex;align-items:center;justify-content:center}
.mistvideo-subtitles{position:absolute;width:100%;height:100%;pointer-events:none;display:flex;align-items:flex-end;justify-content:center}
.mistvideo-subtitles>*{margin-bottom:.5em;padding:.1em .3em;text-align:center;background:rgba(0,0,0,.6);white-space:pre-wrap}
.mistvideo-subtitles>:empty{display:none}
.mistvideo[data-fullscreen] .mistvideo-subtitles{font-size:3vh}
.mistvideo-background{background-color:$background}
.mistvideo-totalTime:before{content:'/';margin:.2em}
.mistvideo-progress{padding:10px 0;margin:-10px 0;z-index:2}

View file

@ -67,6 +67,10 @@ svg.icon .spin,svg.icon.spin{animation:mistvideo-spin 1.5s infinite linear;trans
.mistvideo{line-height:1.2;font-size:14.5px}
.mistvideo svg{margin:2.5px}
.mistvideo-video{display:flex;align-items:center;justify-content:center}
.mistvideo-subtitles{position:absolute;width:100%;height:100%;pointer-events:none;display:flex;align-items:flex-end;justify-content:center}
.mistvideo-subtitles>*{margin-bottom:.5em;padding:.1em .3em;text-align:center;background:rgba(0,0,0,.6);white-space:pre-wrap}
.mistvideo-subtitles>:empty{display:none}
.mistvideo[data-fullscreen] .mistvideo-subtitles{font-size:3vh}
.mistvideo-background{background-color:$background}
.mistvideo-totalTime:before{content:'/';margin:.2em}
.mistvideo-progress{padding:10px 0;margin:-10px 0;z-index:2}

View file

@ -1 +1 @@
mistplayers.dashjs={name:"Dash.js player",mimes:["dash/video/mp4"],priority:MistUtil.object.keys(mistplayers).length+1,isMimeSupported:function(e){return MistUtil.array.indexOf(this.mimes,e)==-1?false:true},isBrowserSupported:function(e,t,i){if(location.protocol!=MistUtil.http.url.split(t.url).protocol){i.log("HTTP/HTTPS mismatch for this source");return false}if(location.protocol=="file:"){i.log("This source ("+e+") won't load if the page is run via file://");return false}if(!("MediaSource"in window)){return false}if(!MediaSource.isTypeSupported){return true}var r={};var a=false;for(var s in i.info.meta.tracks){if(i.info.meta.tracks[s].type=="meta"){if(i.info.meta.tracks[s].codec=="subtitle"){a=true}continue}if(!(i.info.meta.tracks[s].type in r)){r[i.info.meta.tracks[s].type]={}}r[i.info.meta.tracks[s].type][MistUtil.tracks.translateCodec(i.info.meta.tracks[s])]=1}var o=[];for(var n in r){var l=false;for(var f in r[n]){if(MediaSource.isTypeSupported('video/mp4;codecs="'+f+'"')){l=true;break}}if(l){o.push(n)}}if(a){for(var s in i.info.source){if(i.info.source[s].type=="html5/text/vtt"){o.push("subtitle");break}}}return o.length?o:false},player:function(){this.onreadylist=[]},scriptsrc:function(e){return e+"/dashjs.js"}};var p=mistplayers.dashjs.player;p.prototype=new MistPlayer;p.prototype.build=function(e,t){var i=this;this.onDashLoad=function(){if(e.destroyed){return}e.log("Building DashJS player..");var r=document.createElement("video");if("Proxy"in window){var a={get:{},set:{}};e.player.api=new Proxy(r,{get:function(e,t,i){if(t in a.get){return a.get[t].apply(e,arguments)}var r=e[t];if(typeof r==="function"){return function(){return r.apply(e,arguments)}}return r},set:function(e,t,i){if(t in a.set){return a.set[t].call(e,i)}return e[t]=i}});if(e.info.type=="live"){a.get.duration=function(){var t=0;if(this.buffered.length){t=this.buffered.end(this.buffered.length-1)}var i=((new Date).getTime()-e.player.api.lastProgress.getTime())*.001;return t+i+-1*e.player.api.liveOffset+45};a.set.currentTime=function(t){var i=t-e.player.api.duration;e.log("Seeking to "+MistUtil.format.time(t)+" ("+Math.round(i*-10)/10+"s from live)");e.video.currentTime=t};MistUtil.event.addListener(r,"progress",function(){e.player.api.lastProgress=new Date});e.player.api.lastProgress=new Date;e.player.api.liveOffset=0}}else{i.api=r}if(e.options.autoplay){r.setAttribute("autoplay","")}if(e.options.loop&&e.info.type!="live"){r.setAttribute("loop","")}if(e.options.poster){r.setAttribute("poster",e.options.poster)}if(e.options.muted){r.muted=true}if(e.options.controls=="stock"){r.setAttribute("controls","")}var s=dashjs.MediaPlayer().create();s.initialize(r,e.source.url,e.options.autoplay);i.dash=s;var o=["METRIC_ADDED","METRIC_UPDATED","METRIC_CHANGED","METRICS_CHANGED","FRAGMENT_LOADING_STARTED","FRAGMENT_LOADING_COMPLETED","LOG","PLAYBACK_TIME_UPDATED","PLAYBACK_PROGRESS"];for(var n in dashjs.MediaPlayer.events){if(o.indexOf(n)<0){i.dash.on(dashjs.MediaPlayer.events[n],function(t){e.log("Player event fired: "+t.type)})}}e.player.setSize=function(e){this.api.style.width=e.width+"px";this.api.style.height=e.height+"px"};e.player.api.setSource=function(t){e.player.dash.attachSource(t)};var l=false;i.dash.on("allTextTracksAdded",function(){l=true});e.player.api.setSubtitle=function(t){if(!l){var r=function(){e.player.api.setSubtitle(t);i.dash.off("allTextTracksAdded",r)};i.dash.on("allTextTracksAdded",r);return}if(!t){i.dash.enableText(false);return}var a=i.dash.getTracksFor("text");for(var s in a){var o="idx"in t?t.idx:t.trackid;if(a[s].id==o){i.dash.setTextTrack(s);if(!i.dash.isTextEnabled()){i.dash.enableText()}return true}}return false};MistUtil.event.addListener(r,"progress",function(t){if(e.container.getAttribute("data-loading")=="stalled"){e.container.removeAttribute("data-loading")}});i.api.unload=function(){i.dash.reset()};e.log("Built html");t(r)};if("dashjs"in window){this.onDashLoad()}else{var r=MistUtil.scripts.insert(e.urlappend(mistplayers.dashjs.scriptsrc(e.options.host)),{onerror:function(t){var i="Failed to load dashjs.js";if(t.message){i+=": "+t.message}e.showError(i)},onload:i.onDashLoad},e)}};
mistplayers.dashjs={name:"Dash.js player",mimes:["dash/video/mp4"],priority:MistUtil.object.keys(mistplayers).length+1,isMimeSupported:function(e){return MistUtil.array.indexOf(this.mimes,e)==-1?false:true},isBrowserSupported:function(e,t,i){if(location.protocol!=MistUtil.http.url.split(t.url).protocol){i.log("HTTP/HTTPS mismatch for this source");return false}if(location.protocol=="file:"){i.log("This source ("+e+") won't load if the page is run via file://");return false}if(!("MediaSource"in window)){return false}if(!MediaSource.isTypeSupported){return true}var r={};var a=false;for(var s in i.info.meta.tracks){if(i.info.meta.tracks[s].type=="meta"){if(i.info.meta.tracks[s].codec=="subtitle"){a=true}continue}if(!(i.info.meta.tracks[s].type in r)){r[i.info.meta.tracks[s].type]={}}r[i.info.meta.tracks[s].type][MistUtil.tracks.translateCodec(i.info.meta.tracks[s])]=1}var o=[];for(var n in r){var l=false;for(var f in r[n]){if(MediaSource.isTypeSupported('video/mp4;codecs="'+f+'"')){l=true;break}}if(l){o.push(n)}}if(a){for(var s in i.info.source){if(i.info.source[s].type=="html5/text/vtt"){o.push("subtitle");break}}}return o.length?o:false},player:function(){this.onreadylist=[]},scriptsrc:function(e){return e+"/dashjs.js"}};var p=mistplayers.dashjs.player;p.prototype=new MistPlayer;p.prototype.build=function(e,t){var i=this;this.onDashLoad=function(){if(e.destroyed){return}e.log("Building DashJS player..");var r=document.createElement("video");if("Proxy"in window){var a={get:{},set:{}};e.player.api=new Proxy(r,{get:function(e,t,i){if(t in a.get){return a.get[t].apply(e,arguments)}var r=e[t];if(typeof r==="function"){return function(){return r.apply(e,arguments)}}return r},set:function(e,t,i){if(t in a.set){return a.set[t].call(e,i)}return e[t]=i}});if(e.info.type=="live"){a.get.duration=function(){var t=0;if(this.buffered.length){t=this.buffered.end(this.buffered.length-1)}var i=((new Date).getTime()-e.player.api.lastProgress.getTime())*.001;return t+i+-1*e.player.api.liveOffset+45};a.set.currentTime=function(t){var i=t-e.player.api.duration;e.log("Seeking to "+MistUtil.format.time(t)+" ("+Math.round(i*-10)/10+"s from live)");e.video.currentTime=t};MistUtil.event.addListener(r,"progress",function(){e.player.api.lastProgress=new Date});e.player.api.lastProgress=new Date;e.player.api.liveOffset=0}}else{i.api=r}if(e.options.autoplay){r.setAttribute("autoplay","")}if(e.options.loop&&e.info.type!="live"){r.setAttribute("loop","")}if(e.options.poster){r.setAttribute("poster",e.options.poster)}if(e.options.muted){r.muted=true}if(e.options.controls=="stock"){r.setAttribute("controls","")}var s=dashjs.MediaPlayer().create();s.initialize(r,e.source.url,e.options.autoplay);i.dash=s;var o=["METRIC_ADDED","METRIC_UPDATED","METRIC_CHANGED","METRICS_CHANGED","FRAGMENT_LOADING_STARTED","FRAGMENT_LOADING_COMPLETED","LOG","PLAYBACK_TIME_UPDATED","PLAYBACK_PROGRESS"];for(var n in dashjs.MediaPlayer.events){if(o.indexOf(n)<0){i.dash.on(dashjs.MediaPlayer.events[n],function(t){e.log("Player event fired: "+t.type)})}}e.player.setSize=function(e){this.api.style.width=e.width+"px";this.api.style.height=e.height+"px"};e.player.api.setSource=function(t){e.player.dash.attachSource(t)};if(e.options.controls!="stock"){i.dash.updateSettings({streaming:{text:{defaultEnabled:false}}})}var l=false;i.dash.on("allTextTracksAdded",function(){l=true});e.player.api.setSubtitle=function(t){if(!l){var r=function(){e.player.api.setSubtitle(t);i.dash.off("allTextTracksAdded",r)};i.dash.on("allTextTracksAdded",r);return}if(!t){i.dash.enableText(false);return}var a=i.dash.getTracksFor("text");for(var s in a){var o="idx"in t?t.idx:t.trackid;if(a[s].id==o){i.dash.setTextTrack(s);if(!i.dash.isTextEnabled()){i.dash.enableText()}return true}}return false};MistUtil.event.addListener(r,"progress",function(t){if(e.container.getAttribute("data-loading")=="stalled"){e.container.removeAttribute("data-loading")}});i.api.unload=function(){i.dash.reset()};e.log("Built html");t(r)};if("dashjs"in window){this.onDashLoad()}else{var r=MistUtil.scripts.insert(e.urlappend(mistplayers.dashjs.scriptsrc(e.options.host)),{onerror:function(t){var i="Failed to load dashjs.js";if(t.message){i+=": "+t.message}e.showError(i)},onload:i.onDashLoad},e)}};

View file

@ -35,7 +35,8 @@ function MistVideo(streamName,options) {
maxheight: false, //no max height (apart from targets dimensions)
ABR_resize: true, //for supporting wrappers: when the player resizes, request a video track that matches the resolution best
ABR_bitrate: true, //for supporting wrappers: when there are playback issues, request a lower bitrate video track
useDateTime: true, //when the unix timestamp of the stream is known, display the date/time
useDateTime: true, //when the unix timestamp of the stream is known, display the date/time,
subscribeToMetaTrack: false, //pass [[track index,callback]]; the callback function will be called whenever the specified meta data track receives a message.
MistVideoObject: false//no reference object is passed
},options);
if (options.host) { options.host = MistUtil.http.url.sanitizeHost(options.host); }
@ -115,7 +116,6 @@ function MistVideo(streamName,options) {
this.log("A reloadDelay of more than an hour was set: assuming milliseconds were intended. ReloadDelay is now "+options.reloadDelay+"s");
}
new MistSkin(this);
this.checkCombo = function(options,quiet) {
@ -507,7 +507,7 @@ function MistVideo(streamName,options) {
if (MistVideo.reporting) {
MistVideo.reporting.init();
}
if ("api" in MistVideo.player) {
//add monitoring
@ -663,7 +663,251 @@ function MistVideo(streamName,options) {
MistUtil.event.addListener(MistVideo.video,events[i],function(){
if (MistVideo.monitor) { MistVideo.monitor.reset(); }
});
}
}
if ("currentTime" in MistVideo.player.api) {
var json_source = MistUtil.sources.find(MistVideo.info.source,{
type: "html5/text/javascript",
protocol: "ws"+(location.protocol.charAt(location.protocol.length-2) == "s" ? "s" : "")+":"
});
if (json_source) {
MistVideo.metaTrackSubscriptions = {
subscriptions: {},
socket: null,
listeners: {},
init: function(){
var me = this;
this.socket = new WebSocket(MistUtil.http.url.addParam(MistVideo.urlappend(json_source.url),{rate:1}));
me.send_queue = [];
me.checktimer = null;
me.s = function(obj){
if (me.socket.readyState == me.socket.OPEN) {
me.socket.send(JSON.stringify(obj));
return true;
}
if (me.socket.readyState >= me.socket.CLOSING) {
//reopen websocket
me.init();
}
//add message to queue
this.send_queue.push(obj);
};
var stayahead = 5; //ask MistServer to fastforward to stayahead seconds ahead, so we receive messages earlier
var isfarahead = false; //for rate limiting the 'pause when too far ahead'-function
me.socket.setTracks = function(){
me.s({type:"tracks",meta:MistUtil.object.keys(me.subscriptions).join(",")});
};
me.socket.onopen = function(){
MistVideo.log("Metadata socket opened");
me.socket.setTracks();
if (MistVideo.player.api.playbackRate != 1) { me.s({type:"set_speed",play_rate:MistVideo.player.api.playbackRate}); }
me.s({type:"seek",seek_time:Math.round(MistVideo.player.api.currentTime*1e3),ff_to:Math.round((MistVideo.player.api.currentTime+stayahead)*1e3)});
me.socket.addEventListener("message",function(e){
if (!e.data) { MistVideo.log("Subtitle websocket received empty message."); return; }
var message = JSON.parse(e.data);
if (!message) { MistVideo.log("Subtitle websocket received invalid message."); return; }
if (("time" in message) && ("track" in message) && ("data" in message)) {
if (message.track in me.subscriptions) {
//console.warn("received:",message.track,message.data);
me.subscriptions[message.track].buffer.push(message);
console.warn("received:",message.track,message.time*1e-3,"currentTime:",MistVideo.player.api.currentTime,"latency",Math.round(MistVideo.player.api.currentTime-message.time*1e-3),"bufferlength:",me.subscriptions[message.track].buffer.length,"timer:",!!me.checktimer);
if (!me.checktimer) {
me.check();
}
else {
var willCheckAt = MistVideo.timers.list[me.checktimer];
if (willCheckAt) {
var messageAt = (new Date()).getTime() + message.time - MistVideo.player.api.currentTime*1e3;
if (willCheckAt > messageAt) {
MistVideo.log("The metadata socket received a message that should be displayed sooner than the current check time; resetting");
MistVideo.timers.stop(me.checktimer);
me.checktimer = null;
me.check();
}
}
}
}
}
//per track, the messages should arrive in the correct order and we shouldn't need to do sorting
if ("type" in message) {
switch (message.type) {
case "on_time": {
if (!isfarahead && (message.data.current > (MistVideo.player.api.currentTime + stayahead*6)*1e3)) {
//the playing point for the metadata track is very far ahead of the player;
isfarahead = true;
me.s({type:"hold"});
MistVideo.log("Pausing metadata buffer because it is very far ahead, checking again in 5 seconds: "+message.data.current+" > "+MistVideo.player.api.currentTime*1e3)
MistVideo.timers.start(function(){
if (!MistVideo.player.api.paused) { me.s({type:"play"}); }
me.s({type:"fast_forward",ff_to:Math.round((MistVideo.player.api.currentTime+stayahead)*1e3)});
},5e3);
}
break;
}
case "seek": {
for (var i in me.subscriptions) {
me.subscriptions[i].buffer = [];
}
MistVideo.log("Cleared metadata buffer after completed seek");
if (me.checktimer) {
//there might be a timer going for some time in the future: stop it,
MistVideo.timers.stop(me.checktimer);
me.checktimer = null;
}
}
break;
}
}
});
me.socket.onclose = function(){
//dont me.init();, send function will reopen if needed instead
MistVideo.log("Metadata socket closed");
}
while (me.send_queue.length && (me.socket.readyState == me.socket.OPEN)) {
me.s(me.send_queue.shift());
}
};
if (!("seeked" in this.listeners)) { //prevent duplication
var lastff = (new Date()).getTime(); //init at now, as a seek with ff_to is also sent at init time
me.check = function(){
//console.warn(me.checktimer,"check");
if (me.checktimer) {
MistVideo.timers.stop(me.checktimer);
me.checktimer = null;
}
if (MistVideo.player.api.paused) { return; }
var nextAtGlobal = null;
for (var i in me.subscriptions) {
var buffer = me.subscriptions[i].buffer;
while (buffer.length && (buffer[0].time <= MistVideo.player.api.currentTime*1e3)) {
var message = buffer.shift();
if (message.time < (MistVideo.player.api.currentTime - 5) * 1e3) {
//the message is at least 5 seconds older than the video time
continue;
}
else {
for (var j in me.subscriptions[i].callbacks) {
me.subscriptions[i].callbacks[j].call(MistVideo,message);
}
}
}
if (buffer.length) {
//save when the next message should be played
nextAtGlobal = Math.min(nextAtGlobal === null ? 1e9 : nextAtGlobal,buffer[0].time);
}
}
//add rate limiting: do not ask for fast forward more than once every 5 seconds
var now = (new Date()).getTime()
if (now > lastff+5e3) {
me.s({type:"fast_forward",ff_to:Math.round((MistVideo.player.api.currentTime+stayahead)*1e3)});
lastff = now;
}
if (nextAtGlobal) {
var delay = nextAtGlobal-MistVideo.player.api.currentTime*1e3;
me.checktimer = MistVideo.timers.start(function(){
me.check();
},delay);
}
};
this.listeners.seeked = MistUtil.event.addListener(MistVideo.video,"seeked",function(){
for (var i in me.subscriptions) {
me.subscriptions[i].buffer = [];
}
me.s({type:"seek",seek_time:Math.round(MistVideo.player.api.currentTime*1e3),ff_to:Math.round((MistVideo.player.api.currentTime+stayahead)*1e3)});
lastff = (new Date()).getTime();
//console.warn("seek to",Math.round(MistVideo.player.api.currentTime*1e3));
});
this.listeners.pause = MistUtil.event.addListener(MistVideo.video,"pause",function(){
me.s({type:"hold"});
MistVideo.timers.stop(me.checktimer);
me.checktimer = null;
});
this.listeners.playing = MistUtil.event.addListener(MistVideo.video,"playing",function(){
me.s({type:"play"});
if (!me.checktimer) me.check();
});
this.listeners.ratechange = MistUtil.event.addListener(MistVideo.video,"ratechange",function(){
me.s({type:"set_speed",play_rate:MistVideo.player.api.playbackRate});
});
}
},
destroy: function(){
MistVideo.log("Closing metadata socket..");
this.socket.close();
this.socket = null;
this.subscriptions = {};
for (var i in this.listeners) {
MistUtil.event.removeListener(this.listeners[i]);
}
this.listeners = {};
},
add: function (trackid,callback) {
if (typeof callback != "function") { return; }
if (!(trackid in this.subscriptions)) {
this.subscriptions[trackid] = {
buffer: [],
callbacks: []
};
}
this.subscriptions[trackid].callbacks.push(callback);
if (this.socket === null) {
this.init();
}
else {
this.socket.setTracks();
}
},
remove: function(trackid,callback){
if (trackid in this.subscriptions) {
for (var i in this.subscriptions[trackid].callbacks) {
if (callback == this.subscriptions[trackid].callbacks[i]) {
this.subscriptions[trackid].callbacks.splice(i,1);
break;
}
}
if (this.subscriptions[trackid].callbacks.length == 0) {
delete this.subscriptions[trackid];
if (MistUtil.object.keys(this.subscriptions).length) {
this.socket.setTracks();
}
else {
this.destroy();
}
}
}
}
};
if (options.subscribeToMetaTrack.length) {
if (typeof options.subscribeToMetaTrack[0] != "object") {
options.subscribeToMetaTrack = [options.subscribeToMetaTrack];
}
for (var i in options.subscribeToMetaTrack) {
MistVideo.metaTrackSubscriptions.add.apply(MistVideo.metaTrackSubscriptions,options.subscribeToMetaTrack[i]);
}
}
}
}
}
//remove placeholder and add UI structure
@ -1051,7 +1295,7 @@ function MistVideo(streamName,options) {
//switch to polling-mode if websockets are not supported
function openWithGet() {
var url = MistUtil.http.url.addParam(MistVideo.urlappend(options.host+"/json_"+encodeURIComponent(MistVideo.stream)+".js"),{metaeverywhere:1});
var url = MistUtil.http.url.addParam(MistVideo.urlappend(options.host+"/json_"+encodeURIComponent(MistVideo.stream)+".js"),{metaeverywhere:1,inclzero:1});
MistVideo.log("Requesting stream info from "+url);
MistUtil.http.get(url,function(d){
if (MistVideo.destroyed) { return; }
@ -1070,7 +1314,7 @@ function MistVideo(streamName,options) {
function openSocket() {
MistVideo.log("Opening stream status stream through websocket..");
var url = MistVideo.options.host.replace(/^http/i,"ws");
url = MistUtil.http.url.addParam(MistVideo.urlappend(url+"/json_"+encodeURIComponent(MistVideo.stream)+".js"),{metaeverywhere:1});
url = MistUtil.http.url.addParam(MistVideo.urlappend(url+"/json_"+encodeURIComponent(MistVideo.stream)+".js"),{metaeverywhere:1,inclzero:1});
var socket;
try {
socket = new WebSocket(url);
@ -1415,10 +1659,8 @@ function MistVideo(streamName,options) {
if (diff) {
//console.log("Difference",diff,data,MistVideo.info);
if ("source" in diff) {
if ("error" in MistVideo.info) {
MistVideo.reload("Reloading, stream info has error");
}
if (("source" in diff) && ("error" in MistVideo.info)) {
MistVideo.reload("Reloading, stream info has error");
return;
}
@ -1513,6 +1755,9 @@ function MistVideo(streamName,options) {
}
}
}
if (this.metaTrackSubscriptions && this.metaTrackSubscriptions.socket) {
this.metaTrackSubscriptions.destroy();
}
if ((this.UI) && (this.UI.elements)) {
for (var i in this.UI.elements) {
var e = this.UI.elements[i];

View file

@ -59,7 +59,8 @@ MistSkins["default"] = {
type: "container",
children: [
{type: "videobackground", alwaysDisplay: false, delay: 5 },
{type: "video"}
{type: "video"},
{type: "subtitles"}
]
},
controls: {
@ -1533,20 +1534,30 @@ MistSkins["default"] = {
for (var j in tracktypes) {
var type = tracktypes[j];
var t = tracks[type];
if (MistUtil.array.indexOf(["video","audio","subtitle"],type) <= -1) {
//Do not display this track type
continue;
}
if (type == "subtitle") {
if ((!("player" in MistVideo)) || (!("api" in MistVideo.player)) || (!("setSubtitle" in MistVideo.player.api))) {
if ((!("player" in MistVideo)) || (!("api" in MistVideo.player)) || (!("setWSSubtitle" in MistVideo.player.api) && !("setSubtitle" in MistVideo.player.api))) {
//this player does not support adding subtitles, don't show track selection in the interface
MistVideo.log("Subtitle selection was disabled as this player does not support it.");
continue;
}
var mime = "html5/text/vtt"
if ("setWSSubtitle" in MistVideo.player.api) {
mime = "html5/text/javascript";
}
//check if the VTT output is available
var subtitleSource = false;
for (var i in MistVideo.info.source) {
var source = MistVideo.info.source[i];
//this is a subtitle source, and it's the same protocol (HTTP/HTTPS) as the video source
if ((source.type == "html5/text/vtt") && (MistUtil.http.url.split(source.url).protocol == MistUtil.http.url.split(MistVideo.source.url).protocol.replace(/^ws/,"http"))) {
if ((source.type == mime) && (MistUtil.http.url.split(source.url).protocol == MistUtil.http.url.split(MistVideo.source.url).protocol.replace(/^ws/,"http"))) {
subtitleSource = source.url.replace(/.srt$/,".vtt");
break;
}
@ -1554,7 +1565,7 @@ MistSkins["default"] = {
if (!subtitleSource) {
//if we can't find a subtitle output, don't show track selection in the interface
MistVideo.log("Subtitle selection was disabled as an SRT source could not be found.");
MistVideo.log("Subtitle selection was disabled as a source could not be found.");
continue;
}
@ -1721,17 +1732,22 @@ MistSkins["default"] = {
localStorage["mistSubtitleLanguage"] = t[this.value].lang;
}
catch (e) {}
if (this.value != "") {
//gather metadata for this subtitle track here
var trackinfo = MistUtil.object.extend({},t[this.value]);
trackinfo.label = orderValues(trackinfo.describe).join(" ");
trackinfo.src = MistUtil.http.url.addParam(subtitleSource,{track:this.value});
MistVideo.player.api.setSubtitle(trackinfo);
if ("setWSSubtitle" in MistVideo.player.api) {
MistVideo.player.api.setWSSubtitle(this.value == "" ? undefined : this.value);
}
else {
MistVideo.player.api.setSubtitle();
if (this.value != "") {
//gather metadata for this subtitle track here
var trackinfo = MistUtil.object.extend({},t[this.value]);
trackinfo.label = orderValues(trackinfo.describe).join(" ");
trackinfo.src = MistUtil.http.url.addParam(subtitleSource,{track:this.value});
MistVideo.player.api.setSubtitle(trackinfo);
}
else {
MistVideo.player.api.setSubtitle();
}
}
});
@ -2101,6 +2117,7 @@ MistSkins["default"] = {
}
if (event.defaultPrevented) {
MistVideo.log("Error event was defaultPrevented, not showing.");
container.clear();
}
};
@ -2277,8 +2294,89 @@ MistSkins["default"] = {
return ele;
}
},
subtitles: function(options){
if (!("WebSocket" in window)) { return false; }
var MistVideo = this;
if (!("player" in MistVideo) || !("api" in MistVideo.player) || !("currentTime" in MistVideo.player.api)) { return false; }
if (!("metaTrackSubscriptions" in MistVideo)) { return false; }
function clearFormatting(str) {
str = str.replace(/\<\/?[bui]\>/gi,""); //remove <b>,</b>,<u>,</u>,<i>,</i>
str = str.replace(/{\/?[bui]}/gi,""); //remove {b},{/b},{u},{/u},{i},{/i}
str = str.replace(/{\\a\d+}/gi,""); //remove {\a3} (line position)
str = str.replace(/\<\/?font[^>]*?\>/gi,""); //remove <font color="white">,</font>
return str;
}
var container = document.createElement("div");
var c = document.createElement("span");
container.appendChild(c);
var textNode = document.createTextNode("");
c.appendChild(textNode);
var timer = false;
function displayMessage(message) {
textNode.nodeValue = clearFormatting(message.data);
if (timer) {
//a previous message is still being displayed, remove the timer so that it doesn't remove the new message
MistVideo.timers.stop(timer);
timer = null;
}
function setTimer(delay) {
timer = MistVideo.timers.start(function(){
if (MistVideo.player.api.paused) {
//leave the subtitle for now, and start a new timer once the video starts playing
var playing = MistUtil.event.addListener(MistVideo.video,"playing",function(){
setTimer(message.time + ("duration" in message ? message.duration : 5e3) - MistVideo.player.api.currentTime*1e3);
MistUtil.event.removeListener(playing);
});
return;
}
textNode.nodeValue = "";
},delay);
}
setTimer("duration" in message ? message.duration : 5e3);
}
//when seeking, clear the current subtitle message
MistUtil.event.addListener(MistVideo.video,"seeked",function(){
textNode.nodeValue = "";
if (timer) { MistVideo.timers.stop(timer); }
timer = null;
});
if (!("setWSSubtitle" in MistVideo.player.api)) {
//insert generic subtitle function unless it already exists
var trackid = false;
MistVideo.player.api.setWSSubtitle = function(id){
if (id == trackid) { return; } //already selected
//first add, then remove: this prevents the websocket closing because no tracks are selected
if (typeof id != "undefined") {
MistVideo.metaTrackSubscriptions.add(id,displayMessage);
}
if (id != trackid) {
MistVideo.metaTrackSubscriptions.remove(trackid,displayMessage);
}
trackid = (id == "undefined" ? false : id);
}
}
return container;
}
},
colors: {
fill: "#fff",

View file

@ -10,6 +10,28 @@
align-items: center;
justify-content: center;
}
.mistvideo-subtitles {
position: absolute;
width: 100%;
height: 100%;
pointer-events: none;
display: flex;
align-items: flex-end;
justify-content: center;
}
.mistvideo-subtitles > * {
margin-bottom: 0.5em;
padding: 0.1em 0.3em;
text-align: center;
background: rgba(0,0,0,0.6);
white-space: pre-wrap;
}
.mistvideo-subtitles > *:empty {
display: none;
}
.mistvideo[data-fullscreen] .mistvideo-subtitles {
font-size: 3vh;
}
.mistvideo-background { background-color: $background; }
.mistvideo-totalTime:before {
content: '/';

View file

@ -848,7 +848,7 @@ var MistUtil = {
}
break;
case "rate":
name[j] = Math.round(track.rate)+"Khz";
name[j] = Math.round(track.rate*1e-3)+"Khz";
break;
case "language":
if (track[j] != "Undetermined") { name[j] = track[j]; }
@ -1130,5 +1130,35 @@ var MistUtil = {
getAndroid: function(){
var match = navigator.userAgent.toLowerCase().match(/android\s([\d\.]*)/i);
return match ? match[1] : false;
},
sources: {
find: function(sources,matchObj){
/*
Example use:
MistUtil.sources.find(MistVideo.info.source,{
type: "html5/text/javascript",
protocol: "wss:"
})
*/
outer:
for (var i in sources) {
for (var j in matchObj) {
if (j == "protocol") {
if (sources[i].url.slice(0,matchObj.protocol.length) != matchObj.protocol) {
continue outer;
}
}
else {
if (sources[i][j] != matchObj[j]) {
continue outer;
}
}
}
//if any key of matchObj did not match the source, the outer loop was continued and this code does not execute
return sources[i];
}
return false;
}
}
};

View file

@ -177,6 +177,9 @@ p.prototype.build = function (MistVideo,callback) {
MistVideo.player.dash.attachSource(url);
};
if (MistVideo.options.controls != "stock"){
me.dash.updateSettings({streaming:{text:{defaultEnabled:false}}});
}
var subsloaded = false;
me.dash.on("allTextTracksAdded",function(){
subsloaded = true;