LoginSignup
1
2

More than 5 years have passed since last update.

Chrome 56 において MediaRecorder API で録画した WebM が MediaSource API で再生できない問題

Last updated at Posted at 2017-02-18

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();

原因

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);
  };
1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2