如何将 h264 编码的 MediaRecorder 流传递到 Chrome 中的 MediaSource?
How to pass a h264 encoded MediaRecorder stream to a MediaSource in Chrome?
我们的屏幕录制 chrome 扩展允许用户使用 getDisplayMedia API, which returns a stream that is fed into the MediaRecorder API.
来录制他们的屏幕
通常,我们会使用带有较新的 vp9 编解码器的 webm 视频容器来录制此流,如下所示:
const mediaRecorder = new MediaRecorder(mediaStream, {
mimeType: "video/webm; codecs=vp9"
});
但是Safari不支持webm容器,也不支持解码vp9编解码器。由于 Chrome 中的 MediaRecorder API 仅支持在 webm 容器中录制,但支持 h264 编码(Safari 可以解码),我们改为在 webm 容器中使用 h264 编解码器录制:
const mediaRecorder = new MediaRecorder(mediaStream, {
mimeType: "video/webm; codecs=h264"
});
这很有效有两个原因:
由于我们的录音应用是chrome扩展,我们不介意它只能在Chrome
中录音
由于视频数据被编码为 h264,我们现在几乎可以立即将视频数据移动到 .mp4 容器中,从而允许 Safari 浏览器查看这些录制的视频,而无需等待昂贵的转码处理(请注意,您可以在常规网络应用程序中观看没有 chrome 扩展名的视频)
但是,由于媒体记录器 API 没有办法获取到目前为止录制的视频流的持续时间,并且用 performance.now
手动测量它被证明是不精确的(25 毫秒到150 毫秒错误),我们不得不更改为将记录器数据馈送到 MediaSource,这样我们就可以使用 mediaSourceBuffer.buffered.end(sourceBuffer.buffered.length - 1) * 1000
API 来 100% 准确读取到目前为止记录的视频流持续时间(以毫秒为单位) ).
问题在于,由于某种原因,当我们使用“video/webm; codecs=h264”mime 类型时,MediaSource 无法实例化。
这样做:
mediaSourceBuffer = mediaSource.addSourceBuffer("video/webm; codecs=h264");
结果:
Failed to execute 'addSourceBuffer' on 'MediaSource': The type provided ('video/webm; codecs=h264') is unsupported.
为什么 mime 类型是 MediaRecorder 支持的,而 MediaSource 不支持?由于它们属于同一个 API 系列,它们不应该支持相同的 mime 类型吗?我们如何在使用 addSourceBuffer 将数据传递到 MediaSource 的同时使用 h264 编解码器进行录制?
到目前为止我们能想到的唯一解决方案是创建 2 个媒体记录器,一个在 vp9 中记录,以便我们使用 buffered.end
API 读取到目前为止录制的视频的准确持续时间,以及一个 h264 格式的录制,让我们能够立即将视频数据移动到 mp4 容器,而无需为 Safari 用户将编解码器从 vp9 转码为 h264。但是,这将非常低效,因为它实际上会在 RAM 中保存两倍的数据。
复制案例/codesandbox 示例
- vp9 example(均有效)
- h264 example(媒体记录器工作,媒体源不工作)
解码器和编码器完全不同。例如 Webkit (Safari) 可以解码一些格式,但它不能编码任何东西。
此外,MediaSource API 要求传递给它的媒体可以是碎片化的,因此无法读取浏览器可以解码的所有媒体,例如,如果某个浏览器有一天支持生成标准 (非碎片)mp4 文件,那么他们仍然无法将其传递给 MediaSource API.
我不能确定他们是否可以支持这个特定的编解码器(我想是的),但你甚至可能根本不需要所有这些解决方法。
如果您的扩展程序能够生成 DOM 元素,那么您可以简单地使用 <video>
元素来告诉您录制视频的持续时间,使用 中描述的技巧:
将视频的currentTime
设置为非常大的数字,等待seeked
事件,你会得到正确的duration
。
const canvas_stream = getCanvasStream();
const rec = new MediaRecorder( canvas_stream.stream );
const chunks = [];
rec.ondataavailable = (evt) => chunks.push( evt.data );
rec.onstop = async (evt) => {
canvas_stream.stop();
console.log( "duration:", await measureDuration( chunks ) );
};
rec.start();
setTimeout( () => rec.stop(), 5000 );
console.log( 'Recording 5s' );
function measureDuration( chunks ) {
const blob = new Blob( chunks, { type: "video/webm" } );
const vid = document.createElement( 'video' );
return new Promise( (res, rej) => {
vid.onerror = rej;
vid.onseeked = (evt) => res( vid.duration );
vid.onloadedmetadata = (evt) => {
URL.revokeObjectURL( vid.src );
// for demo only, to show it's Infinity in Chrome
console.log( 'before seek', vid.duration );
};
vid.src = URL.createObjectURL( blob );
vid.currentTime = 1e10;
} );
}
// just so we can have a MediaStream in StackSnippet
function getCanvasStream() {
const canvas = document.createElement( 'canvas' );
const ctx = canvas.getContext( '2d' );
let stopped = false;
function draw() {
ctx.fillRect( 0,0,1,1 );
if( !stopped ) {
requestAnimationFrame( draw );
}
}
draw();
return {
stream: canvas.captureStream(),
stop: () => stopped = true
};
}
我们的屏幕录制 chrome 扩展允许用户使用 getDisplayMedia API, which returns a stream that is fed into the MediaRecorder API.
来录制他们的屏幕通常,我们会使用带有较新的 vp9 编解码器的 webm 视频容器来录制此流,如下所示:
const mediaRecorder = new MediaRecorder(mediaStream, {
mimeType: "video/webm; codecs=vp9"
});
但是Safari不支持webm容器,也不支持解码vp9编解码器。由于 Chrome 中的 MediaRecorder API 仅支持在 webm 容器中录制,但支持 h264 编码(Safari 可以解码),我们改为在 webm 容器中使用 h264 编解码器录制:
const mediaRecorder = new MediaRecorder(mediaStream, {
mimeType: "video/webm; codecs=h264"
});
这很有效有两个原因:
由于我们的录音应用是chrome扩展,我们不介意它只能在Chrome
中录音由于视频数据被编码为 h264,我们现在几乎可以立即将视频数据移动到 .mp4 容器中,从而允许 Safari 浏览器查看这些录制的视频,而无需等待昂贵的转码处理(请注意,您可以在常规网络应用程序中观看没有 chrome 扩展名的视频)
但是,由于媒体记录器 API 没有办法获取到目前为止录制的视频流的持续时间,并且用 performance.now
手动测量它被证明是不精确的(25 毫秒到150 毫秒错误),我们不得不更改为将记录器数据馈送到 MediaSource,这样我们就可以使用 mediaSourceBuffer.buffered.end(sourceBuffer.buffered.length - 1) * 1000
API 来 100% 准确读取到目前为止记录的视频流持续时间(以毫秒为单位) ).
问题在于,由于某种原因,当我们使用“video/webm; codecs=h264”mime 类型时,MediaSource 无法实例化。
这样做:
mediaSourceBuffer = mediaSource.addSourceBuffer("video/webm; codecs=h264");
结果:
Failed to execute 'addSourceBuffer' on 'MediaSource': The type provided ('video/webm; codecs=h264') is unsupported.
为什么 mime 类型是 MediaRecorder 支持的,而 MediaSource 不支持?由于它们属于同一个 API 系列,它们不应该支持相同的 mime 类型吗?我们如何在使用 addSourceBuffer 将数据传递到 MediaSource 的同时使用 h264 编解码器进行录制?
到目前为止我们能想到的唯一解决方案是创建 2 个媒体记录器,一个在 vp9 中记录,以便我们使用 buffered.end
API 读取到目前为止录制的视频的准确持续时间,以及一个 h264 格式的录制,让我们能够立即将视频数据移动到 mp4 容器,而无需为 Safari 用户将编解码器从 vp9 转码为 h264。但是,这将非常低效,因为它实际上会在 RAM 中保存两倍的数据。
复制案例/codesandbox 示例
- vp9 example(均有效)
- h264 example(媒体记录器工作,媒体源不工作)
解码器和编码器完全不同。例如 Webkit (Safari) 可以解码一些格式,但它不能编码任何东西。
此外,MediaSource API 要求传递给它的媒体可以是碎片化的,因此无法读取浏览器可以解码的所有媒体,例如,如果某个浏览器有一天支持生成标准 (非碎片)mp4 文件,那么他们仍然无法将其传递给 MediaSource API.
我不能确定他们是否可以支持这个特定的编解码器(我想是的),但你甚至可能根本不需要所有这些解决方法。
如果您的扩展程序能够生成 DOM 元素,那么您可以简单地使用 <video>
元素来告诉您录制视频的持续时间,使用
将视频的currentTime
设置为非常大的数字,等待seeked
事件,你会得到正确的duration
。
const canvas_stream = getCanvasStream();
const rec = new MediaRecorder( canvas_stream.stream );
const chunks = [];
rec.ondataavailable = (evt) => chunks.push( evt.data );
rec.onstop = async (evt) => {
canvas_stream.stop();
console.log( "duration:", await measureDuration( chunks ) );
};
rec.start();
setTimeout( () => rec.stop(), 5000 );
console.log( 'Recording 5s' );
function measureDuration( chunks ) {
const blob = new Blob( chunks, { type: "video/webm" } );
const vid = document.createElement( 'video' );
return new Promise( (res, rej) => {
vid.onerror = rej;
vid.onseeked = (evt) => res( vid.duration );
vid.onloadedmetadata = (evt) => {
URL.revokeObjectURL( vid.src );
// for demo only, to show it's Infinity in Chrome
console.log( 'before seek', vid.duration );
};
vid.src = URL.createObjectURL( blob );
vid.currentTime = 1e10;
} );
}
// just so we can have a MediaStream in StackSnippet
function getCanvasStream() {
const canvas = document.createElement( 'canvas' );
const ctx = canvas.getContext( '2d' );
let stopped = false;
function draw() {
ctx.fillRect( 0,0,1,1 );
if( !stopped ) {
requestAnimationFrame( draw );
}
}
draw();
return {
stream: canvas.captureStream(),
stop: () => stopped = true
};
}