我可以使用 readPixels 从画布上捕获帧(https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/readPixels),但是然后呢?
我认为 ffmpeg 有一些 wasm 版本,但它们很慢。
我找到了这个 Web Assembly mp4 编码器,但它不再受支持,并且无法在我的手机 (Android 13) 上的 Chrome 中工作。
还有其他建议吗?
“在 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>