chromium/content/test/data/media/mediarecorder_test.html

<!DOCTYPE html>
<html>
<head>
<title>MediaStream Recoder Browser Test (w/ MediaSource)</title>
</head>
<body>
  <div> Record Real-Time video content browser test.</div>
  <video id="video" autoplay></video>
  <video id="remoteVideo" autoplay></video>
</body>
<script type="text/javascript" src="mediarecorder_test_utils.js"></script>
<script type="text/javascript" src="webrtc_test_utilities.js"></script>
<script>

'use strict';

const DEFAULT_CONSTRAINTS = {audio: true, video: true};
const DEFAULT_RECORDER_MIME_TYPE = '';
const DEFAULT_TIME_SLICE = 100;
const FREQUENCY = 880;
// Note that not all audio sampling rates are supported by the underlying
// Opus audio codec: the valid rates are 8kHz, 12kHz, 16kHz, 24kHz, 48kHz.
// See crbug/569089 for details.
const SAMPLING_RATE = 48000;
const NUM_SAMPLES = 2 * SAMPLING_RATE;

// Function assert_throws inspired from Blink's
// web_tests/resources/testharness.js

function assertThrows(func, description) {
  try {
    func.call(this);
  } catch (e) {
    console.log(e);
    return;
  }
  throw new Error('Error:' + func + description + ' did not throw!');
}

// TODO(cpaulin): factor this method out of here, http://crbug.com/574503.
function setupPeerConnection(stream) {
  return new Promise(function(resolve, reject) {
    var localStream = stream;
    var remoteStream = null;
    var localPeerConnection = new RTCPeerConnection();
    var remotePeerConnection = new RTCPeerConnection();

    function createAnswer(description) {
      remotePeerConnection.createAnswer(function(description) {
        remotePeerConnection.setLocalDescription(description);
        localPeerConnection.setRemoteDescription(description);
      }, reject);
    }

    localPeerConnection.onicecandidate = function(event) {
      if (event.candidate) {
        remotePeerConnection.addIceCandidate(new RTCIceCandidate(
            event.candidate));
      }
    };
    remotePeerConnection.onicecandidate = function(event) {
      if (event.candidate) {
        localPeerConnection.addIceCandidate(new RTCIceCandidate(
            event.candidate));
      }
    };
    remotePeerConnection.onaddstream = function(event) {
      document.getElementById('remoteVideo').srcObject = event.stream;
      resolve(event.stream);
    };

    localPeerConnection.addStream(localStream);

    localPeerConnection.createOffer(function(description) {
      localPeerConnection.setLocalDescription(description);
      remotePeerConnection.setRemoteDescription(description);
      createAnswer(description);
    }, reject);

    document.getElementById('video').srcObject = localStream;
  });
}

function createAndStartMediaRecorder(stream, mimeType, slice) {
  return new Promise(function(resolve, reject) {
    document.getElementById('video').srcObject = stream;
    var recorder = new MediaRecorder(stream, {'mimeType' : mimeType});
    console.log('Recorder object created, mimeType = ' + mimeType);
    if (slice != undefined) {
      recorder.start(slice);
      console.log('Recorder started with time slice', slice);
    } else {
      recorder.start(0);
    }
    resolve(recorder);
  });
}

function createMediaRecorder(stream, mimeType) {
  return new Promise(function(resolve, reject) {
    var recorder = new MediaRecorder(stream, {'mimeType' : mimeType});
    console.log('Recorder object created.');
    resolve(recorder);
  });
}

// Tests that the MediaRecorder's start(0) function will cause the |state| to be
// 'recording' and that a 'start' event is fired.
function testStartAndRecorderState() {
  var startEventReceived = false;
  return navigator.mediaDevices.getUserMedia(DEFAULT_CONSTRAINTS)
      .then(function(stream) {
        return createMediaRecorder(stream, DEFAULT_RECORDER_MIME_TYPE);
      })
      .then(function(recorder) {
        recorder = recorder;
        recorder.onstart = function(event) {
          startEventReceived = true;
          assertEquals('recording', recorder.state);
        };
        recorder.start(0);
      })
      .then(function() {
        return waitFor('Make sure the start event was received',
            function() {
              return startEventReceived;
            });
      })
      .then(logSuccess);
}

