Chrome 56 において MediaRecorder API で録画した WebM が MediaSource API で再生できない問題
問題
MediaRecorder で生成した WebM の Blob を MediaSource に読ませて再生したい。
しかし、以下のコードでは Chrome 56.0.2924.87 (64-bit) では再生できない。
async function main(){
const video = document.createElement('video');
const stream = await navigator.mediaDevices.getUserMedia({video: true, audio: true});
const rec = new MediaRecorder(stream, {mimeType: 'video/webm; codecs="opus,vp8"'});
const ms = new MediaSource();
video.src = URL.createObjectURL(ms);
video.controls = true;
video.autoplay = true;
document.body.appendChild(video);
await waitEvent(ms, "sourceopen");
const sb = ms.addSourceBuffer(rec.mimeType);
const chunks: ArrayBuffer[] = [];
rec.ondataavailable = async ({data: blob})=>{
const buf = await readAsArrayBuffer(blob);
chunks.push(buf);
};
console.info("start recorfing");
rec.start(100);
(async function recur(){
if(rec.state === "paused"){ return; }
if(sb.updating){
await sleep(10);
return recur();
}
const chunk = chunks.shift();
if(chunk == null){
await sleep(10);
return recur();
}
sb.appendBuffer(chunk);
console.info("appendBuffer:", chunk.byteLength, "B");
await waitEvent(sb, "updateend");
return recur();
})();
await sleep(30 * 1000);
rec.stop();
stream.getTracks().map((track)=>{ track.stop(); });
if(ms.readyState === "open"){ ms.endOfStream(); }
console.info("stop recorfing");
await sleep(60 * 1000);
console.info("garbage collection");
ms.removeSourceBuffer(sb);
URL.revokeObjectURL(video.src);
}
function sleep(ms: number): Promise<void> {
return new Promise<void>((resolve)=>{ setTimeout(resolve, ms); });
}
function waitEvent<EV extends Event>(target: EventTarget, event: string, error?: string): Promise<EV> {
return new Promise<EV>((resolve, reject)=>{
target.addEventListener(event, _resolve);
if(typeof error === "string"){
target.addEventListener(error, _reject);
}
function _removeListener(){
target.removeEventListener(event, _resolve);
if(typeof error === "string"){
target.removeEventListener(error, _reject);
}
}
function _resolve(ev: EV){
_removeListener();
resolve(ev);
}
function _reject(ev: EV){
_removeListener();
reject(ev);
}
});
}
function readAsArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
const reader = new FileReader();
reader.readAsArrayBuffer(blob);
return waitEvent(reader, "loadend", "error").then(()=> reader.result);
}
main();
原因
- https://bugs.chromium.org/p/chromium/issues/detail?id=606000#c22
- Chrome 56 の WebM はヘッダに
Segment > Tracks > TrackEntry > DefaultDuration
を含むが、フレーム間のデルタ時刻が一定でない場合うまく再生できなくなるため。 - Chrome 58.0.3015.0 canary (64-bit) では
DefaultDuration
を含まないようにする パッチが適用された。
Chrome58 を使わない対策
ondataavailable
で送られてくる WebM の EBML を ts-ebml を使ってパースし、取り除く。
+ import * as EBML from "ts-ebml";
async function main(){
const video = document.createElement('video');
const stream = await navigator.mediaDevices.getUserMedia({video: true, audio: true});
+ const dec = new EBML.Decoder();
+ const enc = new EBML.Encoder();
rec.ondataavailable = async ({data: blob})=>{
const buf = await readAsArrayBuffer(blob);
+ const elms = dec.decode(buf);
+ const _elms = elms.filter((elm)=> elm.name !== "DefaultDuration");
+ const _buf = enc.encode(_elms);
- chunks.push(buf);
+ chunks.push(_buf);
};