blob: bdfb8d1d982caa18a290ea88fe38391d363fe449 [file] [log] [blame]
<!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
// LayoutTests/resources/testharness.js
function assertThrows(func, description) {
try {
func.call(this);
failTest('Error:' + func + description + ' did not throw!');
} catch (e) {
console.log(e);
reportTestSuccess();
}
}
// 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);
}, function(error) { failTest(error.toString()); });
}
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').src = URL.createObjectURL(
event.stream);
resolve(event.stream);
};
localPeerConnection.addStream(localStream);
localPeerConnection.createOffer(function(description) {
localPeerConnection.setLocalDescription(description);
remotePeerConnection.setRemoteDescription(description);
createAnswer(description);
}, function(error) { failTest(error.toString()); });
document.getElementById('video').src = URL.createObjectURL(
localStream);
});
}
function createAndStartMediaRecorder(stream, mimeType, slice) {
return new Promise(function(resolve, reject) {
document.getElementById('video').src = URL.createObjectURL(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;
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;
});
})
.catch(function(err) {
return failTest(err.toString());
})
.then(function() {
reportTestSuccess();
});
}
// 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;
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;
});
})
.catch(function(err) {
return failTest(err.toString());
})
.then(function() {
reportTestSuccess();
});
}
// 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;
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.');
})
.catch(function(err) {
return failTest(err.toString());
})
.then(function() {
reportTestSuccess();
});
}
// 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;
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.');
})
.catch(function(err) { return failTest(err.toString()); })
.then(function() { reportTestSuccess(); });
}
// 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;
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;
});
})
.catch(function(err) {
return failTest(err.toString());
})
.then(function() {
reportTestSuccess();
});
}
// Tests that is it not possible to resume an inactive MediaRecorder.
function testIllegalResumeThrowsDOMError() {
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');
});
}
// Tests that MediaRecorder sends data blobs when resume() is called.
function testResumeAndDataAvailable(mimeType) {
var videoSize = 0;
var emptyBlobs = 0;
var lastTimecode = NaN;
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.');
})
.catch(function(err) {
return failTest(err.toString());
})
.then(function() {
reportTestSuccess();
});
}
// Tests that when paused the recorder will transition |state| to |paused| and
// then trigger a |pause| event.
function testPauseAndRecorderState() {
var pauseEventReceived = false;
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;
});
})
.catch(function(err) {
return failTest(err.toString());
})
.then(function() {
reportTestSuccess();
});
}
// Tests that is is possible to stop a paused MediaRecorder and that the |state|
// becomes 'inactive'.
function testPauseStopAndRecorderState() {
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);
})
.catch(function(err) {
return failTest(err.toString());
})
.then(function() {
reportTestSuccess();
});
}
// Tests that no dataavailable event is fired after MediaRecorder's pause()
// function is called.
function testPausePreventsDataavailableFromBeingFired() {
navigator.mediaDevices.getUserMedia(DEFAULT_CONSTRAINTS)
.then(function(stream) {
return createAndStartMediaRecorder(stream, DEFAULT_RECORDER_MIME_TYPE);
})
.then(function(recorder) {
recorder.pause();
recorder.ondataavailable = function(event) {
failTest('Received unexpected data after pause!');
};
})
.then(function() {
return waitDuration(2000);
})
.catch(function(err) {
return failTest(err.toString());
})
.then(function() {
reportTestSuccess();
});
}
// Tests that MediaRecorder's pause() throws an exception if |state| is not
// 'recording'.
function testIllegalPauseThrowsDOMError() {
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');
});
}
// Tests that a remote peer connection stream can be successfully recorded.
function testRecordRemotePeerConnection(mimeType) {
var videoSize = 0;
var timeStamps = [];
var lastTimecode = NaN;
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;
});
})
.catch(function(err) {
return failTest(err.toString());
})
.then(function() {
reportTestSuccess();
});
}
// Tests that MediaRecorder's stop() throws an exception if |state| is not
// 'recording'.
function testIllegalStopThrowsDOMError() {
navigator.mediaDevices.getUserMedia(DEFAULT_CONSTRAINTS)
.then(function(stream) {
return createMediaRecorder(stream, DEFAULT_RECORDER_MIME_TYPE);
})
.then(function(recorder) {
assertThrows(function() {recorder.stop()}, 'Calling stop() in' +
' inactive state should cause a DOM error');
});
}
// Tests that MediaRecorder's start() throws an exception if |state| is
// 'recording'.
function testIllegalStartInRecordingStateThrowsDOMError() {
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');
});
}
// Tests that MediaRecorder's start() throws an exception if |state| is
// 'paused'.
function testIllegalStartInPausedStateThrowsDOMError() {
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');
});
}
// Tests that MediaRecorder can record a 2 Channel audio stream.
function testTwoChannelAudio() {
var audioSize = 0;
var context = new OfflineAudioContext(2, NUM_SAMPLES, SAMPLING_RATE);
var oscillator = context.createOscillator();
oscillator.type = 'sine';
oscillator.frequency.value = FREQUENCY;
var dest = context.createMediaStreamDestination();
dest.channelCount = 2;
oscillator.connect(dest);
var lastTimecode = NaN;
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();
context.startRendering();
})
.then(function() {
return waitFor('Make sure the recording has data',
function() {
return audioSize > 0;
});
})
.catch(function(err) {
return failTest(err.toString());
})
.then(function() {
console.log('audioSize', audioSize);
reportTestSuccess();
});
}
// 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 = canvas.height = 64;
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); });
}
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;
});
})
.catch(function(err) {
return failTest(err.toString());
})
.then(function() {
reportTestSuccess();
});
}
// Tests that MediaRecorder's requestData() throws an exception if |state| is
// 'inactive'.
function testIllegalRequestDataThrowsDOMError() {
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');
});
}
// Tests that MediaRecorder fires an Error Event when the associated MediaStream
// gets a Track added.
function testAddingTrackToMediaStreamFiresErrorEvent() {
var theStream;
var errorEventReceived = false;
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;
});
})
.catch(function(err) {
return failTest(err.toString());
})
.then(function() {
reportTestSuccess();
});
}
// Tests that MediaRecorder fires an Error Event when the associated MediaStream
// gets a Track removed.
function testRemovingTrackFromMediaStreamFiresErrorEvent() {
var theStream;
var errorEventReceived = false;
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;
});
})
.catch(function(err) {
return failTest(err.toString());
})
.then(function() {
reportTestSuccess();
});
}
</script>
</body>
</html>