// Tests that the MediaRecorder's stop() function will effectively cause the
// |state| to be 'inactive' and that a 'stop' event is fired.
function testStartStopAndRecorderState() {
  var stopEventReceived = false;
  return navigator.mediaDevices.getUserMedia(DEFAULT_CONSTRAINTS)
      .then(function(stream) {
        return createAndStartMediaRecorder(stream, DEFAULT_RECORDER_MIME_TYPE);
      })
      .then(function(recorder) {
        recorder.onstop = function(event) {
          stopEventReceived = true;
          assertEquals('inactive', recorder.state);
        };
        recorder.stop();
      })
      .then(function() {
        return waitFor('Make sure the stop event was received',
            function() {
              return stopEventReceived;
            });
      })
      .then(logSuccess);
}

// Tests that when MediaRecorder's start(0) function is called, some data is
// made available by media recorder via dataavailable events, containing non
// empty blob data.
function testStartAndDataAvailable(mimeType) {
  var videoSize = 0;
  var emptyBlobs = 0;
  var lastTimecode = NaN;
  return navigator.mediaDevices.getUserMedia(DEFAULT_CONSTRAINTS)
      .then(function(stream) {
        return createAndStartMediaRecorder(stream, mimeType);
      })
      .then(function(recorder) {
        // Save history of Blobs received via dataavailable.
        recorder.ondataavailable = function(event) {
          if (event.data.size > 0)
            videoSize += event.data.size;
          else
            emptyBlobs += 1;

          if (!isNaN(lastTimecode))
            assertTrue(event.timecode > lastTimecode, 'timecodes');
          lastTimecode = event.timecode;
        };
      })
      .then(function() {
        return waitFor('Make sure the recording has data',
            function() {
              return videoSize > 0;
            });
      })
      .then(function() {
        assertTrue(emptyBlobs == 0, 'Recording has ' + emptyBlobs +
            ' empty blobs, there should be no such empty blobs.');
      })
      .then(logSuccess);
}

// Tests that when MediaRecorder's start(timeSlice) is called, some data
// available events are fired containing non empty blob data.
function testStartWithTimeSlice(mimeType) {
  var videoSize = 0;
  var emptyBlobs = 0;
  var timeStamps = [];
  var lastTimecode = NaN;
  return navigator.mediaDevices.getUserMedia(DEFAULT_CONSTRAINTS)
      .then(function(stream) {
        return createAndStartMediaRecorder(stream, mimeType,
                                           DEFAULT_TIME_SLICE);
      })
      .then(function(recorder) {
        recorder.ondataavailable = function(event) {
          timeStamps.push(event.timeStamp);
          if (event.data.size > 0)
            videoSize += event.data.size;
          else
            emptyBlobs += 1;

          if (!isNaN(lastTimecode))
            assertTrue(event.timecode > lastTimecode, 'timecodes');
          lastTimecode = event.timecode;
        };
      })
      .then(function() {
        return waitFor('Making sure the recording has data',
            function() {
              return videoSize > 0 && timeStamps.length > 10;
            });
      })
      .then(function() {
        assertTrue(emptyBlobs == 0, 'Recording has ' + emptyBlobs +
            ' empty blobs, there should be no such empty blobs.');
      })
      .then(logSuccess);
}


// Tests that when a MediaRecorder's resume() is called, the |state| is
// 'recording' and a 'resume' event is fired.
function testResumeAndRecorderState() {
  var theRecorder;
  var resumeEventReceived = false;
  return navigator.mediaDevices.getUserMedia(DEFAULT_CONSTRAINTS)
      .then(function(stream) {
        return createAndStartMediaRecorder(stream, DEFAULT_RECORDER_MIME_TYPE);
      })
      .then(function(recorder) {
        theRecorder = recorder;
        theRecorder.pause();
      })
      .then(function() {
        theRecorder.onresume = function(event) {
          resumeEventReceived = true;
          assertEquals('recording', theRecorder.state);
        };
        theRecorder.resume();
      })
      .then(function() {
        return waitFor('Making sure the resume event has been received',
            function() {
              return resumeEventReceived;
            });
      })
      .then(logSuccess);
}

// Tests that is it not possible to resume an inactive MediaRecorder.
function testIllegalResumeThrowsDOMError() {
  return navigator.mediaDevices.getUserMedia(DEFAULT_CONSTRAINTS)
      .then(function(stream) {
        return createMediaRecorder(stream, DEFAULT_RECORDER_MIME_TYPE);
      })
      .then(function(recorder) {
        assertThrows(function() {recorder.resume()}, 'Calling resume() in' +
            ' inactive state should cause a DOM error');
      })
      .then(logSuccess);
}

// Tests that MediaRecorder sends data blobs when resume() is called.
function testResumeAndDataAvailable(mimeType) {
  var videoSize = 0;
  var emptyBlobs = 0;
  var lastTimecode = NaN;
  return navigator.mediaDevices.getUserMedia(DEFAULT_CONSTRAINTS)
      .then(function(stream) {
        return createAndStartMediaRecorder(stream, mimeType);
      })
      .then(function(recorder) {
        recorder.pause();
        recorder.ondataavailable = function(event) {
          if (event.data.size > 0) {
            videoSize += event.data.size;
          } else {
            console.log('This dataavailable event is empty', event);
            emptyBlobs += 1;
          }

          if (!isNaN(lastTimecode))
            assertTrue(event.timecode > lastTimecode, 'timecodes');
          lastTimecode = event.timecode;
        };
        recorder.resume();
      })
      .then(function() {
        return waitFor('Make sure the recording has data after resuming',
            function() {
              return videoSize > 0;
            });
      })
      .then(function() {
        // There should be no empty blob while recording.
        assertTrue(emptyBlobs == 0, 'Recording has ' + emptyBlobs +
            ' empty blobs, there should be no such empty blobs.');
      })
      .then(logSuccess);
}

// Tests that when paused the recorder will transition |state| to |paused| and
// then trigger a |pause| event.
function testPauseAndRecorderState() {
  var pauseEventReceived = false;
  return navigator.mediaDevices.getUserMedia(DEFAULT_CONSTRAINTS)
      .then(function(stream) {
        return createAndStartMediaRecorder(stream, DEFAULT_RECORDER_MIME_TYPE);
      })
      .then(function(recorder) {
        recorder.onpause = function(event) {
          pauseEventReceived = true;
          assertEquals('paused', recorder.state);
        };
        recorder.pause();
      })
      .then(function() {
        return waitFor('Making sure the pause event has been received',
            function() {
              return pauseEventReceived;
            });
      })
      .then(logSuccess);
}

// Tests that is is possible to stop a paused MediaRecorder and that the |state|
// becomes 'inactive'.
function testPauseStopAndRecorderState() {
  return navigator.mediaDevices.getUserMedia(DEFAULT_CONSTRAINTS)
      .then(function(stream) {
        return createAndStartMediaRecorder(stream, DEFAULT_RECORDER_MIME_TYPE);
      })
      .then(function(recorder) {
        recorder.pause();
        recorder.stop();
        assertEquals('inactive', recorder.state);
      })
      .then(logSuccess);
}

// Tests that no dataavailable event is fired after MediaRecorder's pause()
// function is called.
function testPausePreventsDataavailableFromBeingFired() {
  return navigator.mediaDevices.getUserMedia(DEFAULT_CONSTRAINTS)
      .then(function(stream) {
        return createAndStartMediaRecorder(stream, DEFAULT_RECORDER_MIME_TYPE);
      })
      .then(function(recorder) {
        recorder.pause();
        return Promise.race([
          new Promise((resolve, reject) => {
            recorder.ondataavailable = function(event) {
              reject(new Error('Received unexpected data after pause!'));
            };
          }),
          waitDuration(2000),
        ]);
      })
      .then(logSuccess);
}

// Tests that MediaRecorder's pause() throws an exception if |state| is not
// 'recording'.
function testIllegalPauseThrowsDOMError() {
  return navigator.mediaDevices.getUserMedia(DEFAULT_CONSTRAINTS)
      .then(function(stream) {
        return createMediaRecorder(stream, DEFAULT_RECORDER_MIME_TYPE);
      })
      .then(function(recorder) {
        assertThrows(function() {recorder.pause()}, 'Calling pause() in' +
            ' inactive state should cause a DOM error');
      })
      .then(logSuccess);
}

// Tests that a remote peer connection stream can be successfully recorded.
function testRecordRemotePeerConnection(mimeType) {
  var videoSize = 0;
  var timeStamps = [];
  var lastTimecode = NaN;
  return navigator.mediaDevices.getUserMedia(DEFAULT_CONSTRAINTS)
      .then(function(localStream) {
        return setupPeerConnection(localStream);
      })
      .then(function(remoteStream) {
        return createMediaRecorder(remoteStream, mimeType);
      })
      .then(function(recorder) {
        recorder.ondataavailable = function(event) {
          timeStamps.push(event.timeStamp);
          videoSize += event.data.size;

          if (!isNaN(lastTimecode))
            assertTrue(event.timecode > lastTimecode, 'timecodes');
          lastTimecode = event.timecode;
        };
        recorder.start(0);
      })
      .then(function() {
        return waitFor('Making sure the recording has data',
            function() {
              return videoSize > 0 && timeStamps.length > 100;
            });
      })
      .then(logSuccess);
}

// Tests that MediaRecorder's start() throws an exception if |state| is
// 'recording'.
function testIllegalStartInRecordingStateThrowsDOMError() {
  return navigator.mediaDevices.getUserMedia(DEFAULT_CONSTRAINTS)
      .then(function(stream) {
        return createAndStartMediaRecorder(stream, DEFAULT_RECORDER_MIME_TYPE);
      })
      .then(function(recorder) {
        assertThrows(function() {recorder.start()}, 'Calling start() in' +
            ' recording state should cause a DOM error');
      })
      .then(logSuccess);
}

// Tests that MediaRecorder's start() throws an exception if |state| is
// 'paused'.
function testIllegalStartInPausedStateThrowsDOMError() {
  return navigator.mediaDevices.getUserMedia(DEFAULT_CONSTRAINTS)
      .then(function(stream) {
        return createAndStartMediaRecorder(stream, DEFAULT_RECORDER_MIME_TYPE);
      })
      .then(function(recorder) {
        recorder.pause();
        assertThrows(function() {recorder.start()}, 'Calling start() in' +
            ' paused state should cause a DOM error');
      })
      .then(logSuccess);
}

// Tests that MediaRecorder can record a 2 Channel audio stream.
function testTwoChannelAudio() {
  var audioSize = 0;
  var context = new AudioContext();
  var oscillator = context.createOscillator();
  oscillator.type = 'sine';
  oscillator.frequency.value = FREQUENCY;
  var dest = context.createMediaStreamDestination();
  dest.channelCount = 2;
  oscillator.connect(dest);
  var lastTimecode = NaN;
  return createMediaRecorder(dest.stream, DEFAULT_RECORDER_MIME_TYPE)
      .then(function(recorder) {
        recorder.ondataavailable = function(event) {
          audioSize += event.data.size;
          if (!isNaN(lastTimecode))
            assertTrue(event.timecode > lastTimecode, 'timecodes');
          lastTimecode = event.timecode;
        };
        recorder.start(0);
        oscillator.start();
      })
      .then(function() {
        return waitFor('Make sure the recording has data',
            function() {
              return audioSize > 0;
            });
      })
      .then(function() {
        console.log('audioSize', audioSize);
        return logSuccess();
      });
}

