Хочу реализовать кросс-браузерный веб-плеер для своей системы видео стриминга, в которой клиент отправляет серверу сырые VP8 кадры, а сервер отдаёт их браузеру по WebSocket-ам при подписке на соответствующее событие.
Для выполнения задачи нашёл в сети реализацию vpx_decoder на JS (dixie.js), которая, судя по отзывам в сети, у людей работает, но у меня почему-то изображение искажённое. Ниже приведу код, в котором получаемый с сервера видео-контент направляется на рендер как через этот скрипт, так и через свою обёртку над HTML5 video (для сравнения):
<script>
// For canvas redner...
let decoder = new vp8_decoder_ctx();
let got_key = false;
let cnv = document.createElement("canvas");
cnv.width = 1280;
cnv.height = 532;
document.body.appendChild(cnv);
let ctx = cnv.getContext('2d');
var imgData = ctx.createImageData(cnv.width, cnv.height);
// Image format conversion and support functions
function clapm_byte(value) { return(value>255) ? 255 : ((value<0)?0:value); }
function R(y, u, v) { return clapm_byte( (298 * y + 409 * v + 128) >> 8); }
function G(y, u, v) { return clapm_byte( (298 * y - 100 * u - 208 * v + 128) >> 8); }
function B(y, u, v) { return clapm_byte( (298 * y + 516 * u + 128) >> 8); }
function I420toBGR(width, height, stride, y, u, v, out){
let shift = -1;
let half_stride = stride/2;
let p_y, p_u, p_v;
let bgr = out.data;
for(let h = 0; h < height; ++h){
for(let w = 0; w < width; ++w){
p_y = y[h * stride + w ] - 16;
p_u = u[h/2 * half_stride + w/2] - 128;
p_v = v[h/2 * half_stride + w/2] - 128;
bgr[++shift] = B(p_y, p_u, p_v);
bgr[++shift] = G(p_y, p_u, p_v);
bgr[++shift] = R(p_y, p_u, p_v);
}
}
ctx.putImageData(out, 0, 0);
}
// wrapper for stream object (for render in video tag)
class MediaIO{
constructor(){
this.id = null;
this.key = null;
this.media = null;
this.muxer = null;
}
Match(data){
if (!this.id) return false;
if (this.id.length !== data.length) return false;
for(let i = 0; i < data.length; ++i) if (data[i] !== this.id.charCodeAt(i)) return false;
return true;
}
};
// WebSocket packet handling procedure
function HandlePacket(data, pool){
let cxpf = new cxp_frame; // custom protocol to exchange with stream server
let opcode = cxpf.parse(new Uint8Array(data));
const stream = (cxp_stream | cxp_user | cxp_time | cxp_data);
for(let i = 0; i < pool.length; ++i){
let object = pool[i];
if (!object.Match(cxpf.user)) continue;
switch(opcode){
case stream | object.key:{
let is_key_frame = (0x8000 & cxpf.time) > 0;
// cxpf.data - is a raw VP8 frame produced on С++ by calling vpx_codec_encode and vpx_codec_get_cx_data from libvpx
// Decoding in HTML5 video tag with manual EBML markup
object.muxer.Push(cxpf.data, is_key_frame);
// Manual decoding raw VP8 using dixie.js to draw on canvas
if (!got_key){
if (is_key_frame) got_key = true;
else break;
}
if (VPX_CODEC_OK === vp8_dixie_decode_frame(decoder, cxpf.data, cxpf.data.length)){
let img = decoder.ref_frames[0].img;
I420toBGR(img.d_w, img.d_h, img.stride[0], img.planes[0], img.planes[1], img.planes[2], imgData);
}
break;
}
case cxp_user:{
if (object.muxer) object.muxer.Reset();
console.log("User",cxpf.user, "disconnectd from stream server");
break;
}
case cxp_user | object.key:{
object.muxer = new Stream(object.media.device); // Custom EBML muxer
console.log("User",cxpf.user,"key",object.key, "registered at stream server");
break;
}
default: break;
}
}
}
let user = new cxp_frame;
user.load(cxp_user,atob("<?php echo $stream_access;?>"));
let screen = new MediaIO;
screen.id = "<?php echo $user_id;?>";
screen.key = cxp_screen;
screen.media = new Display(1280, 720, 0, document.body, true); // Custom wrapper for video tag
let Sources = Array();
Sources.push(screen);
let socket = new Network( // custom WebSocket wrapper
"localhost",
778,
function(event) { HandlePacket(event.data, Sources); }
);
let option = 0;
for(let i = 0; i < Sources.length; ++i) option |= Sources[i].key;
socket.Send(user.pack(option));
</script>
Выглядит это в браузере так (вверху рендер на Canvas через dixie.js, внизу тэг video):

То есть контент валидный, и HTML5 video его выводит нормально. К сожалению реализация через HTML5 video не будет бросс-браузерной - в тот же Safari, например, VP8 не зашит и работать не будет. Поэтому пытаюсь разобраться с ручной реализацией декодера. Код перевода из I420 в BGR / RGB взял из проекта на C++, где он уже отлажен и давал корректные изображения.
Может кто уже пытался сделать подобное, подскажите пожалуйста что не так с ручным декодером или с его использованием в моём коде?
for(let h = 0; h < height; ++h){for(let w = 0; w < width; ++w){, их там должно быть 4 -rgba, запишите в четвертый всегда 255 – Stranger in the Q Nov 28 '19 at 14:09bgr[++shift] = 255;в конце конверсии, подгрузил его в скрипт через PHP и попробовал разложить это изоброажение в RGBA - отрисовало на canvas корректно! Спасибо за подсказку! VP8 после декодирования всё ещё искажено, но тут уже буду разбираться с тем, что даёт декодер на выходе – Iceman Nov 28 '19 at 14:32