HTML5 DVR 不工作 -- SourceBuffer 从父元素中移除
HTML5 DVR not working -- SourceBuffer removed from parent element
目标
我正在尝试使用 MediaRecorder
、MediaSource
和 SourceBuffer
为 HTML5 视频元素创建一个基本的 "DVR"。目前这只是一个概念证明。然而,由于像 HLS.js 这样的许多项目都利用了 HTML5 视频元素,我相信这将具有广泛的价值。
代码
这是我的代码的要点:
<html>
<head>
</head>
<body>
<video id="src-video" src="http://localhost:8080/video/source.mp4" autoplay></video>
<video id="dvr-video"></video>
<input id="seekbar" type="range" min="-120" max="0" value="0" />
<script>
var mr; // MediaRecorder
var ms = new MediaSource();
var srcBuf; // SourceBuffer
var srcUrl = URL.createObjectURL(ms);
var srcVid = document.getElementById("src-video");
var dvrVid = document.getElementById("dvr-video");
var dvrData = []; // array of ArrayBuffer
var queue = [];
ms.addEventListener("sourceopen", sourceOpen);
srcVid.addEventListener("playing", setupMediaRecorder);
dvrVid.src = srcUrl;
var seekBar = document.getElementById("seekbar");
seekBar.addEventListener("change", function(e) {
// Destroy the old media source and make a new one
URL.revokeObjectURL(srcUrl);
srcBuf = null;
ms = new MediaSource();
ms.addEventListener("sourceopen", sourceOpen);
srcUrl = URL.createObjectURL(ms);
body.removeChild(dvr);
dvr = document.createElement("video");
body.insertBefore(dvr, seekBar);
dvr.src = srcUrl;
});
function sourceOpen()
{
// Create the source buffer
if (!srcBuf)
{
srcBuf = src.addSourceBuffer('video/webm; codecs="opus,vp8"');
srcBuf.mode = "sequence";
}
srcBuf.addEventListener('updateend', function() {
if ( queue.length ) {
srcBuf.appendBuffer(queue.shift());
} else {
dvr.play();
}
}, false);
// Add all fragments in cache
var start = dvrData.length + parseInt(seekBar.value);
queue = [];
for( var i = start; i < dvrData.length; i++ )
{
if (dvrData[i])
queue.push(dvrData[i]);
}
if (queue.length)
srcBuf.appendBuffer(queue.shift());
}
function setupMediaRecorder()
{
var stream = srcVid.captureStream()
mr = new MediaRecorder(stream);
mr.ondataavailable = function(e) {
// Convert the Blob to an ArrayBuffer
var fileReader = new FileReader();
fileReader.onload = function() {
// Append this ArrayBuffer to our playing video
if (srcBuf)
{
if (srcBuf.updating || queue.length)
queue.push(this.result);
else
srcBuf.appendBuffer(this.result);
}
// And to our historical array (for seeking purposes)
dvrData.push(this.result);
if (dvrData.length > 120) {
// Keep only 2 minutes of data
dvrData.splice(0, 1);
}
};
fileReader.readAsArrayBuffer(e.data);
};
mr.start();
// Record 1-second chunks
setInterval(function() {
mr.requestData();
}, 1000);
}
</script>
</body>
</html>
结果
页面首次加载时,"live" 视频元素开始播放,1 秒后 "dvr" 元素开始播放 - 延迟 1 秒。所以它一开始似乎是有效的。
当我执行搜索时,dvr 元素变黑并且我在控制台中收到以下错误(行号可能与上面的代码不完全匹配):
Uncaught DOMException: Failed to execute 'appendBuffer' on 'SourceBuffer': This SourceBuffer has been removed from the parent media source.
at SourceBuffer.<anonymous> (http://localhost:8080/video/dvr.html:80:13)
查看 chrome://media-internals
了解更多详细信息,我看到以下 DVR 播放器:
+-------------+----------------+--------------------------------------------------------------------------------------------
| Timestamp | Property | Value
+-------------+----------------+--------------------------------------------------------------------------------------------
| 00:00:00 00 | origin_url | http://localhost:8080/
| 00:00:00 00 | frame_url | http://localhost:8080/video/dvr.html
| 00:00:00 00 | frame_title |
| 00:00:00 00 | url | blob:http://localhost:8080/cec4134a-4498-43c5-8321-3743761636ac
| 00:00:00 00 | info | ChunkDemuxer: buffering by DTS
| 00:00:00 00 | pipeline_state | kStarting
| 00:00:00 03 | error | Unexpected element ID 0xa3
| 00:00:00 03 | error | Append: stream parsing failed. Data size=112300 append_window_start=0 append_window_end=inf
| 00:00:00 08 | pipeline_error | CHUNK_DEMUXER_ERROR_APPEND_FAILED
| 00:00:00 10 | pipeline_state | kStopping
| 00:00:00 10 | pipeline_state | kStopped
+-------------+----------------+--------------------------------------------------------------------------------------------
Unexpected element ID 0xa3
似乎是罪魁祸首。虽然出于某种原因,当页面首次加载时没有抛出这个错误(我将相同的 ArrayBuffer
附加到我的 SourceBuffer
,所以如果他们在我不这样做之前没有抛出这个错误知道他们为什么现在扔它)
查找关于 WEBM 格式的 0xa3
,这听起来像是指 "SimpleBlock" -- https://chromium.googlesource.com/webm/libwebm/+/libwebm-1.0.0.26/webmids.hpp -- 我不知道为什么这会抛出一个错误?
我尝试过的事情
- 由于初始视频开始播放需要一些时间,我认为在设置 MediaSource 时可能存在竞争条件。我在创建新的 MediaSource、SourceBuffer 等之前添加了 1 秒的延迟。这没有帮助
- 我认为如果加载的第一个块不包含关键帧可能会出错,所以我将块大小从 1 秒增加到 10 秒
- 我尝试了各种源文件(MKV、MOV、MP4、RTMP 流、WebRTC 流等)
- 我试过销毁整个
video
元素并重新创建它
- 如果
SourceBuffer
在播放时以某种方式修改 ArrayBuffer
,我尝试附加副本而不是原始对象 (srcBuf.appendBuffer(queue.shift().slice(0));
)
- 我试过在
SourceBuffer
上的 segment
和 sequence
模式之间切换
- 如果 DVR 元素正在播放我创建
SourceBuffer
(在我有数据之前)并进入 "media complete" 状态,我尝试暂停 DVR 元素
到目前为止,我还没有让 DVR 正常工作。我错过了什么?
我知道了
经过多次试验,我终于弄明白了这个问题。
WEBM 文件实际上是 binary-encoded XML 文件。架构看起来像这样:
<EBML>
<EBMLVersion>...</EBMLVersion>
<EBMLReadVersion>...</EBMLReadVersion>
<EBMLMaxIDLength>...</EBMLMaxIDLength>
<EBMLMaxSizeLength>...</EMBLMaxSizeLength>
<DocType>...</DocType>
<DocTypeVersion>...</DocTypeVersion>
<DocTypeReadVersion>...</DocTypeReadVersion>
</EBML>
<Segment>
<SeekHead>
<Seek>...</Seek>
<Seek>...</Seek>
<Seek>...</Seek>
</SeekHead>
<Void></Void>
<Info>...</Info>
<Tracks>
<TrackEntry>
<Video>
<Colour>
<MatrixCoeffciient>...</MatrixCoefficients>
...
</Colour>
</Video>
<Audio>
...
</Audio>
</TrackEntry>
</Tracks>
<Cues>
<CuePoint>
<CueTrackPositions>...</CueTrackPositions>
</CuePoint>
</Cues>
<Cluster>
<Timecode>...</Timecode>
<SimpleBlock>...</SimpleBlock>
<SimpleBlock>...</SimpleBlock>
<SimpleBlock>...</SimpleBlock>
<SimpleBlock>...</SimpleBlock>
...
</Cluster>
<Cluster>
<Timecode>...</Timecode>
<SimpleBlock>...</SimpleBlock>
<SimpleBlock>...</SimpleBlock>
<SimpleBlock>...</SimpleBlock>
<SimpleBlock>...</SimpleBlock>
...
</Cluster>
...
</Segment>
按照我读取数据的方式,第一个块包含所有 header 信息(EBML 数据、段数据、轨道、时间码等),所有后面的块只是 <Cluster>
和 <SimpleBlock>
标签(偶尔有一个奇怪的 <Timecode>
)
最终我要做的是构建一个基本的多路分解器来解析 EBML 文件并提取 header 信息。然后每当我执行搜索时,这个 header 信息在任何视频数据之前被注入缓冲区。
我的建议
不客气。 MSE 很糟糕,这是一场 72 小时的噩梦。免得您头疼。
目标
我正在尝试使用 MediaRecorder
、MediaSource
和 SourceBuffer
为 HTML5 视频元素创建一个基本的 "DVR"。目前这只是一个概念证明。然而,由于像 HLS.js 这样的许多项目都利用了 HTML5 视频元素,我相信这将具有广泛的价值。
代码
这是我的代码的要点:
<html>
<head>
</head>
<body>
<video id="src-video" src="http://localhost:8080/video/source.mp4" autoplay></video>
<video id="dvr-video"></video>
<input id="seekbar" type="range" min="-120" max="0" value="0" />
<script>
var mr; // MediaRecorder
var ms = new MediaSource();
var srcBuf; // SourceBuffer
var srcUrl = URL.createObjectURL(ms);
var srcVid = document.getElementById("src-video");
var dvrVid = document.getElementById("dvr-video");
var dvrData = []; // array of ArrayBuffer
var queue = [];
ms.addEventListener("sourceopen", sourceOpen);
srcVid.addEventListener("playing", setupMediaRecorder);
dvrVid.src = srcUrl;
var seekBar = document.getElementById("seekbar");
seekBar.addEventListener("change", function(e) {
// Destroy the old media source and make a new one
URL.revokeObjectURL(srcUrl);
srcBuf = null;
ms = new MediaSource();
ms.addEventListener("sourceopen", sourceOpen);
srcUrl = URL.createObjectURL(ms);
body.removeChild(dvr);
dvr = document.createElement("video");
body.insertBefore(dvr, seekBar);
dvr.src = srcUrl;
});
function sourceOpen()
{
// Create the source buffer
if (!srcBuf)
{
srcBuf = src.addSourceBuffer('video/webm; codecs="opus,vp8"');
srcBuf.mode = "sequence";
}
srcBuf.addEventListener('updateend', function() {
if ( queue.length ) {
srcBuf.appendBuffer(queue.shift());
} else {
dvr.play();
}
}, false);
// Add all fragments in cache
var start = dvrData.length + parseInt(seekBar.value);
queue = [];
for( var i = start; i < dvrData.length; i++ )
{
if (dvrData[i])
queue.push(dvrData[i]);
}
if (queue.length)
srcBuf.appendBuffer(queue.shift());
}
function setupMediaRecorder()
{
var stream = srcVid.captureStream()
mr = new MediaRecorder(stream);
mr.ondataavailable = function(e) {
// Convert the Blob to an ArrayBuffer
var fileReader = new FileReader();
fileReader.onload = function() {
// Append this ArrayBuffer to our playing video
if (srcBuf)
{
if (srcBuf.updating || queue.length)
queue.push(this.result);
else
srcBuf.appendBuffer(this.result);
}
// And to our historical array (for seeking purposes)
dvrData.push(this.result);
if (dvrData.length > 120) {
// Keep only 2 minutes of data
dvrData.splice(0, 1);
}
};
fileReader.readAsArrayBuffer(e.data);
};
mr.start();
// Record 1-second chunks
setInterval(function() {
mr.requestData();
}, 1000);
}
</script>
</body>
</html>
结果
页面首次加载时,"live" 视频元素开始播放,1 秒后 "dvr" 元素开始播放 - 延迟 1 秒。所以它一开始似乎是有效的。
当我执行搜索时,dvr 元素变黑并且我在控制台中收到以下错误(行号可能与上面的代码不完全匹配):
Uncaught DOMException: Failed to execute 'appendBuffer' on 'SourceBuffer': This SourceBuffer has been removed from the parent media source.
at SourceBuffer.<anonymous> (http://localhost:8080/video/dvr.html:80:13)
查看 chrome://media-internals
了解更多详细信息,我看到以下 DVR 播放器:
+-------------+----------------+--------------------------------------------------------------------------------------------
| Timestamp | Property | Value
+-------------+----------------+--------------------------------------------------------------------------------------------
| 00:00:00 00 | origin_url | http://localhost:8080/
| 00:00:00 00 | frame_url | http://localhost:8080/video/dvr.html
| 00:00:00 00 | frame_title |
| 00:00:00 00 | url | blob:http://localhost:8080/cec4134a-4498-43c5-8321-3743761636ac
| 00:00:00 00 | info | ChunkDemuxer: buffering by DTS
| 00:00:00 00 | pipeline_state | kStarting
| 00:00:00 03 | error | Unexpected element ID 0xa3
| 00:00:00 03 | error | Append: stream parsing failed. Data size=112300 append_window_start=0 append_window_end=inf
| 00:00:00 08 | pipeline_error | CHUNK_DEMUXER_ERROR_APPEND_FAILED
| 00:00:00 10 | pipeline_state | kStopping
| 00:00:00 10 | pipeline_state | kStopped
+-------------+----------------+--------------------------------------------------------------------------------------------
Unexpected element ID 0xa3
似乎是罪魁祸首。虽然出于某种原因,当页面首次加载时没有抛出这个错误(我将相同的 ArrayBuffer
附加到我的 SourceBuffer
,所以如果他们在我不这样做之前没有抛出这个错误知道他们为什么现在扔它)
查找关于 WEBM 格式的 0xa3
,这听起来像是指 "SimpleBlock" -- https://chromium.googlesource.com/webm/libwebm/+/libwebm-1.0.0.26/webmids.hpp -- 我不知道为什么这会抛出一个错误?
我尝试过的事情
- 由于初始视频开始播放需要一些时间,我认为在设置 MediaSource 时可能存在竞争条件。我在创建新的 MediaSource、SourceBuffer 等之前添加了 1 秒的延迟。这没有帮助
- 我认为如果加载的第一个块不包含关键帧可能会出错,所以我将块大小从 1 秒增加到 10 秒
- 我尝试了各种源文件(MKV、MOV、MP4、RTMP 流、WebRTC 流等)
- 我试过销毁整个
video
元素并重新创建它 - 如果
SourceBuffer
在播放时以某种方式修改ArrayBuffer
,我尝试附加副本而不是原始对象 (srcBuf.appendBuffer(queue.shift().slice(0));
) - 我试过在
SourceBuffer
上的 - 如果 DVR 元素正在播放我创建
SourceBuffer
(在我有数据之前)并进入 "media complete" 状态,我尝试暂停 DVR 元素
segment
和 sequence
模式之间切换
到目前为止,我还没有让 DVR 正常工作。我错过了什么?
我知道了
经过多次试验,我终于弄明白了这个问题。
WEBM 文件实际上是 binary-encoded XML 文件。架构看起来像这样:
<EBML>
<EBMLVersion>...</EBMLVersion>
<EBMLReadVersion>...</EBMLReadVersion>
<EBMLMaxIDLength>...</EBMLMaxIDLength>
<EBMLMaxSizeLength>...</EMBLMaxSizeLength>
<DocType>...</DocType>
<DocTypeVersion>...</DocTypeVersion>
<DocTypeReadVersion>...</DocTypeReadVersion>
</EBML>
<Segment>
<SeekHead>
<Seek>...</Seek>
<Seek>...</Seek>
<Seek>...</Seek>
</SeekHead>
<Void></Void>
<Info>...</Info>
<Tracks>
<TrackEntry>
<Video>
<Colour>
<MatrixCoeffciient>...</MatrixCoefficients>
...
</Colour>
</Video>
<Audio>
...
</Audio>
</TrackEntry>
</Tracks>
<Cues>
<CuePoint>
<CueTrackPositions>...</CueTrackPositions>
</CuePoint>
</Cues>
<Cluster>
<Timecode>...</Timecode>
<SimpleBlock>...</SimpleBlock>
<SimpleBlock>...</SimpleBlock>
<SimpleBlock>...</SimpleBlock>
<SimpleBlock>...</SimpleBlock>
...
</Cluster>
<Cluster>
<Timecode>...</Timecode>
<SimpleBlock>...</SimpleBlock>
<SimpleBlock>...</SimpleBlock>
<SimpleBlock>...</SimpleBlock>
<SimpleBlock>...</SimpleBlock>
...
</Cluster>
...
</Segment>
按照我读取数据的方式,第一个块包含所有 header 信息(EBML 数据、段数据、轨道、时间码等),所有后面的块只是 <Cluster>
和 <SimpleBlock>
标签(偶尔有一个奇怪的 <Timecode>
)
最终我要做的是构建一个基本的多路分解器来解析 EBML 文件并提取 header 信息。然后每当我执行搜索时,这个 header 信息在任何视频数据之前被注入缓冲区。
我的建议
不客气。 MSE 很糟糕,这是一场 72 小时的噩梦。免得您头疼。