javascript - 媒体源扩展无法工作
我正在尝试使用 MediaSource API 将不同的 WebM 视频附加到同一个源。
我发现了一个试图做同样事情的 Github 项目,其中加载了一个 WebM 播放列表,每个 WebM 都被附加为一个 SourceBuffer。但是它最后一次提交是在一年前,因此与当前规范不同步。所以我将其分叉,并更新到最新的 API 属性/方法,同时进行了一些重构。现有的许多代码直接取自规范的示例和 Eric Bidelman 的测试页面。
然而,我无法让它按预期工作。我在两个浏览器上进行测试,都是在 Mac OS X 10.9.2 上:Chrome 35 稳定版(写本文时的最新版本)和 Firefox 30 测试版,在 about:config 中将标志 media.mediasource.enabled 设置为 true(此功能将要到 FF 25 才引入,目前稳定版是 24)。
这里是我遇到的问题。
两个浏览器
我希望最终的视频是由这11个WebM(00.webm, 01.webm, …, 10.webm)组成的一个长视频。现在,每个浏览器只播放视频的一个片段。
谷歌浏览器
行为极不一致。似乎无法可靠地重现这些错误。
- 有时视频是空白的,或者中间有一条高高的黑条,并且无法播放。
- 有时视频会加载并停在 01.webm 的第一帧上。
- 有时,视频会播放 02.webm 的几帧然后暂停,只加载了前三个片段。
- 播放按钮最初是灰色的。
- 按下灰色的播放按钮会产生非常不一致的行为。有时候,它会加载一个黑屏、无法播放的视频。其他时候,它会播放第一段,然后,当你播放到结尾时,它会停止,当你再次按下播放/暂停时,它会加载下一段。即便如此,它有时会跳过某些段落,并卡在 04.webm 上。无论如何,它从未播放最后一段,即使控制台报告已经经过了所有缓冲区。
说实话,每次都不一样。我无法在这里列出所有情况。
已知注意事项:Chrome目前尚未实现sourceBuffer.mode,不过我不知道这可能会产生什么影响。
火狐
- 只能播放 00.webm。总运行时间是 0:08,即该视频的长度。
- 视频跳转无效。(这是预期的行为,因为在 onSeeking 事件处理程序中实际上没有任何操作。)
- 视频一旦播放完毕就无法重新开始。
我最初的理论是,这与 mediaSource.sourceBuffers[0].timestampOffset=duration 和 duration=mediaSource.duration 有关。但我似乎无法从 mediaSource.duration 得到任何值,除了 NaN,即使我正在追加新的片段。
完全迷失了。非常感谢指导。
编辑:我取消了代码中持续时间部分的注释,并在所有视频上运行了 Aaron Colwell 的媒体源扩展工具中的 mse_webm_remuxer(感谢 Adam Hart 的提示)。瞧,Chrome 中不再出现不可预测的故障了!但可惜的是,当一个媒体片段结束时,它仍然会暂停,即使按下播放,有时也会卡在一帧上。
在 Firefox Beta 中,它无法播放第一个片段之后的内容,并显示以下响应:
TypeError: 分配给 SourceBuffer.timestampOffset 的值不是有限的浮点数。
记录 duration 的值返回 NaN(但仅在 FF 中)。
答案#1
主要问题出在视频文件上。如果你打开 chrome://media-internals/,你会看到 error Media segment did not begin with keyframe. 使用格式正确的视频,比如 Eric Bidelman 示例中的视频(希望他不会因为我不断直接链接到那个视频而生气,但这是我找到的唯一可用示例视频),你的代码在 appendNextMediaSegment() 中进行如下更改后是可以工作的:
duration = mediaSource.duration;
mediaSource.sourceBuffers[0].timestampOffset = duration;
mediaSource.sourceBuffers[0].appendBuffer(mediaSegment);
你可以尝试使用Aaron Colwell的媒体来源扩展工具来尝试让你的视频正常工作,但我的成功有限。
在你添加片段之前查看 onProgress 事件似乎也有点奇怪,但我猜如果你只想在视频实际播放时追加,这样做是可行的。由于视频长度未知,这可能会让进度条表现得奇怪,不过无论如何这都可能是个问题。
答案#2
我同意Adam Hart说的观点。对于一个webm文件,我尝试实现一个像 http://html5-demos.appspot.com/static/media-source.html 那样的示例,然后得出结论,问题是由我使用的源文件引起的。
如果你还有一个箭头,为什么不尝试使用在 https://developer.mozilla.org/en-US/docs/Web/HTML/DASH_Adaptive_Streaming_for_HTML_5_Video 中介绍的“samplemuxer”。在我看来,samplemuxer 是像 FFMPEG 一样的编码器之一。
我发现转换后的文件可以使用 MediaSource API。如果你也看到它能工作,请告诉我。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>MediaSource API Demo</title>
</head>
<body>
<h3>Appending .webm video chunks using the Media Source API</h3>
<section>
<video controls autoplay width="320" height="240"></video>
<pre id="log"></pre>
</section>
<script>
//ORIGINAL CODE http://html5-demos.appspot.com/static/media-source.html
var FILE = 'IU_output2.webm';
var NUM_CHUNKS = 5;
var video = document.querySelector('video');
var mediaSource = new MediaSource();
video.src = window.URL.createObjectURL(mediaSource);
function callback(e) {
var sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vorbis,vp8"');
logger.log('mediaSource readyState: ' + this.readyState);
GET(FILE, function(uInt8Array) {
var file = new Blob([uInt8Array], {type: 'video/webm'});
var chunkSize = Math.ceil(file.size / NUM_CHUNKS);
logger.log('num chunks:' + NUM_CHUNKS);
logger.log('chunkSize:' + chunkSize + ', totalSize:' + file.size);
// Slice the video into NUM_CHUNKS and append each to the media element.
var i = 0;
(function readChunk_(i) {
var reader = new FileReader();
// Reads aren't guaranteed to finish in the same order they're started in,
// so we need to read + append the next chunk after the previous reader
// is done (onload is fired).
reader.onload = function(e) {
try {
sourceBuffer.appendBuffer(new Uint8Array(e.target.result));
logger.log('appending chunk:' + i);
}catch(e){
console.log(e);
}
if (i == NUM_CHUNKS - 1) {
if(!sourceBuffer.updating)
mediaSource.endOfStream();
} else {
if (video.paused) {
video.play(); // Start playing after 1st chunk is appended.
}
sourceBuffer.addEventListener('updateend', function(e){
if( i < NUM_CHUNKS - 1 )
readChunk_(++i);
});
} //end if
};
var startByte = chunkSize * i;
var chunk = file.slice(startByte, startByte + chunkSize);
reader.readAsArrayBuffer(chunk);
})(i); // Start the recursive call by self calling.
});
}
mediaSource.addEventListener('sourceopen', callback, false);
// mediaSource.addEventListener('webkitsourceopen', callback, false);
//
// mediaSource.addEventListener('webkitsourceended', function(e) {
// logger.log('mediaSource readyState: ' + this.readyState);
// }, false);
function GET(url, callback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'arraybuffer';
xhr.send();
xhr.onload = function(e) {
if (xhr.status != 200) {
alert("Unexpected status code " + xhr.status + " for " + url);
return false;
}
callback(new Uint8Array(xhr.response));
};
}
</script>
<script>
function Logger(id) {
this.el = document.getElementById('log');
}
Logger.prototype.log = function(msg) {
var fragment = document.createDocumentFragment();
fragment.appendChild(document.createTextNode(msg));
fragment.appendChild(document.createElement('br'));
this.el.appendChild(fragment);
};
Logger.prototype.clear = function() {
this.el.textContent = '';
};
var logger = new Logger('log');
</script>
</body>
</html>
另一个测试代码
<!DOCTYPE html>
<html>
<head>
<title>MediaSource API Demo</title>
</head>
<body>
<h3>Appending .webm video chunks using the Media Source API</h3>
<section>
<video controls autoplay width="320" height="240"></video>
<pre id="log"></pre>
</section>
<script>
//ORIGINAL CODE http://html5-demos.appspot.com/static/media-source.html
var FILE = 'IU_output2.webm';
// var FILE = 'test_movie_output.webm';
var NUM_CHUNKS = 10;
var video = document.querySelector('video');
var mediaSource = new MediaSource();
video.src = window.URL.createObjectURL(mediaSource);
function callback(e) {
var sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vorbis,vp8"');
logger.log('mediaSource readyState: ' + this.readyState);
GET(FILE, function(uInt8Array) {
logger.log('byteLength:' + uInt8Array.byteLength );
sourceBuffer.appendBuffer(uInt8Array);
});
}
mediaSource.addEventListener('sourceopen', callback, false);
// mediaSource.addEventListener('webkitsourceopen', callback, false);
//
// mediaSource.addEventListener('webkitsourceended', function(e) {
// logger.log('mediaSource readyState: ' + this.readyState);
// }, false);
function GET(url, callback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'arraybuffer';
xhr.send();
xhr.onload = function(e) {
if (xhr.status != 200) {
alert("Unexpected status code " + xhr.status + " for " + url);
return false;
}
callback(new Uint8Array(xhr.response));
};
}
</script>
<script>
function Logger(id) {
this.el = document.getElementById('log');
}
Logger.prototype.log = function(msg) {
var fragment = document.createDocumentFragment();
fragment.appendChild(document.createTextNode(msg));
fragment.appendChild(document.createElement('br'));
this.el.appendChild(fragment);
};
Logger.prototype.clear = function() {
this.el.textContent = '';
};
var logger = new Logger('log');
</script>
</body>
</html>
谢谢。
