// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// The file runs a series of Media Source Entensions (MSE) operations on a
// video tag to set up a media file for playback. The test takes several URL
// parameters described in the loadTestParams() function.
(() => {
// Map from media content to MIME type. All test content must be added to this
// map. (Feel free to extend it for your test case!)
const MEDIA_MIMES = {
'aac_audio.mp4': 'audio/mp4; codecs="mp4a.40.2"',
'h264_video.mp4': 'video/mp4; codecs="avc1.640028"',
'tulip0.av1.mp4': 'video/mp4; codecs="av01.0.05M.08"',
'tulip2.vp9.webm': 'video/webm; codecs="opus,vp9"',
};
const testParams = {}
function test() {
loadTestParams();
if (testParams.waitForPageLoaded) {
document.body.onload = () => {
runTest();
}
} else {
runTest();
}
}
function loadTestParams() {
var queryParameters = parseQueryParameters();
// waitForPageLoaded determines whether to wait for body.onload event or
// to start right away.
testParams.waitForPageLoaded =
(queryParameters['waitForPageLoaded'] === 'true');
// startOffset is used to start the media at an offset instead of at the
// beginning of the file.
testParams.startOffset = parseInt(queryParameters['startOffset'] || '0');
// appendSize determines how large a chunk of the media file to append.
testParams.appendSize = parseInt(queryParameters['appendSize'] || '128000');
// media argument lists the media files to play.
testParams.media = queryParameters['media'];
if (!testParams.media)
throw Error('media parameter must be defined to provide test content');
if (!Array.isArray(testParams.media))
testParams.media = [testParams.media];
}
function parseQueryParameters() {
var params = {};
var r = /([^&=]+)=([^&]*)/g;
var match;
while (match = r.exec(window.location.search.substring(1))) {
key = decodeURIComponent(match[1])
value = decodeURIComponent(match[2]);
if (value.includes(',')) {
value = value.split(',');
}
params[key] = value;
}
return params;
}
function runTest() {
let appenders = [];
let mediaElement = document.getElementById('video_id');
let mediaSource = new window.MediaSource();
window.__mediaSource = mediaSource;
// Pass the test if currentTime of the media increases since that means that
// the file has started playing.
// This code can be modified in the future for full playback tests.
mediaElement.addEventListener('timeupdate', () => {
window.clearTimeout(timeout);
PassTest('Test completed after timeupdate event was received.')
}, {once: true});
// Also pass the test if ended occurs; since we're appending small chunks
// there are cases where 'timeupdate' may not necessarily fire.
mediaElement.addEventListener('ended', () => {
window.clearTimeout(timeout);
if (mediaElement.currentTime > 0)
PassTest('Test completed after ended event was received.')
else
FailTest('Test failed because ended occured before currentTime > 0.')
}, {once: true});
// Fail the test if we time out.
var timeout = setTimeout(function() {
FailTest('Test timed out waiting for a timeupdate or ended event.');
}, 10000);
mediaSource.addEventListener('sourceopen', (open_event) => {
let mediaSource = open_event.target;
for (let i = 0; i < appenders.length; ++i) {
appenders[i].onSourceOpen(mediaSource);
}
// Append each segment and wait for the append to complete.
let num_complete_appends = 0;
for (let i = 0; i < appenders.length; ++i) {
appenders[i].attemptAppend(() => {
num_complete_appends++;
if (num_complete_appends === testParams.media.length) {
mediaSource.endOfStream();
mediaElement.play();
}
});
}
});
// Do not attach MediaSource object until all the buffer appenders have
// received the data from the network that they'll append. This removes
// the factor of network overhead from the attachment timing.
let number_of_appenders_with_data = 0;
for (const media_file of testParams.media) {
appender = new BufferAppender(media_file, MEDIA_MIMES[media_file]);
appender.requestMediaBytes(() => {
number_of_appenders_with_data++;
if (number_of_appenders_with_data === testParams.media.length) {
// This attaches the mediaSource object to the mediaElement. Once this
// operation has completed internally, the mediaSource object
// readyState will transition from closed to open, and the sourceopen
// event will fire.
mediaElement.src = URL.createObjectURL(mediaSource);
}
});
appenders.push(appender);
}
}
class BufferAppender {
constructor(media_file, mimetype) {
this.media_file = media_file;
this.mimetype = mimetype;
this.xhr = new XMLHttpRequest();
this.sourceBuffer = null;
}
requestMediaBytes(callback) {
this.xhr.addEventListener('loadend', callback, {once: true});
this.xhr.open('GET', this.media_file);
this.xhr.setRequestHeader(
'Range', 'bytes=' + testParams.startOffset + '-' +
(testParams.startOffset + testParams.appendSize - 1));
this.xhr.responseType = 'arraybuffer';
this.xhr.send();
}
onSourceOpen(mediaSource) {
if (this.sourceBuffer)
return;
this.sourceBuffer = mediaSource.addSourceBuffer(this.mimetype);
}
attemptAppend(callback) {
if (!this.xhr.response || !this.sourceBuffer)
return;
this.sourceBuffer.addEventListener('updateend', callback, {once: true});
this.sourceBuffer.appendBuffer(this.xhr.response);
this.xhr = null;
}
} // End BufferAppender
function PassTest(message) {
console.log('Test passed: ' + message);
window.__testDone = true;
}
function FailTest(error_message) {
console.error('Test failed: ' + error_message);
window.__testFailed = true;
window.__testError = error_message;
window.__testDone = true;
}
window.onerror = (messageOrEvent, source, lineno, colno, error) => {
// Fail the test if there are any errors. See crbug/777489.
// Note that error.stack already contains error.message.
FailTest(error.stack);
}
window.__test = test;
// These are outputs to be consumed by media Telemetry test driver code.
window.__testDone = false;
window.__testFailed = false;
window.__testError = '';
})();