在网络浏览器中使用 Javascript 对 webgl 画布中的帧的 mp4 视频进行编码的最高效方法是什么?

问题描述 投票:0回答:1

我可以使用 readPixels 从画布上捕获帧(https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/readPixels),但是然后呢?

我认为 ffmpeg 有一些 wasm 版本,但它们很慢。

我找到了这个 Web Assembly mp4 编码器,但它不再受支持,并且无法在我的手机 (Android 13) 上的 Chrome 中工作。

还有其他建议吗?

javascript ffmpeg html5-canvas mp4
1个回答
0
投票

“在 Web 浏览器中使用 Javascript 对 webgl 画布中的帧的 mp4 视频进行编码的最高效方法是什么?”

最高效的方法是使用 WebCodecs API。它是 Chrome 浏览器的内置功能(并且是 Android 上支持的功能)。

MP4 是音频和视频帧的容器。您想要首先以压缩视频格式(例如:H.264)对像素进行编码,然后使用桌面媒体播放器(例如:VLC)播放该 H.264 文件,或者如果您需要在浏览器中显示它,那么它必须混合到 MP4 容器中...

如果创建您自己的复用器,那么您需要查找 MP4 规范(MOV 规范也可用,因为两种格式与相同的 ISO 规范相似)。

MP4 主要是整数,因此您可以创建一个(普通)数组,用 MP4 的有效值填充它,然后添加编码帧以获得可播放的 MP4 文件。

//# write 32 bits in BIG Endian format to a position with an Array
//# params: ( target Array, write position Offset, Value to write )
function write_uint32_BE ( in_Array, in_Offset, in_val )
{
    in_Array[ in_Offset + 0 ] = (in_val & 0xFF000000) >>> 24;
    in_Array[ in_Offset + 1 ] = (in_val & 0x00FF0000) >>> 16;
    in_Array[ in_Offset + 2 ] = (in_val & 0x0000FF00) >>> 8;
    in_Array[ in_Offset + 3 ] = (in_val & 0x000000FF);

}

因此,要创建 MP4 数据的不同原子(或盒子),您必须执行以下操作:

这些示例字节用于

stss
框(告诉哪些帧编号是关键帧):

00 00 00 14 73 74 73 73 00 00 00 00 00 00 00 01 00 00 00 01 00 00 00 B2

其中每 4 个字节表示:

00 00 00 18
= 大小 0x18 == 十进制 24
73 74 73 73
= 文本“stss”
00 00 00 00
=版本等
00 00 00 01
= 关键帧条目总数
00 00 00 01
= 条目 1 == 第 1 帧
00 00 00 08
= 条目 2 == 第 8 帧

我们可以看到第1帧和第8帧都是关键帧。 MP4解码器到目前为止还不会崩溃。

要通过代码创建上述字节,您需要运行“write 32-bit uint”函数 6 次:

write_uint32_BE ( myMP4_bufferArray, (someOffsetposition + 0), 0x00000018 );
write_uint32_BE ( myMP4_bufferArray, (someOffsetposition + 4), 0x73747373 );
write_uint32_BE ( myMP4_bufferArray, (someOffsetposition + 8), 0x00000000 );
write_uint32_BE ( myMP4_bufferArray, (someOffsetposition + 12), 0x00000002 );
write_uint32_BE ( myMP4_bufferArray, (someOffsetposition + 16), 0x00000001 );
write_uint32_BE ( myMP4_bufferArray, (someOffsetposition + 16), 0x00000008 );

创建 MP4 的字节是另一个话题。您首先需要能够对帧进行编码。

尝试下面的代码来编码测试视频帧...
它只会编码为 H264(压缩图片)格式。您将需要编写更多代码来创建 MP4(甚至 AVI、FLV、MOV、MKV 等)等容器格式的字节。这称为多路复用,您可以尝试使用 Mux.js、MP4Box.js 等库,或者编写自己的多路复用代码以将音频/视频帧放入一个容器中。

<!DOCTYPE html>
<html>
<body>

<canvas id="myCanvas" width="320" height="240" style="" >

</canvas>

<script>

var myCanvas = document.getElementById("myCanvas");

//## Encoder vars
var fileName;
var is_keyFrame = true;

var myConfigBytes;
var encoded_chunkData;

