Embed: Raw/WS (HEVC only atm) player
This commit is contained in:
parent
ac13686048
commit
86379e44eb
20 changed files with 12742 additions and 53 deletions
|
@ -18,6 +18,21 @@ mistplayers.dashjs = {
|
|||
MistVideo.log("This source ("+mimetype+") won't load if the page is run via file://");
|
||||
return false;
|
||||
}
|
||||
|
||||
var codecs = {};
|
||||
for (var i in MistVideo.info.meta.tracks) {
|
||||
if (MistVideo.info.meta.tracks[i].type != "meta") {
|
||||
codecs[MistVideo.info.meta.tracks[i].codec] = 1;
|
||||
}
|
||||
}
|
||||
codecs = MistUtil.object.keys(codecs);
|
||||
//if there's a h265 track, remove it from the list of codecs
|
||||
for (var i = codecs.length-1; i >= 0; i--) {
|
||||
if (codecs[i].substr(0,4) == "HEVC") {
|
||||
codecs.splice(i,1);
|
||||
}
|
||||
}
|
||||
if (codecs.length < source.simul_tracks) { return false; } //if there's no longer enough playable tracks, skip this player
|
||||
|
||||
return ("MediaSource" in window);
|
||||
},
|
||||
|
|
|
@ -11,7 +11,22 @@ mistplayers.hlsjs = {
|
|||
MistVideo.log("HTTP/HTTPS mismatch for this source");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
var codecs = {};
|
||||
for (var i in MistVideo.info.meta.tracks) {
|
||||
if (MistVideo.info.meta.tracks[i].type != "meta") {
|
||||
codecs[MistVideo.info.meta.tracks[i].codec] = 1;
|
||||
}
|
||||
}
|
||||
codecs = MistUtil.object.keys(codecs);
|
||||
//if there's a h265 track, remove it from the list of codecs
|
||||
for (var i = codecs.length-1; i >= 0; i--) {
|
||||
if (codecs[i].substr(0,4) == "HEVC") {
|
||||
codecs.splice(i,1);
|
||||
}
|
||||
}
|
||||
if (codecs.length < source.simul_tracks) { return false; } //if there's no longer enough playable tracks, skip this player
|
||||
|
||||
return true;
|
||||
},
|
||||
player: function(){},
|
||||
|
|
|
@ -42,37 +42,38 @@ mistplayers.html5 = {
|
|||
return support;
|
||||
}
|
||||
|
||||
function translateCodec(track) {
|
||||
|
||||
function bin2hex(index) {
|
||||
return ("0"+track.init.charCodeAt(index).toString(16)).slice(-2);
|
||||
}
|
||||
|
||||
switch (track.codec) {
|
||||
case "AAC":
|
||||
return "mp4a.40.2";
|
||||
case "MP3":
|
||||
return "mp3";
|
||||
//return "mp4a.40.34";
|
||||
case "AC3":
|
||||
return "ec-3";
|
||||
case "H264":
|
||||
return "avc1."+bin2hex(1)+bin2hex(2)+bin2hex(3);
|
||||
case "HEVC":
|
||||
return "hev1."+bin2hex(1)+bin2hex(6)+bin2hex(7)+bin2hex(8)+bin2hex(9)+bin2hex(10)+bin2hex(11)+bin2hex(12);
|
||||
default:
|
||||
return track.codec.toLowerCase();
|
||||
}
|
||||
|
||||
}
|
||||
var codecs = {};
|
||||
for (var i in MistVideo.info.meta.tracks) {
|
||||
if (MistVideo.info.meta.tracks[i].type != "meta") {
|
||||
codecs[translateCodec(MistVideo.info.meta.tracks[i])] = 1;
|
||||
}
|
||||
}
|
||||
codecs = MistUtil.object.keys(codecs);
|
||||
|
||||
if (shortmime == "video/mp4") {
|
||||
function translateCodec(track) {
|
||||
|
||||
function bin2hex(index) {
|
||||
return ("0"+track.init.charCodeAt(index).toString(16)).slice(-2);
|
||||
}
|
||||
|
||||
switch (track.codec) {
|
||||
case "AAC":
|
||||
return "mp4a.40.2";
|
||||
case "MP3":
|
||||
return "mp3";
|
||||
//return "mp4a.40.34";
|
||||
case "AC3":
|
||||
return "ec-3";
|
||||
case "H264":
|
||||
return "avc1."+bin2hex(1)+bin2hex(2)+bin2hex(3);
|
||||
case "HEVC":
|
||||
return "hev1."+bin2hex(1)+bin2hex(6)+bin2hex(7)+bin2hex(8)+bin2hex(9)+bin2hex(10)+bin2hex(11)+bin2hex(12);
|
||||
default:
|
||||
return track.codec.toLowerCase();
|
||||
}
|
||||
|
||||
}
|
||||
var codecs = {};
|
||||
for (var i in MistVideo.info.meta.tracks) {
|
||||
if (MistVideo.info.meta.tracks[i].type != "meta") {
|
||||
codecs[translateCodec(MistVideo.info.meta.tracks[i])] = 1;
|
||||
}
|
||||
}
|
||||
codecs = MistUtil.object.keys(codecs);
|
||||
if (codecs.length) {
|
||||
if (codecs.length > source.simul_tracks) {
|
||||
//not all of the tracks have to work
|
||||
|
@ -85,9 +86,18 @@ mistplayers.html5 = {
|
|||
}
|
||||
return (working >= source.simul_tracks);
|
||||
}
|
||||
shortmime += ";codecs=\""+codecs.join(",")+"\"";
|
||||
return test(shortmime+";codecs=\""+codecs.join(",")+"\"");
|
||||
}
|
||||
}
|
||||
else {
|
||||
//if there's a h265 track, remove it from the list of codecs
|
||||
for (var i = codecs.length-1; i >= 0; i--) {
|
||||
if (codecs[i].substr(0,4) == "hev1") {
|
||||
codecs.splice(i,1);
|
||||
}
|
||||
}
|
||||
if (codecs.length < source.simul_tracks) { return false; } //if there's no longer enough playable tracks, skip this player
|
||||
}
|
||||
|
||||
support = test(shortmime);
|
||||
} catch(e){}
|
||||
|
|
|
@ -44,6 +44,10 @@ mistplayers.mews = {
|
|||
var codecs = {};
|
||||
for (var i in MistVideo.info.meta.tracks) {
|
||||
if (MistVideo.info.meta.tracks[i].type != "meta") {
|
||||
if (MistVideo.info.meta.tracks[i].codec == "HEVC") {
|
||||
//the iPad claims to be able to play MP4/WS H265 tracks.. haha no.
|
||||
continue;
|
||||
}
|
||||
codecs[translateCodec(MistVideo.info.meta.tracks[i])] = MistVideo.info.meta.tracks[i].codec;
|
||||
}
|
||||
}
|
||||
|
|
675
embed/wrappers/rawws.js
Normal file
675
embed/wrappers/rawws.js
Normal file
|
@ -0,0 +1,675 @@
|
|||
mistplayers.rawws = {
|
||||
name: "RAW to Canvas",
|
||||
mimes: ["ws/video/raw"],
|
||||
priority: MistUtil.object.keys(mistplayers).length + 1,
|
||||
isMimeSupported: function (mimetype) {
|
||||
return (MistUtil.array.indexOf(this.mimes,mimetype) == -1 ? false : true);
|
||||
},
|
||||
isBrowserSupported: function (mimetype,source,MistVideo) {
|
||||
|
||||
//check for http/https mismatch
|
||||
if (location.protocol != MistUtil.http.url.split(source.url.replace(/^ws/,"http")).protocol) {
|
||||
if ((location.protocol == "file:") && (MistUtil.http.url.split(source.url.replace(/^ws/,"http")).protocol == "http:")) {
|
||||
MistVideo.log("This page was loaded over file://, the player might not behave as intended.");
|
||||
}
|
||||
else {
|
||||
MistVideo.log("HTTP/HTTPS mismatch for this source");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (var i in MistVideo.info.meta.tracks) {
|
||||
if (MistVideo.info.meta.tracks[i].codec == "HEVC") { return true; }
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
player: function(){
|
||||
this.onreadylist = [];
|
||||
},
|
||||
scriptsrc: function(host) { return host+"/libde265.js"; }
|
||||
};
|
||||
var p = mistplayers.rawws.player;
|
||||
p.prototype = new MistPlayer();
|
||||
p.prototype.build = function (MistVideo,callback) {
|
||||
|
||||
var player = this;
|
||||
|
||||
player.onDecoderLoad = function() {
|
||||
if (MistVideo.destroyed) { return; }
|
||||
|
||||
MistVideo.log("Building rawws player..");
|
||||
|
||||
var api = {};
|
||||
MistVideo.player.api = api;
|
||||
|
||||
var ele = document.createElement("canvas");
|
||||
var ctx = ele.getContext("2d");
|
||||
|
||||
ele.style.objectFit = "contain";
|
||||
|
||||
player.vars = {}; //will contain data like currentTime
|
||||
if (MistVideo.options.autoplay) {
|
||||
//if wantToPlay is false, playback will be paused after the first frame
|
||||
player.vars.wantToPlay = true;
|
||||
}
|
||||
player.dropping = false;
|
||||
player.frames = { //contains helper functions and statistics
|
||||
received: 0,
|
||||
bitsReceived: 0,
|
||||
decoded: 0,
|
||||
dropped: 0,
|
||||
behind: function(){
|
||||
return this.received - this.decoded - this.dropped;
|
||||
},
|
||||
timestamps: {},
|
||||
frame2time: function(frame,clean){
|
||||
if (frame in this.timestamps) {
|
||||
if (clean) {
|
||||
//clear any entries before the entry we're actually using
|
||||
for (var i in this.timestamps) {
|
||||
if (i == frame) { break; }
|
||||
delete this.timestamps[i];
|
||||
}
|
||||
}
|
||||
return this.timestamps[frame]*1e-3;
|
||||
}
|
||||
return 0;
|
||||
|
||||
|
||||
//get the closest known timestamp for the frame, then correct for the offset using the framerate
|
||||
var last = 0;
|
||||
var last_time = 0;
|
||||
for (var i in this.timestamps) {
|
||||
last = i;
|
||||
last_time = this.timestamps[i];
|
||||
if (i >= frame) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (clean) {
|
||||
//clear any entries before the entry we're actually using
|
||||
for (var i in this.timestamps) {
|
||||
if (i == last) { break; }
|
||||
delete this.timestamps[i];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var framerate = this.framerate();
|
||||
if ((typeof framerate != "undefined") && (framerate > 0)) {
|
||||
return last_time + (frame - last) / framerate;
|
||||
}
|
||||
else {
|
||||
return last_time;
|
||||
}
|
||||
|
||||
},
|
||||
history: {
|
||||
log: [],
|
||||
add: function() {
|
||||
this.log.unshift({
|
||||
time: new Date().getTime(),
|
||||
received: player.frames.received,
|
||||
bitsReceived: player.frames.bitsReceived,
|
||||
decoded: player.frames.decoded
|
||||
});
|
||||
if (this.log.length > 3) { this.log.splice(3); }
|
||||
}
|
||||
},
|
||||
framerate_in: function(){
|
||||
var l = this.history.log.length -1;
|
||||
if (l < 1) { return 0; }
|
||||
var dframe = this.history.log[0].received - this.history.log[l].received;
|
||||
var dt = (this.history.log[0].time - this.history.log[l].time) * 1e-3;
|
||||
return dframe / dt;
|
||||
},
|
||||
bitrate_in: function(){
|
||||
var l = this.history.log.length -1;
|
||||
if (l < 1) { return 0; }
|
||||
var dbits = this.history.log[0].bitsReceived - this.history.log[l].bitsReceived;
|
||||
var dt = (this.history.log[0].time - this.history.log[l].time) * 1e-3;
|
||||
return dbits / dt;
|
||||
},
|
||||
framerate_out: function(){
|
||||
var l = this.history.log.length -1;
|
||||
if (l < 1) { return 0; }
|
||||
var dframe = this.history.log[0].decoded - this.history.log[l].decoded;
|
||||
var dt = (this.history.log[0].time - this.history.log[l].time) * 1e-3;
|
||||
return dframe / dt;
|
||||
},
|
||||
framerate: function(){
|
||||
if ("rate_theoretical" in this) { return this.rate_theoretical; }
|
||||
return this.framerate_in();
|
||||
//return undefined;
|
||||
},
|
||||
keepingUp: function(){
|
||||
var l = this.history.log.length -1;
|
||||
if (l < 1) { return 0; }
|
||||
var dBehind = this.history.log[l].received - this.history.log[l].decoded - (this.history.log[0].received - this.history.log[0].decoded);
|
||||
var dt = (this.history.log[0].time - this.history.log[l].time) * 1e-3;
|
||||
var keepingUp_frames = dBehind / dt; //amount of frames falling behind (negative) or catching up (positive) per second
|
||||
|
||||
return keepingUp_frames / this.framerate(); //in seconds per seconds
|
||||
}
|
||||
};
|
||||
api.framerate_in = function () { return player.frames.framerate_in(); }
|
||||
api.framerate_out = function() { return player.frames.framerate_out(); }
|
||||
api.currentBps = function () { return player.frames.bitrate_in(); }
|
||||
api.loop = MistVideo.options.loop;
|
||||
|
||||
//TODO define these if we're adding audio capabilities
|
||||
/*Object.defineProperty(MistVideo.player.api,"volume",{
|
||||
get: function(){ return 0; }
|
||||
});
|
||||
Object.defineProperty(MistVideo.player.api,"muted",{
|
||||
get: function(){ return true; }
|
||||
});*/
|
||||
|
||||
Object.defineProperty(MistVideo.player.api,"webkitDecodedFrameCount",{
|
||||
get: function(){ return player.frames.decoded; }
|
||||
});
|
||||
Object.defineProperty(MistVideo.player.api,"webkitDroppedFrameCount",{
|
||||
get: function(){ return player.frames.dropped; }
|
||||
});
|
||||
|
||||
|
||||
var decoder;
|
||||
this.decoder = null;
|
||||
|
||||
//shorter code to fake an event from the "video" (== canvas) element
|
||||
function emitEvent(type) {
|
||||
//console.log(type);
|
||||
MistUtil.event.send(type,undefined,ele);
|
||||
}
|
||||
|
||||
function init() {
|
||||
|
||||
function init_decoder() {
|
||||
decoder = new libde265.Decoder();
|
||||
MistVideo.player.decoder = decoder;
|
||||
|
||||
var onDecode = [];
|
||||
decoder.addListener = function(func){
|
||||
onDecode.push(func);
|
||||
};
|
||||
decoder.removeListener = function(func){
|
||||
var i = onDecode.indexOf(func);
|
||||
if (i < 0) { return; }
|
||||
onDecode.splice(i,1);
|
||||
return true;
|
||||
};
|
||||
|
||||
//pull requestAnimationFrame-if outside of display_image callback function so it only gets called once
|
||||
var displayImage;
|
||||
if (window.requestAnimationFrame) {
|
||||
displayImage = function(display_image_data){
|
||||
decoder.pending_image_data = display_image_data;
|
||||
window.requestAnimationFrame(function() {
|
||||
if (decoder.pending_image_data) {
|
||||
ctx.putImageData(decoder.pending_image_data, 0, 0);
|
||||
decoder.pending_image_data = null;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
else {
|
||||
displayImage = function(display_image_data){
|
||||
ctx.putImageData(display_image_data, 0, 0);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
decoder.set_image_callback(function(image) {
|
||||
player.frames.decoded++;
|
||||
if (player.vars.wantToPlay && (player.state != "seeking")) {
|
||||
emitEvent("timeupdate");
|
||||
}
|
||||
|
||||
//image.display() wants a starting image, create it if it doesn't exist yet
|
||||
if (!decoder.image_data) {
|
||||
var w = image.get_width();
|
||||
var h = image.get_height();
|
||||
if (w != ele.width || h != ele.height || !this.image_data) {
|
||||
ele.width = w;
|
||||
ele.height = h;
|
||||
|
||||
var img = ctx.createImageData(w, h);
|
||||
decoder.image_data = img;
|
||||
}
|
||||
}
|
||||
|
||||
if (player.state != "seeking") {
|
||||
//when seeking, do not display the new frame if we're not yet at the appropriate timestamp
|
||||
|
||||
image.display(this.image_data, function(display_image_data) {
|
||||
decoder.decoding = false;
|
||||
displayImage(display_image_data);
|
||||
});
|
||||
}
|
||||
image.free();
|
||||
|
||||
//we've decoded and displayed a frame: change player state and emit events if required
|
||||
switch (player.state) {
|
||||
case "play":
|
||||
case "waiting": {
|
||||
if (!player.dropping) {
|
||||
emitEvent("canplay");
|
||||
emitEvent("playing");
|
||||
player.state = "playing";
|
||||
if (!player.vars.wantToPlay) {
|
||||
MistVideo.player.send({type:"hold"});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "seeking": {
|
||||
var t = player.frames.frame2time(player.frames.decoded + player.frames.dropped);
|
||||
if (t >= player.vars.seekTo) {
|
||||
emitEvent("seeked");
|
||||
player.vars.seekTo = null;
|
||||
player.state = "playing";
|
||||
if (!player.vars.wantToPlay) {
|
||||
emitEvent("timeupdate");
|
||||
MistVideo.player.send({type:"hold"});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
player.state = "playing";
|
||||
}
|
||||
}
|
||||
|
||||
//console.log("pending",player.frames.behind());
|
||||
|
||||
for (var i in onDecode) {
|
||||
onDecode[i]();
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
init_decoder();
|
||||
|
||||
/*
|
||||
* infoBytes:
|
||||
* start - length - meaning
|
||||
* 0 1 track index
|
||||
* 1 1 if == 1 ? keyframe : normal frame
|
||||
* 2 8 timestamp (when frame should be sent to decoder) [ms]
|
||||
* 9 2 offset (when frame should be outputted compared to timestamp) [ms]
|
||||
* */
|
||||
function isKeyFrame(infoBytes) {
|
||||
return !!infoBytes[1];
|
||||
}
|
||||
function toTimestamp(infoBytes) { //returns timestamp in ms
|
||||
var v = new DataView(new ArrayBuffer(8));
|
||||
for (var i = 0; i < 8; i++) {
|
||||
v.setUint8(i,infoBytes[i+2]);
|
||||
}
|
||||
//return v.getBigInt64();
|
||||
return v.getInt32(4); //64 bit is an issue in browsers apparently, but we can settle for a 32bit integer that rolls over
|
||||
}
|
||||
|
||||
function connect(){
|
||||
emitEvent("loadstart");
|
||||
|
||||
//?buffer=0 ensures real time sending
|
||||
//?video=hevc,|minbps selects the lowest bitrate hevc track
|
||||
var url = MistUtil.http.url.addParam(MistVideo.source.url,{buffer:0,video:"hevc,|minbps"});
|
||||
|
||||
var socket = new WebSocket(url);
|
||||
MistVideo.player.ws = socket;
|
||||
socket.binaryType = "arraybuffer";
|
||||
function send(obj) {
|
||||
if (!MistVideo.player.ws) { throw "No websocket to send to"; }
|
||||
if (socket.readyState == 1) {
|
||||
socket.send(JSON.stringify(obj));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
MistVideo.player.send = send;
|
||||
socket.wasConnected = false;
|
||||
socket.onopen = function(){
|
||||
if (!MistVideo.player.built) {
|
||||
MistVideo.player.built = true;
|
||||
callback(ele);
|
||||
}
|
||||
send({type:"request_codec_data",supported_codecs:["HEVC"]});
|
||||
socket.wasConnected = true;
|
||||
}
|
||||
socket.onclose = function(){
|
||||
if (this.wasConnected && (!MistVideo.destroyed) && (MistVideo.state == "Stream is online")) {
|
||||
MistVideo.log("Raw over WS: reopening websocket");
|
||||
connect(url);
|
||||
}
|
||||
else {
|
||||
MistVideo.showError("Raw over WS: websocket closed");
|
||||
}
|
||||
}
|
||||
socket.onerror = function(e){
|
||||
MistVideo.showError("Raw over WS: websocket error");
|
||||
};
|
||||
socket.onmessage = function(e){
|
||||
//console.log(new Uint8Array(e.data));
|
||||
if (typeof e.data == "string") {
|
||||
var d = JSON.parse(e.data);
|
||||
switch (d.type) {
|
||||
case "on_time": {
|
||||
//console.log("received",MistUtil.format.time(d.data.current*1e-3));
|
||||
|
||||
player.vars.paused = false;
|
||||
|
||||
player.frames.history.add();
|
||||
|
||||
if (player.vars.duration != d.data.end*1e-3) {
|
||||
player.vars.duration = d.data.end*1e-3;
|
||||
emitEvent("durationchange");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "seek": {
|
||||
//MistVideo.player.decoder.reset(); //should be used when seeking, but makes things worse, honestly
|
||||
MistVideo.player.frames.timestamps = {};
|
||||
if (MistVideo.player.dropping) {
|
||||
MistVideo.log("Emptying drop queue for seek");
|
||||
MistVideo.player.frames.dropped += MistVideo.player.dropping.length;
|
||||
MistVideo.player.dropping = [];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "codec_data": {
|
||||
emitEvent("loadedmetadata");
|
||||
send({type:"play"});
|
||||
player.state = "play";
|
||||
break;
|
||||
}
|
||||
case "info": {
|
||||
var tracks = MistVideo.info.meta.tracks;
|
||||
var track;
|
||||
for (var i in tracks) {
|
||||
if (tracks[i].idx == d.data.tracks[0]) {
|
||||
track = tracks[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ((typeof track != undefined) && (track.fpks > 0)) {
|
||||
player.frames.rate_theoretical = track.fpks*1e-3;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "pause": {
|
||||
player.vars.paused = d.paused;
|
||||
if (d.paused) {
|
||||
player.decoder.flush(); //push last 6 frames through
|
||||
emitEvent("pause");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "on_stop": {
|
||||
if (player.state == "ended") { return; }
|
||||
player.state = "ended";
|
||||
player.vars.paused = true;
|
||||
socket.onclose = function(){}; //don't reopen websocket, just close, it's okay, rly
|
||||
socket.close();
|
||||
player.decoder.flush(); //push last 6 frames through
|
||||
emitEvent("ended");
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
//console.log("ws message",d.type,d.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
player.frames.received++;
|
||||
player.frames.bitsReceived += e.data.byteLength*8;
|
||||
|
||||
var l = 12;
|
||||
var infoBytes = new Uint8Array(e.data.slice(0,l));
|
||||
//console.log(infoBytes);
|
||||
|
||||
var data = new Uint8Array(e.data.slice(l,e.data.byteLength)); //actual raw h265 frame
|
||||
|
||||
player.frames.timestamps[player.frames.received] = toTimestamp(infoBytes);
|
||||
|
||||
function prepare(data,infoBytes) {
|
||||
|
||||
//to avoid clogging the websocket onmessage, process the frame asynchronously
|
||||
setTimeout(function(){
|
||||
|
||||
if (player.dropping) {
|
||||
//console.log(player.frames.behind(),player.dropping.length);
|
||||
if (player.state != "waiting") {
|
||||
emitEvent("waiting");
|
||||
player.state = "waiting";
|
||||
}
|
||||
|
||||
if (isKeyFrame(infoBytes)) {
|
||||
if (player.dropping.length) {
|
||||
player.frames.dropped += player.dropping.length;
|
||||
MistVideo.log("Dropped "+player.dropping.length+" frames");
|
||||
player.dropping = [];
|
||||
}
|
||||
else {
|
||||
MistVideo.log("Caught up! no longer dropping");
|
||||
player.dropping = false;
|
||||
|
||||
}
|
||||
}
|
||||
else {
|
||||
player.dropping.push([infoBytes,data]);
|
||||
if (!decoder.decoding) {
|
||||
var d = player.dropping.shift();
|
||||
MistVideo.player.process(d[1],d[0]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (player.frames.behind() > 20) {
|
||||
//enable dropping
|
||||
player.dropping = [];
|
||||
MistVideo.log("Falling behind, dropping files..");
|
||||
}
|
||||
}
|
||||
|
||||
MistVideo.player.process(data,infoBytes);
|
||||
|
||||
},0);
|
||||
}
|
||||
|
||||
prepare(data,infoBytes);
|
||||
}
|
||||
}
|
||||
|
||||
socket.listeners = {}; //kind of event listener list for websocket messages
|
||||
socket.addListener = function(type,f){
|
||||
if (!(type in this.listeners)) { this.listeners[type] = []; }
|
||||
this.listeners[type].push(f);
|
||||
};
|
||||
socket.removeListener = function(type,f) {
|
||||
if (!(type in this.listeners)) { return; }
|
||||
var i = this.listeners[type].indexOf(f);
|
||||
if (i < 0) { return; }
|
||||
this.listeners[type].splice(i,1);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
MistVideo.player.connect = connect;
|
||||
MistVideo.player.process = function(data,infoBytes){
|
||||
|
||||
//add to the decoding queue
|
||||
decoder.decoding = true;
|
||||
var err = decoder.push_data(data);
|
||||
|
||||
if (player.state == "play") {
|
||||
emitEvent("loadeddata");
|
||||
player.state = "waiting";
|
||||
}
|
||||
|
||||
if (player.vars.wantToPlay && (player.state != "seeking")) {
|
||||
emitEvent("progress");
|
||||
}
|
||||
|
||||
function onerror(err) {
|
||||
if (err == 0) { return; }
|
||||
if (err == libde265.DE265_ERROR_WAITING_FOR_INPUT_DATA) {
|
||||
//emitEvent("waiting");
|
||||
player.state = "waiting";
|
||||
//do nothing, we'll decode again when we get the next data message
|
||||
return;
|
||||
}
|
||||
if (!libde265.de265_isOK(err)) {
|
||||
//console.warn("decode",libde265.de265_get_error_text(err));
|
||||
ele.error = "Decode error: "+libde265.de265_get_error_text(err);
|
||||
emitEvent("error");
|
||||
return true; //don't call decoder.decode();
|
||||
}
|
||||
}
|
||||
|
||||
if (!onerror(err)) {
|
||||
decoder.decode(onerror);
|
||||
}
|
||||
else {
|
||||
decoder.free();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
connect();
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
//redirect properties
|
||||
//using a function to make sure the "item" is in the correct scope
|
||||
function reroute(item) {
|
||||
Object.defineProperty(MistVideo.player.api,item,{
|
||||
get: function(){ return player.vars[item]; },
|
||||
set: function(value){
|
||||
return player.vars[item] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
var list = [
|
||||
"duration"
|
||||
,"paused"
|
||||
,"error"
|
||||
];
|
||||
for (var i in list) {
|
||||
reroute(list[i]);
|
||||
}
|
||||
|
||||
api.play = function(){
|
||||
return new Promise(function(resolve,reject){
|
||||
player.vars.wantToPlay = true;
|
||||
var f = function(){
|
||||
resolve();
|
||||
MistVideo.player.decoder.removeListener(f);
|
||||
};
|
||||
MistVideo.player.decoder.addListener(f);
|
||||
|
||||
if (MistVideo.player.ws.readyState > MistVideo.player.ws.OPEN) {
|
||||
//websocket has closed
|
||||
MistVideo.player.connect();
|
||||
MistVideo.log("Websocket was closed: reconnecting to resume playback");
|
||||
return;
|
||||
}
|
||||
if (api.paused) MistVideo.player.send({type:"play"});
|
||||
player.state = "play";
|
||||
});
|
||||
};
|
||||
api.pause = function(){
|
||||
player.vars.wantToPlay = false;
|
||||
MistVideo.player.send({type:"hold"});
|
||||
};
|
||||
|
||||
MistVideo.player.api.unload = function(){
|
||||
//close socket
|
||||
if (MistVideo.player.ws) {
|
||||
MistVideo.player.ws.onclose = function(){};
|
||||
MistVideo.player.ws.close();
|
||||
}
|
||||
|
||||
if (MistVideo.player.decoder) {
|
||||
//prevent adding of new data
|
||||
MistVideo.player.decoder.push_data = function(){};
|
||||
//free decoder
|
||||
MistVideo.player.decoder.flush();
|
||||
MistVideo.player.decoder.free();
|
||||
}
|
||||
}
|
||||
MistVideo.player.setSize = function(size){
|
||||
ele.style.width = size.width+"px";
|
||||
ele.style.height = size.height+"px";
|
||||
};
|
||||
|
||||
//override seeking
|
||||
Object.defineProperty(MistVideo.player.api,"currentTime",{
|
||||
get: function(){
|
||||
var n = player.frames.decoded + player.frames.dropped;
|
||||
if (player.state == "seeking") { return player.vars.seekTo; }
|
||||
if (n in player.frames.timestamps) { return player.frames.frame2time(n); }
|
||||
return 0;
|
||||
},
|
||||
set: function(value){
|
||||
emitEvent("seeking");
|
||||
player.state = "seeking";
|
||||
player.vars.seekTo = value;
|
||||
MistVideo.player.send({type:"seek", seek_time: value*1e3});
|
||||
//player.frames.timestamps[player.frames.received] = value; //set currentTime to value
|
||||
return value;
|
||||
}
|
||||
});
|
||||
|
||||
//show the difference between decoded frames and received frames as the buffer
|
||||
Object.defineProperty(MistVideo.player.api,"buffered",{
|
||||
get: function(){
|
||||
return {
|
||||
start: function(i){
|
||||
if (this.length && i == 0) {
|
||||
return player.frames.frame2time(player.frames.decoded + player.frames.dropped);
|
||||
}
|
||||
},
|
||||
end: function(i){
|
||||
if (this.length && i == 0) {
|
||||
return player.frames.frame2time(player.frames.received);
|
||||
}
|
||||
},
|
||||
length: player.frames.received - player.frames.decoded > 0 ? 1 : 0
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
//loop
|
||||
if (MistVideo.info.type != "live") {
|
||||
MistUtil.event.addListener(ele,"ended",function(){
|
||||
if (player.api.loop) {
|
||||
player.api.play();
|
||||
player.api.currentTime = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ("libde265" in window) {
|
||||
this.onDecoderLoad();
|
||||
}
|
||||
else {
|
||||
var scripttag = MistUtil.scripts.insert(MistVideo.urlappend(mistplayers.rawws.scriptsrc(MistVideo.options.host)),{
|
||||
onerror: function(e){
|
||||
var msg = "Failed to load H265 decoder";
|
||||
if (e.message) { msg += ": "+e.message; }
|
||||
MistVideo.showError(msg);
|
||||
},
|
||||
onload: MistVideo.player.onDecoderLoad
|
||||
},MistVideo);
|
||||
}
|
||||
}
|
|
@ -18,6 +18,21 @@ mistplayers.videojs = {
|
|||
MistVideo.log("This source ("+mimetype+") won't load if the page is run via file://");
|
||||
return false;
|
||||
}
|
||||
|
||||
var codecs = {};
|
||||
for (var i in MistVideo.info.meta.tracks) {
|
||||
if (MistVideo.info.meta.tracks[i].type != "meta") {
|
||||
codecs[MistVideo.info.meta.tracks[i].codec] = 1;
|
||||
}
|
||||
}
|
||||
codecs = MistUtil.object.keys(codecs);
|
||||
//if there's a h265 track, remove it from the list of codecs
|
||||
for (var i = codecs.length-1; i >= 0; i--) {
|
||||
if (codecs[i].substr(0,4) == "HEVC") {
|
||||
codecs.splice(i,1);
|
||||
}
|
||||
}
|
||||
if (codecs.length < source.simul_tracks) { return false; } //if there's no longer enough playable tracks, skip this player
|
||||
|
||||
return ("MediaSource" in window);
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue