由于H265受版权的影响,浏览器都不愿意支持,所以webrtc不能实现H265的解码播放。
因为很多编码芯片基本上都是支持H264和H265的,VP9和AV1基本上硬件都不支持,大量的显示需要H265来减少流量,特别是4G应用,H265比H264 要少接近一半的流量。为了学习编解码和webrtc以及wasm相关技术,结合网上开源的播放器基础,重新实现了一个基于webrtc的播放器,编码是rv1126 的H265硬编,传输采用webrtc datachannel,信令采用mqtt,播放采用webgl,使用了多worker模式,数据采用指针传递和缓存模式。
浏览器端采用了比较成熟的ffmpeg wasm 技术,感谢开源项目(排名不分先后)https://github.com/goldvideo/decoder_wasm和GitHub - sonysuqin/WasmVideoPlayer: Play file/stream with wasm & webgl & web audio api, using ffmpeg for multi codec support, especially for h265,support http, websocket, http-flv stream.
hhnumberwolf/h265web.js: HEVC/H.265 网页直播/点播播放器 支持H.265的HttpFLV/HLS/MP4/TS/FLV/M3U8/Websocket播放。 A HEVC/H.265 Web Player, for LIVE/VOD stream. Support H.265 Codec with HttpFLV/HLS/MP4/TS/FLV/M3U8/Websocket. (github.com)
https://github.com/langhuihui/jessibuca/tree/v3
这两个项目是我对比了,觉得结构最清晰,仿写最容易的,想学习wasm的也可以从这两个项目开始。以下是核心的解码worker
self.Module = {
onRuntimeInitialized: function () {
onWasmLoaded();
}
};
self.importScripts("common.js");
self.importScripts("libffmpeg_264_265.js");
function Decoder() {
this.logger = new Logger("Decoder");
this.coreLogLevel = 1;
this.accurateSeek = true;
this.wasmLoaded = false;
this.tmpReqQue = [];
this.cacheBuffer = null;
this.decodeTimer = null;
this.videoCallback = null;
this.audioCallback = null;
this.requestCallback = null;
}
var frameBuffer= [];
var pts =0;
Decoder.prototype.initDecoder = function (fileSize, chunkSize) {
var ret=0;
this.logger.logInfo("initDecoder return " + ret + ".");
var objData = {
t: kInitDecoderRsp,
e: ret
};
self.postMessage(objData);
};
Decoder.prototype.uninitDecoder = function () {
this.logger.logInfo("Uninit ffmpeg decoder return " + ret + ".");
if (this.cacheBuffer != null) {
Module._free(this.cacheBuffer);
this.cacheBuffer = null;
}
};
var LOG_LEVEL_JS = 0;
var LOG_LEVEL_WASM = 1;
var LOG_LEVEL_FFMPEG = 2;
var DECODER_H264 = 0;
var DECODER_H265 = 1;
var decoder_type = DECODER_H265;
Decoder.prototype.openDecoder = function () {
var paramCount = 7, paramSize = 4;
var ret = Module._openDecoder(decoder_type, this.videoCallback, LOG_LEVEL_WASM)
this.logger.logInfo("openDecoder return " + ret);
var ret =0;
var objData = {
t: kOpenDecoderRsp,
e: ret,
v: {
d: 0,
p: 1,
w: 1289,
h: 720
},
};
self.postMessage(objData);
};
Decoder.prototype.closeDecoder = function () {
this.logger.logInfo("closeDecoder.");
if (this.decodeTimer) {
clearInterval(this.decodeTimer);
this.decodeTimer = null;
this.logger.logInfo("Decode timer stopped.");
}
this.logger.logInfo("Close ffmpeg decoder return " + ret + ".");
var objData = {
t: kCloseDecoderRsp,
e: 0
};
self.postMessage(objData);
};
Decoder.prototype.startDecoding = function (interval) {
this.logger.logInfo("Start decoding.");
if (this.decodeTimer) {
clearInterval(this.decodeTimer);
}
this.decodeTimer = setInterval(this.decode, interval);
};
Decoder.prototype.pauseDecoding = function () {
this.logger.logInfo("Pause decoding.");
if (this.decodeTimer) {
clearInterval(this.decodeTimer);
this.decodeTimer = null;
}
};
Decoder.prototype.getBufferTimerLength = function() {
if (!frameBuffer || frameBuffer.length == 0) {
return 0;
}
let oldest = frameBuffer[0];
let newest = frameBuffer[frameBuffer.length - 1];
return newest.s - oldest.s;
};
Decoder.prototype.decode = function () {
console.log("Decoding")
if(frameBuffer.length>0){
var typedArray = frameBuffer.shift();//frameBuffer[0];
var size = typedArray.length;
this.cacheBuffer = Module._malloc(size);
Module.HEAPU8.set(typedArray, this.cacheBuffer);
Module._decodeData(this.cacheBuffer, size, pts++)
if (this.cacheBuffer != null) {
Module._free(this.cacheBuffer);
this.cacheBuffer = null;
}
}
};
Decoder.prototype.sendData = function (data) {
var typedArray = new Uint8Array(data);
frameBuffer.push(typedArray);
};
Decoder.prototype.seekTo = function (ms) {
var accurateSeek = this.accurateSeek ? 1 : 0;
var ret = Module._seekTo(ms, accurateSeek);
var objData = {
t: kSeekToRsp,
r: ret
};
self.postMessage(objData);
};
Decoder.prototype.processReq = function (req) {
//this.logger.logInfo("processReq " + req.t + ".");
switch (req.t) {
case kInitDecoderReq:
this.initDecoder(req.s, req.c);
break;
case kUninitDecoderReq:
this.uninitDecoder();
break;
case kOpenDecoderReq:
this.openDecoder();
break;
case kCloseDecoderReq:
this.closeDecoder();
break;
case kStartDecodingReq:
this.startDecoding(req.i);
break;
case kPauseDecodingReq:
this.pauseDecoding();
break;
case kFeedDataReq:
this.sendData(req.d);
break;
case kSeekToReq:
this.seekTo(req.ms);
break;
default:
this.logger.logError("Unsupport messsage " + req.t);
}
};
Decoder.prototype.cacheReq = function (req) {
if (req) {
this.tmpReqQue.push(req);
}
};
Decoder.prototype.onWasmLoaded = function () {
this.logger.logInfo("Wasm loaded.");
this.wasmLoaded = true;
var videoSize = 0;
this.videoCallback = Module.addFunction(function (addr_y, addr_u, addr_v, stride_y, stride_u, stride_v, width, height, pts) {
console.log("[%d]In video callback, size = %d * %d, pts = %d", ++videoSize, width, height, pts)
let size = width * height + (width / 2) * (height / 2) + (width / 2) * (height / 2)
let data = new Uint8Array(size)
let pos = 0
for(let i=0; i< height; i++) {
let src = addr_y + i * stride_y
let tmp = HEAPU8.subarray(src, src + width)
tmp = new Uint8Array(tmp)
data.set(tmp, pos)
pos += tmp.length
}
for(let i=0; i< height / 2; i++) {
let src = addr_u + i * stride_u
let tmp = HEAPU8.subarray(src, src + width / 2)
tmp = new Uint8Array(tmp)
data.set(tmp, pos)
pos += tmp.length
}
for(let i=0; i< height / 2; i++) {
let src = addr_v + i * stride_v
let tmp = HEAPU8.subarray(src, src + width / 2)
tmp = new Uint8Array(tmp)
data.set(tmp, pos)
pos += tmp.length
}
var duration = pts;//paramArray[0];
var videoPixFmt = 1;//paramArray[1];
var videoWidth = width;//paramArray[2];
var videoHeight = height;//paramArray[3];
var ret =0;
var objData = {
t: kVideoParameters,
e: ret,
v: {
d: duration,
p: videoPixFmt,
w: videoWidth,
h: videoHeight
},
};
self.postMessage(objData);
var objData = {
t: kVideoFrame,
s: pts,
d: data
};
self.postMessage(objData, [objData.d.buffer]);
// displayVideoFrame(obj);
});
this.requestCallback = Module.addFunction(function (offset, availble) {
var objData = {
t: kRequestDataEvt,
o: offset,
a: availble
};
self.postMessage(objData);
}, 'vii');
while (this.tmpReqQue.length > 0) {
var req = this.tmpReqQue.shift();
this.processReq(req);
}
};
self.decoder = new Decoder;
self.onmessage = function (evt) {
if (!self.decoder) {
console.log("[ER] Decoder not initialized!");
return;
}
var req = evt.data;
if (!self.decoder.wasmLoaded) {
self.decoder.cacheReq(req);
self.decoder.logger.logInfo("Temp cache req " + req.t + ".");
return;
}
self.decoder.processReq(req);
};
function onWasmLoaded() {
if (self.decoder) {
self.decoder.onWasmLoaded();
} else {
console.log("[ER] No decoder!");
}
}
开源地址如下https://github.com/xiangxud/webrtc_H265player