var frameFromCanvas;

////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////

//# Phase (1)
//# Setup an Encoder ...

const encoder_init = {
    output: handleChunk,
    error: (e) => { console.log("Encode error : " + e.message) },
};

const encoder_config = {
    
    //# set codec
    codec: "avc1.42C01E", //# works 420 Baseline profile
    
    //# set AVC format to "annexb" to get START CODES added automatically
    avc: { format: 'annexb' },
    
    //# best to set when decoding, not when encoding
    //hardwareAcceleration: 'prefer-hardware',
     
    width: 320,
    height: 240,
    
    framerate: 30,
    
    latencyMode: "realtime",
    //latencyMode: "quality", //# (default)
    
    //bitrateMode: "constant",
    bitrateMode: "variable",
    
    //# if you want to set it manually
    //bitrate: 500_000, //# test 500 kbps
    //bitrate: 2_000_000, //# test 2 Mbps
    
};

const encoder = new VideoEncoder( encoder_init );
encoder.configure( encoder_config );
console.log(">>> Encoder is now ready");

////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////

//# Phase (2)
//# Create Image data ...

var vid_width = 320; var vid_height = 240;

myCanvas.setAttribute( "width", vid_width );
myCanvas.setAttribute( "height", vid_height );

var ctx_frame = myCanvas.getContext("2d");
var ctx_frame_imageData = ctx_frame.createImageData( vid_width, vid_height );

//# //# fill canvas with some R-G-B values at each pixel component
for (let i = 0; i < ctx_frame_imageData.data.length; i += 4) 
{
    //# Modify pixel data (blue square on main canvas)
    ctx_frame_imageData.data[i + 0] = 200; //# R value
    ctx_frame_imageData.data[i + 1] = 190; //# G value
    ctx_frame_imageData.data[i + 2] = 90;  //# B value

    ctx_frame_imageData.data[i + 3] = 255;  // Alpha value

}

//# Draw image data to the canvas
ctx_frame.putImageData(ctx_frame_imageData, 0, 0);


////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////

//# Phase (3)
//# Encode the Image data as a Video Frame ...

frameFromCanvas = new VideoFrame( myCanvas, { timestamp: 0 });
encode_frame( frameFromCanvas );

function encode_frame ( frame )
{
    if (encoder.encodeQueueSize > 2) 
    {
        //# drop this frame (since 2 is too many frames in 1 queue)
        frame.close();
    } 
    else 
    {
        is_keyFrame = true; 
        
        //# encode as "keyframe" (or false == a "delta" frame)
        encoder.encode(frame, { keyFrame: is_keyFrame } );
    }
}

let tmp_obj_metadata;

//# handle chunks (eg: could mux frames into fragmented MP4 container)
async function handleChunk( encoded_frame_data, metadata) 
{
    if (metadata.decoderConfig) 
    {
        //# save the decoder description (is SPS and PPS for AVC / MP4 )
        myConfigBytes = new Uint8Array(  metadata.decoderConfig.description );
    }

    //# get actual bytes of encoded data
    encoded_chunkData = new Uint8Array( encoded_frame_data.byteLength );
    encoded_frame_data.copyTo( encoded_chunkData );

    //# end the encoding session (eg: if no more frames are expected)
    let temp = await encoder.flush();
    
    //# test save of encoded bytes
    myFile_name = "vc_test_encode_webcodecs_05.h264"
    
    //saveArrayToFile( encoded_chunkData, myFile_name )
    
    return temp;
}

////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////

//# Phase (4)
//# Save H.264  as file...

function saveArrayToFile( in_Array, in_fileName ) 
{
    let temp_out_bytes = [...myConfigBytes, ...in_Array]
    
    let myBlob = new Blob(  
                            [ Uint8Array.from( temp_out_bytes ) ] , 
                            {type: "application/octet-stream"} 
                        );
                        
    let myBlob_url = window.URL.createObjectURL( myBlob );
    let tmpOutFile = document.createElement("a");

    tmpOutFile.href = myBlob_url;
    tmpOutFile.download = in_fileName;
    tmpOutFile.click();
    window.URL.revokeObjectURL( myBlob_url );
}

</script>

</body>
</html>
© www.soinside.com 2019 - 2024. All rights reserved.