上篇讲到采用ffmpeg wasm实现了H265解码的核心功能,这篇主要讲播放器的控制和实现
整个播放器开启两个worker,playerworker 和decoderworker,并通过消息进行worker间通信,可以理解成js下的多线程,很多UI和webrtc的api在worker里是没法使用的,所以在写的时候一定注意这个坑。播放器入口函数handleVideo()建立了 player worker 在player里面再建立decoderworker,同时启动webrtc信令交互建立p2p连接,打开H265data 传输的dc通道
var webglPlayer, canvas, videoWidth, videoHeight, yLength, uvLength;
var player=null;
function handleVideo() {
player = new Worker("Player.js");
startDeviceSession(player);
player.onmessage = function (evt){
var objData = evt.data;
switch (objData.t) {
case kplayeVideoFrame:
webgldisplayVideoFrame(objData.d);
break;
default:
break;
}
}
var req = {
t: kstartPlayerCoderReq,
};
player.postMessage(req);
}
function stopVideo(){
endWebrtc();
endMqtt();
}
var webt1=new Date().getTime();
function webgldisplayVideoFrame(obj) {
var data = new Uint8Array(obj.data);
var width = obj.width;
var height = obj.height;
var yLength = width * height;
var uvLength = (width / 2) * (height / 2);
if(!webglPlayer) {
const canvasId = "playCanvas";
canvas = document.getElementById(canvasId);
webglPlayer = new WebGLPlayer(canvas, {
preserveDrawingBuffer: false
});
}
const t2=new Date().getTime()-webt1;
console.log("display time:"+t2+" width:"+width+" height"+height+" yLength"+yLength+" uvLength"+uvLength);
webglPlayer.renderFrame(data, width, height, yLength, uvLength);
webt1 = new Date().getTime()
}
self.Module = {
onRuntimeInitialized: function () {
}
};
var webglPlayer, canvas, videoWidth, videoHeight, yLength, uvLength;
var LOG_LEVEL_JS = 0;
var LOG_LEVEL_WASM = 1;
var LOG_LEVEL_FFMPEG = 2;
var DECODER_H264 = 0;
var DECODER_H265 = 1;
self.importScripts("common.js");
function Player(){
this.decoder_type = DECODER_H265;
this.decoding = false;
this.webrtcplayerState = playerStateIdle;
this.decodeInterval = 5;
this.urgent = false;
this.frameBuffer = [];
}
Player.prototype.setDecoder=function(type) {
this.decoder_type = type;
}
Player.prototype.startDecoder=function(){
var self = this;
this.decoderworker=new Worker("decoder.js");
// this.decoderworker.initDecoder();
var req = {
t: kInitDecoderReq,
};
this.decoderworker.postMessage(req);
this.decoderworker.onmessage=function (evt) {
var objData = evt.data;
switch (objData.t) {
case kInitDecoderRsp:
self.onInitDecoder(objData);
break;
case kOpenDecoderRsp:
self.onOpenDecoder(objData);
break;
case kVideoFrame:
self.onVideoFrame(objData);
break;
}
}
};
Player.prototype.startDecoding = function () {
var req = {
t: kStartDecodingReq,
i: this.urgent ? 0 : this.decodeInterval,
};
this.decoderworker.postMessage(req);
this.decoding = true;
this.webrtcplayerState = playerStatePlaying;
};
Player.prototype.pauseDecoding = function () {
var req = {
t: kPauseDecodingReq
};
this.decoderworker.postMessage(req);
this.decoding = false;
};
Player.prototype.onOpenDecoder=function(objData){
console.log("reqOpenDecoder response " + objData.e + ".");
if(objData.e==0){
this.startDecoding();
}
}
Player.prototype.onInitDecoder = function (objData) {
console.log("Init decoder response " + objData.e + ".");
this.reqOpenDecoder();
};
Player.prototype.reqOpenDecoder=function(){
var req = {
t: kOpenDecoderReq
};
this.decoderworker.postMessage(req);
}
Player.prototype.onFrameData = function (data, len) {
// console.log("Got data bytes=" + start + "-" + end + ".");
var objData = {
t: kFeedDataReq,
d: data
};
this.decoderworker.postMessage(objData, [objData.d]);
}
Player.prototype.startBuffering = function () {
this.buffering = true;
this.showLoading();
this.pause();
}
Player.prototype.stopBuffering = function () {
this.buffering = false;
this.hideLoading();
this.resume();
}
Player.prototype.displayVideoFrame = function (frame) {
var audioTimestamp=0;
var delay=0
// console.log("displayVideoFrame delay=" + delay + "=" + " " + frame.s + " - (" + audioCurTs + " + " + this.beginTimeOffset + ")" + "->" + audioTimestamp);
if (audioTimestamp <= 0 || delay <= 0) {
this.renderVideoFrame(frame.d);
return true;
}
return false;
};
Player.prototype.displayLoop = function() {
if (this.webrtcplayerState !== playerStateIdle) {
requestAnimationFrame(this.displayLoop.bind(this));
}
if(this.frameBuffer.length>0){
var frame = this.frameBuffer[0];
if (this.displayVideoFrame(frame)) {
this.frameBuffer.shift();
}
}
};
Player.prototype.renderVideoFrame = function (data) {
// var self = this;
var playFrame={
t: kplayeVideoFrame,
d: data
}
self.postMessage(playFrame,[playFrame.d.data.buffer])
};
Player.prototype.getBufferTimerLength = function() {
if (!this.frameBuffer || this.frameBuffer.length == 0) {
return 0;
}
let oldest = this.frameBuffer[0];
let newest = this.frameBuffer[this.frameBuffer.length - 1];
return newest.s - oldest.s;
};
Player.prototype.bufferFrame = function (frame) {
// If not decoding, it may be frame before seeking, should be discarded.
this.frameBuffer.push(frame);
console.log("bufferFrame pts:" + frame.s+" w:" + frame.d.width + ", h: " + frame.d.height);
}
Player.prototype.onVideoFrame = function (frame) {
this.bufferFrame(frame);
};
Player.prototype.processReq = function (req) {
// console.log("processReq " + req.t + ".");
switch (req.t) {
case kstartPlayerCoderReq:
this.startDecoder();
break;
case kInitPlayerReq:
this.displayLoop();
break;
case ksendPlayerVideoFrameReq:
this.onFrameData(req.d,req.l);
break;
default:
this.logger.logError("Unsupport messsage " + req.t);
}
};
self.player = new Player;
self.onmessage = function (evt) {
if (!self.player) {
console.log("[ER] Player not initialized!");
return;
}
var req = evt.data;
self.player.processReq(req);
};