// Tests that MediaRecorder can handle video input with alpha channel.
function testRecordWithTransparency(mimeType) {
  const ON_DATA_AVAILABLE_THRESHOLD = 10;
  const NUMBER_OF_EVENTS_TO_RECORD = 5;

  var canvas = document.createElement('canvas');
  canvas.width = 640;
  canvas.height = 480;
  var stream = canvas.captureStream();
  assertTrue(stream, 'Error creating MediaStream');
  assertEquals(1, stream.getVideoTracks().length);
  assertEquals(0, stream.getAudioTracks().length);
  var recordedEvents = 0;

  function drawOnCanvas(canvas) {
    var ctx = canvas.getContext('2d', {alpha: true});
    ctx.fillStyle = 'green';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    requestAnimationFrame( function() { drawOnCanvas(canvas); });
  }

  return createMediaRecorder(stream, mimeType)
      .then(function(recorder) {
        recorder.ondataavailable = function(event) {
          if (event.data.size > ON_DATA_AVAILABLE_THRESHOLD)
            ++recordedEvents;
        };
        recorder.start(0);
        drawOnCanvas(canvas);
      })
      .then(function() {
        return waitFor('Make sure the recording has data',
            function() {
              return recordedEvents > NUMBER_OF_EVENTS_TO_RECORD;
            });
      })
      .then(logSuccess);
}

// Tests that MediaRecorder's requestData() throws an exception if |state| is
// 'inactive'.
function testIllegalRequestDataThrowsDOMError() {
  return navigator.mediaDevices.getUserMedia(DEFAULT_CONSTRAINTS)
      .then(function(stream) {
        return createMediaRecorder(stream, DEFAULT_RECORDER_MIME_TYPE);
      })
      .then(function(recorder) {
        assertThrows(function() {recorder.requestData()},
            'Calling requestdata() in inactive state should throw a DOM ' +
            'Exception');
      })
      .then(logSuccess);
}

// Tests that MediaRecorder fires an Error Event when the associated MediaStream
// gets a Track added.
function testAddingTrackToMediaStreamFiresErrorEvent() {
  var theStream;
  var errorEventReceived = false;
  return navigator.mediaDevices.getUserMedia(DEFAULT_CONSTRAINTS)
      .then(function(stream) {
        theStream = stream;
        return createMediaRecorder(stream);
      })
      .then(function(recorder) {
        recorder.onerror = function(event) {
          errorEventReceived = true;
        };
        recorder.start(0);
        // Add a new track, copy of an existing one for simplicity.
        theStream.addTrack(theStream.getTracks()[1].clone());
      })
      .then(function() {
        return waitFor('Waiting for the Error Event',
            function() {
              return errorEventReceived;
            });
      })
      .then(logSuccess);
}

// Tests that MediaRecorder fires an Error Event when the associated MediaStream
// gets a Track removed.
function testRemovingTrackFromMediaStreamFiresErrorEvent() {
  var theStream;
  var errorEventReceived = false;
  return navigator.mediaDevices.getUserMedia(DEFAULT_CONSTRAINTS)
      .then(function(stream) {
        theStream = stream;
        return createMediaRecorder(stream);
      })
      .then(function(recorder) {
        recorder.onerror = function(event) {
          errorEventReceived = true;
        };
        recorder.start(0);
        theStream.removeTrack(theStream.getTracks()[1]);
      })
      .then(function() {
        return waitFor('Waiting for the Error Event',
            function() {
              return errorEventReceived;
            });
      })
      .then(logSuccess);
}

</script>
</body>
</html>