2019/media/conformanceTest.js (1,762 lines of code) (raw):

/** * @license * Copyright 2018 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 'use strict'; /** * MSE Conformance Test Suite. * @class */ var ConformanceTest = function() { var mseVersion = 'Current Editor\'s Draft'; var webkitPrefix = MediaSource.prototype.version.indexOf('webkit') >= 0; var tests = []; var info = 'No MSE Support!'; if (window.MediaSource) { info = 'MSE Spec Version: ' + mseVersion; info += ' | webkit prefix: ' + webkitPrefix.toString(); } info += ' | Default Timeout: ' + TestBase.timeout + 'ms'; var fields = ['passes', 'failures', 'timeouts']; var createConformanceTest = function(name, category, mandatory) { var t = createMSTest(name); t.prototype.index = tests.length; t.prototype.passes = 0; t.prototype.failures = 0; t.prototype.timeouts = 0; t.prototype.category = category || 'General'; if (typeof mandatory === 'boolean') { t.prototype.mandatory = mandatory; } tests.push(t); return t; }; /** * Test if the value of video state is expected when onsourceopen * event happens. */ var createInitialMediaStateTest = function(state, value, check) { var test = createConformanceTest( 'InitialMedia' + util.MakeCapitalName(state), 'Media Element Core'); check = typeof(check) === 'undefined' ? 'checkEq' : check; test.prototype.title = 'Test if the state ' + state + ' is correct when onsourceopen is called'; test.prototype.onsourceopen = function() { this.runner[check](this.video[state], value, state); this.runner.succeed(); }; }; createInitialMediaStateTest('duration', NaN); createInitialMediaStateTest('videoWidth', 0); createInitialMediaStateTest('videoHeight', 0); createInitialMediaStateTest('readyState', HTMLMediaElement.HAVE_NOTHING); createInitialMediaStateTest('src', '', 'checkNE'); createInitialMediaStateTest('currentSrc', '', 'checkNE'); /** * Validate the XHR request can send Uint8Array. */ var testXHRUint8Array = createConformanceTest('XHRUint8Array', 'XHR'); testXHRUint8Array.prototype.title = 'Ensure that XHR can send an Uint8Array'; testXHRUint8Array.prototype.timeout = 10000; testXHRUint8Array.prototype.start = function(runner, video) { var s = 'XHR DATA'; var buf = new ArrayBuffer(s.length); var view = new Uint8Array(buf); for (var i = 0; i < s.length; i++) { view[i] = s.charCodeAt(i); } var xhr = runner.XHRManager.createPostRequest( 'https://drmproxy.appspot.com/echo', function(e) { runner.checkEq(String.fromCharCode.apply(null, xhr.getResponseData()), s, 'XHR response'); runner.succeed(); }, view.length); xhr.send(view); }; /** * Ensure that XHR aborts actually abort by issuing an absurd number of them * and then aborting all but one. */ var testXHRAbort = createConformanceTest('XHRAbort', 'XHR'); testXHRAbort.prototype.title = 'Ensure that XHR aborts actually abort by ' + 'issuing an absurd number of them and then aborting all but one.'; testXHRAbort.prototype.start = function(runner, video) { var N = 100; var startTime = Date.now(); var lastAbortTime; function startXHR(i) { var xhr = runner.XHRManager.createRequest( Media.VP9.VideoNormal.src + '?x=' + Date.now() + '.' + i, function() { if (i >= N) { xhr.getResponseData(); // This will verify status internally. runner.succeed(); } }); if (i < N) { runner.timeouts.setTimeout(xhr.abort.bind(xhr), 10); runner.timeouts.setTimeout(startXHR.bind(null, i + 1), 1); lastAbortTime = Date.now(); } xhr.send(); }; startXHR(0); }; /** * Ensure XMLHttpRequest.open does not reset XMLHttpRequest.responseType. */ var testXHROpenState = createConformanceTest('XHROpenState', 'XHR'); testXHROpenState.prototype.title = 'Ensure XMLHttpRequest.open does not ' + 'reset XMLHttpRequest.responseType'; testXHROpenState.prototype.start = function(runner, video) { var xhr = new XMLHttpRequest; // It should not be an error to set responseType before calling open xhr.responseType = 'arraybuffer'; xhr.open('GET', 'http://google.com', true); runner.checkEq(xhr.responseType, 'arraybuffer', 'XHR responseType'); runner.succeed(); }; /** * Validate existence of MediaSource object. */ var testPresence = createConformanceTest('Presence', 'MSE Core'); testPresence.prototype.title = 'Test if MediaSource object is present.'; testPresence.prototype.start = function(runner, video) { if (!window.MediaSource) return runner.fail('No MediaSource object available.'); var ms = new MediaSource(); if (!ms) return runner.fail('Found MediaSource but could not create one'); if (ms.version) this.log('Media source version reported as ' + ms.version); else this.log('No media source version reported'); runner.succeed(); }; /** * Ensure MediaSource object can be attached to video. */ var testAttach = createConformanceTest('Attach', 'MSE Core'); testAttach.prototype.timeout = 2000; testAttach.prototype.title = 'Test if MediaSource object can be attached to video.'; testAttach.prototype.start = function(runner, video) { this.ms = new MediaSource(); this.ms.addEventListener('sourceopen', function() { runner.succeed(); }); video.src = window.URL.createObjectURL(this.ms); video.load(); }; /** * Test addSourceBuffer is working correctly. */ var testAddSourceBuffer = createConformanceTest('AddSourceBuffer', 'MSE Core'); testAddSourceBuffer.prototype.title = 'Test if we can add source buffer'; testAddSourceBuffer.prototype.onsourceopen = function() { try { this.runner.checkEq( this.ms.sourceBuffers.length, 0, 'Source buffer number'); this.ms.addSourceBuffer(Media.AAC.mimetype); this.runner.checkEq( this.ms.sourceBuffers.length, 1, 'Source buffer number'); this.ms.addSourceBuffer(Media.VP9.mimetype); this.runner.checkEq( this.ms.sourceBuffers.length, 2, 'Source buffer number'); } catch (e) { this.runner.fail(e); } this.runner.succeed(); }; /** * Ensure add incorrect source buffer type will fire the correct exceptions. */ var testAddSourceBufferException = createConformanceTest('AddSBException', 'MSE Core'); testAddSourceBufferException.prototype.title = 'Test if add incorrect ' + 'source buffer type will fire the correct exceptions.'; testAddSourceBufferException.prototype.onsourceopen = function() { var runner = this.runner; var self = this; runner.checkException(function() { self.ms.addSourceBuffer('^^^'); }, DOMException.NOT_SUPPORTED_ERR); runner.checkException(function() { var ms = new MediaSource; ms.addSourceBuffer(Media.AAC.mimetype); }, DOMException.INVALID_STATE_ERR); runner.succeed(); }; /** * Test addSourceBuffer and removeSourceBuffer are working correctly. */ var testSourceRemove = createConformanceTest('RemoveSourceBuffer', 'MSE Core'); testSourceRemove.prototype.title = 'Test if we can add/remove source buffers'; testSourceRemove.prototype.onsourceopen = function() { var sb = this.ms.addSourceBuffer(Media.AAC.mimetype); this.ms.removeSourceBuffer(sb); this.runner.checkEq(this.ms.sourceBuffers.length, 0, 'Source buffer number'); this.ms.addSourceBuffer(Media.AAC.mimetype); this.runner.checkEq(this.ms.sourceBuffers.length, 1, 'Source buffer number'); for (var i = 0; i < 10; ++i) { try { sb = this.ms.addSourceBuffer(Media.VP9.mimetype); this.runner.checkEq(this.ms.sourceBuffers.length, 2, 'Source buffer number'); this.ms.removeSourceBuffer(sb); this.runner.checkEq(this.ms.sourceBuffers.length, 1, 'Source buffer number'); } catch (e) { return this.runner.fail(e); } } this.runner.succeed(); }; /** * Ensure MediaSource state has the expected value when onsourceopen happens. */ var createInitialMSStateTest = function(state, value, check) { var test = createConformanceTest('InitialMS' + util.MakeCapitalName(state), 'MSE Core'); check = typeof(check) === 'undefined' ? 'checkEq' : check; test.prototype.title = 'Test if the state ' + state + ' is correct when onsourceopen is called'; test.prototype.onsourceopen = function() { this.runner[check](this.ms[state], value, state); this.runner.succeed(); }; }; createInitialMSStateTest('duration', NaN); createInitialMSStateTest('readyState', 'open'); /** * Ensure we can set MediaSource.duration. */ var testDuration = createConformanceTest('Duration', 'MSE Core'); testDuration.prototype.title = 'Test if we can set duration.'; testDuration.prototype.onsourceopen = function() { this.ms.duration = 10; this.runner.checkEq(this.ms.duration, 10, 'ms.duration'); this.runner.succeed(); }; /** * Test events on the MediaElement. */ var mediaElementEvents = createConformanceTest('MediaElementEvents', 'MSE Core'); mediaElementEvents.prototype.title = 'Test events on the MediaElement.'; mediaElementEvents.prototype.onsourceopen = function() { var runner = this.runner; var media = this.video; var ms = this.ms; var audioStream = Media.AAC.Audio1MB; var videoStream = Media.VP9.Video1MB; var audioSb = this.ms.addSourceBuffer(audioStream.mimetype); var videoSb = this.ms.addSourceBuffer(videoStream.mimetype); var self = this; var videoXhr = runner.XHRManager.createRequest(videoStream.src, function(e) { self.log('onload called'); var onUpdate = function() { videoSb.removeEventListener('update', onUpdate); setDuration(1.0, ms, [videoSb, audioSb], function() { if (audioSb.updating || videoSb.updating) { runner.fail('Source buffers are updating on duration change.'); return; } ms.endOfStream(); media.play(); }); } videoSb.addEventListener('update', onUpdate); videoSb.appendBuffer(videoXhr.getResponseData()); }); var audioXhr = runner.XHRManager.createRequest(audioStream.src, function(e) { self.log('onload called'); var onAudioUpdate = function() { audioSb.removeEventListener('update', onAudioUpdate); videoXhr.send(); } audioSb.addEventListener('update', onAudioUpdate); audioSb.appendBuffer(audioXhr.getResponseData()); }); media.addEventListener('ended', function() { self.log('onended called'); runner.succeed(); }); audioXhr.send(); }; /** * Test if the events on MediaSource are correct. */ var mediaSourceEvents = createConformanceTest('MediaSourceEvents', 'MSE Core'); mediaSourceEvents.prototype.title = 'Test if the events on MediaSource are correct.'; mediaSourceEvents.prototype.onsourceopen = function() { var runner = this.runner; var media = this.video; var ms = this.ms; var audioStream = Media.AAC.Audio1MB; var videoStream = Media.VP9.Video1MB; var audioSb = this.ms.addSourceBuffer(audioStream.mimetype); var videoSb = this.ms.addSourceBuffer(videoStream.mimetype); var lastState = 'open'; var self = this; var videoXhr = runner.XHRManager.createRequest(videoStream.src, function(e) { self.log('onload called'); videoSb.appendBuffer(videoXhr.getResponseData()); videoSb.abort(); ms.endOfStream(); }); var audioXhr = runner.XHRManager.createRequest(audioStream.src, function(e) { self.log('onload called'); audioSb.appendBuffer(audioXhr.getResponseData()); audioSb.abort(); videoXhr.send(); }); ms.addEventListener('sourceclose', function() { self.log('onsourceclose called'); runner.checkEq(lastState, 'ended', 'The previous state'); runner.succeed(); }); ms.addEventListener('sourceended', function() { self.log('onsourceended called'); runner.checkEq(lastState, 'open', 'The previous state'); lastState = 'ended'; media.removeAttribute('src'); media.load(); }); audioXhr.send(); }; /** * Append to buffer until exceeding the quota error. */ var testBufferSize = createConformanceTest('VideoBufferSize', 'MSE Core'); testBufferSize.prototype.title = 'Determines video buffer sizes by ' + 'appending incrementally until discard occurs, and tests that it meets ' + 'the minimum requirements for streaming.'; testBufferSize.prototype.onsourceopen = function() { var runner = this.runner; // The test clip has a bitrate which is nearly exactly 1MB/sec, and // lasts 1s. We start appending it repeatedly until we get eviction. var videoStream = Media.VP9.Video1MB; var sb = this.ms.addSourceBuffer(videoStream.mimetype); var audioStream = Media.AAC.Audio1MB; var unused_audioSb = this.ms.addSourceBuffer(audioStream.mimetype); var self = this; var MIN_SIZE = 12 * 1024 * 1024; var ESTIMATED_MIN_TIME = 12; var xhr = runner.XHRManager.createRequest(videoStream.src, function(e) { var onBufferFull = function() { runner.checkGE(expectedTime - sb.buffered.start(0), ESTIMATED_MIN_TIME, 'Estimated source buffer size'); runner.succeed(); }; var expectedTime = 0; var expectedSize = 0; var appendCount = 0; sb.addEventListener('updateend', function onUpdate() { appendCount++; self.log('Append count ' + appendCount); if (sb.buffered.start(0) > 0 || expectedTime > sb.buffered.end(0)) { sb.removeEventListener('updateend', onUpdate); onBufferFull(); } else { expectedTime += videoStream.duration; expectedSize += videoStream.size; // Pass the test if the UA can handle 10x more than expected. if (expectedSize > (10 * MIN_SIZE)) { sb.removeEventListener('updateend', onUpdate); onBufferFull(); return; } sb.timestampOffset = expectedTime; try { sb.appendBuffer(xhr.getResponseData()); } catch (e) { var QUOTA_EXCEEDED_ERROR_CODE = 22; if (e.code == QUOTA_EXCEEDED_ERROR_CODE) { sb.removeEventListener('updateend', onUpdate); onBufferFull(); } else { runner.fail(e); } } } }); sb.appendBuffer(xhr.getResponseData()); }); xhr.send(); }; /** * Ensure we can start play before feeding any data. The play should * start automatically after data is appended. */ var testStartPlayWithoutData = createConformanceTest('StartPlayWithoutData', 'MSE Core'); testStartPlayWithoutData.prototype.title = 'Test if we can start play before feeding any data. The play should ' + 'start automatically after data is appended'; testStartPlayWithoutData.prototype.onsourceopen = function() { var runner = this.runner; var media = this.video; var audioStream = Media.AAC.AudioHuge; var videoStream = Media.VP9.VideoHuge; var videoChain = new ResetInit( new FileSource(videoStream.src, runner.XHRManager, runner.timeouts)); var videoSb = this.ms.addSourceBuffer(videoStream.mimetype); var audioChain = new ResetInit( new FileSource(audioStream.src, runner.XHRManager, runner.timeouts)); var audioSb = this.ms.addSourceBuffer(audioStream.mimetype); media.play(); appendUntil(runner.timeouts, media, videoSb, videoChain, 1, function() { appendUntil(runner.timeouts, media, audioSb, audioChain, 1, function() { playThrough( runner.timeouts, media, 1, 2, videoSb, videoChain, audioSb, audioChain, function() { runner.checkGE(media.currentTime, 2, 'currentTime'); runner.succeed(); }); }); }); }; /** * Ensure event timestamp is relative to the initial page load. */ var testEventTimestamp = createConformanceTest('EventTimestamp', 'MSE Core'); testEventTimestamp.prototype.title = 'Test Event Timestamp is relative to ' + 'the initial page load'; testEventTimestamp.prototype.onsourceopen = function() { var runner = this.runner; var video = this.video; var videoStream = Media.VP9.VideoTiny; var audioStream = Media.AAC.AudioTiny; var videoSb = this.ms.addSourceBuffer(videoStream.mimetype); var audioSb = this.ms.addSourceBuffer(audioStream.mimetype); runner.checkGr(Date.now(), 1360000000000, 'Date.now()'); var lastTime = 0.0; var requestCounter = 0; var audioXhr = runner.XHRManager.createRequest(audioStream.src, function(e) { audioSb.appendBuffer(this.getResponseData()); video.addEventListener('timeupdate', function(e) { runner.checkGE(e.timeStamp, lastTime, 'event.timeStamp'); lastTime = e.timeStamp; if (!video.paused && video.currentTime >= 2 && requestCounter >= 3) { runner.succeed(); } requestCounter++; }); video.play(); }, 0, 500000); var videoXhr = runner.XHRManager.createRequest(videoStream.src, function(e) { videoSb.appendBuffer(this.getResponseData()); audioXhr.send(); }, 0, 1500000); videoXhr.send(); }; /** * Ensure timeupdate event fired with correct currentTime value after seeking. */ var testSeekTimeUpdate = createConformanceTest('SeekTimeUpdate', 'MSE Core'); testSeekTimeUpdate.prototype.title = 'Timeupdate event fired with correct currentTime after seeking.'; testSeekTimeUpdate.prototype.onsourceopen = function() { var runner = this.runner; var media = this.video; var videoStream = Media.VP9.VideoNormal; var audioStream = Media.AAC.AudioNormal; var videoSb = this.ms.addSourceBuffer(videoStream.mimetype); var audioSb = this.ms.addSourceBuffer(audioStream.mimetype); var lastTime = 0; var updateCount = 0; var xhr = runner.XHRManager.createRequest(videoStream.src, function() { videoSb.appendBuffer(xhr.getResponseData()); var xhr2 = runner.XHRManager.createRequest(audioStream.src, function() { audioSb.appendBuffer(xhr2.getResponseData()); callAfterLoadedMetaData(media, function() { media.addEventListener('timeupdate', function(e) { if (!media.paused) { ++updateCount; runner.checkGE(media.currentTime, lastTime, 'media.currentTime'); if (updateCount > 3) { updateCount = 0; lastTime += 10; if (lastTime >= 35) runner.succeed(); else media.currentTime = lastTime + 6; } } }); media.play(); }); }, 0, 1000000); xhr2.send(); }, 0, 5000000); this.ms.duration = 100000000; // Ensure that we can seek to any position. xhr.send(); }; /** * Creates a MSE currentTime Accuracy test to validate if the media.currentTime * is accurate to within 250 milliseconds during active playback. This can * be used for video features a standard frame rate or a high frame rate. */ var createCurrentTimeAccuracyTest = function(videoStream, audioStream, frameRate) { var test = createConformanceTest( frameRate + 'Accuracy', 'MSE currentTime'); test.prototype.title = 'Test the currentTime granularity.'; test.prototype.onsourceopen = function() { var runner = this.runner; var video = this.video; var maxTimeDiff = 0; var baseTimeDiff = 0; var times = 0; var videoSb = this.ms.addSourceBuffer(videoStream.mimetype); var audioSb = this.ms.addSourceBuffer(audioStream.mimetype); var videoXhr = runner.XHRManager.createRequest( videoStream.src, function(e) { videoSb.appendBuffer(this.getResponseData()); video.addEventListener('timeupdate', function(e) { if (times === 0) { baseTimeDiff = util.ElapsedTimeInS() - video.currentTime; } else { var timeDiff = util.ElapsedTimeInS() - video.currentTime; maxTimeDiff = Math.max( Math.abs(timeDiff - baseTimeDiff), maxTimeDiff); } if (times > 500 || video.currentTime > 10) { runner.checkLE( maxTimeDiff, 0.25, 'media.currentTime diff during playback'); runner.succeed(); } ++times; }); video.addEventListener('canplaythrough', function(e) { video.play(); }); }, 0, 2500000); var audioXhr = runner.XHRManager.createRequest( audioStream.src, function(e) { audioSb.appendBuffer(this.getResponseData()); videoXhr.send(); }, 0, 2500000); audioXhr.send(); }; }; createCurrentTimeAccuracyTest( Media.VP9.Webgl720p30fps, Media.AAC.AudioNormal, 'SFR'); createCurrentTimeAccuracyTest( Media.VP9.Webgl720p60fps, Media.AAC.AudioNormal, 'HFR'); /** * Creates a MSE currentTime PausedAccuracy test to validate if * the media.currentTime is accurate to within 32 milliseconds when content * is paused. This can be used for video features a standard frame rate * or a high frame rate. Test checks the accuracy of media.currentTime at * two events: when content is paused and when content is played again after * the pause, if either one meets the threshold, test passes. */ var createCurrentTimePausedAccuracyTest = function(videoStream, audioStream, frameRate) { var test = createConformanceTest( frameRate + 'PausedAccuracy', 'MSE currentTime', false); test.prototype.title = 'Test the currentTime granularity when pause.'; test.prototype.onsourceopen = function() { var maxDiffInS = 0.032; var runner = this.runner; var video = this.video; var baseTimeDiff = 0; var times = 0; var assertTimeAtPlay = false; var currentTimeIsAccurate = false; var self = this; var videoSb = this.ms.addSourceBuffer(videoStream.mimetype); var audioSb = this.ms.addSourceBuffer(audioStream.mimetype); var videoXhr = runner.XHRManager.createRequest( videoStream.src, function(e) { videoSb.appendBuffer(this.getResponseData()); function onTimeUpdate(e) { if (times === 0) { baseTimeDiff = util.ElapsedTimeInS() - video.currentTime; } if (times > 500 || video.currentTime > 10) { video.removeEventListener('timeupdate', onTimeUpdate); video.pause(); } ++times; }; video.addEventListener('play', function() { if (assertTimeAtPlay) { var timeDiff = util.ElapsedTimeInS() - video.currentTime; var currentTimeDiff = Math.abs(baseTimeDiff - timeDiff); self.log('media.currentTime is ' + currentTimeDiff + 's different' + ' from actual time when video is played after a pause.'); currentTimeIsAccurate = currentTimeIsAccurate || (currentTimeDiff <= maxDiffInS); runner.checkEq( currentTimeIsAccurate, true, 'media.currentTime diff is within ' + maxDiffInS + 's'); assertTimeAtPlay = false; runner.succeed(); } }); video.addEventListener('pause', function(e) { var timeDiff = util.ElapsedTimeInS() - video.currentTime; var currentTimeDiff = Math.abs(baseTimeDiff - timeDiff); runner.checkEq(video.paused, true, 'media.paused'); self.log('meida.currentTime is ' + currentTimeDiff + 's different from actual time when video is paused.'); currentTimeIsAccurate = currentTimeDiff <= maxDiffInS; assertTimeAtPlay = true; video.play(); }); video.addEventListener('timeupdate', onTimeUpdate); video.addEventListener('canplaythrough', function(e) { video.play(); }) }, 0, 2500000); var audioXhr = runner.XHRManager.createRequest( audioStream.src, function(e) { audioSb.appendBuffer(this.getResponseData()); videoXhr.send(); }, 0, 2500000); audioXhr.send(); }; }; createCurrentTimePausedAccuracyTest( Media.VP9.Webgl720p30fps, Media.AAC.AudioNormal, 'SFR'); createCurrentTimePausedAccuracyTest( Media.VP9.Webgl720p60fps, Media.AAC.AudioNormal, 'HFR'); /** * Validate specified mimetype is supported. */ var createSupportTest = function(mimetype, desc) { var test = createConformanceTest(desc + 'Support', 'MSE Formats'); test.prototype.title = 'Test if we support ' + desc + ' with mimetype: ' + mimetype; test.prototype.onsourceopen = function() { try { this.log('Trying format ' + mimetype); var src = this.ms.addSourceBuffer(mimetype); } catch (e) { return this.runner.fail(e); } this.runner.succeed(); }; }; createSupportTest(Media.AAC.mimetype, 'AAC'); createSupportTest(Media.H264.mimetype, 'H264'); createSupportTest(Media.VP9.mimetype, 'VP9'); createSupportTest(Media.Opus.mimetype, 'Opus'); /** * Test appendBuffer for specified mimetype by appending twice in a row. * When the first append happens, the sourceBuffer becomes temporarily unusable * and it's updating should be set to true, which makes the second appends * unsuccessful and throws INVALID_STATE_ERR exception. * However, sometimes the update happens so fast that the second append manage * as well. */ var createAppendTest = function(stream, unused_stream) { var test = createConformanceTest( 'Append' + stream.codec + util.MakeCapitalName(stream.mediatype), 'MSE (' + stream.codec + ')'); test.prototype.title = 'Test if we can append a whole ' + stream.mediatype + ' file whose size is 1MB.'; test.prototype.onsourceopen = function() { var runner = this.runner; var sb = this.ms.addSourceBuffer(stream.mimetype); var unused_sb = this.ms.addSourceBuffer(unused_stream.mimetype); var xhr = runner.XHRManager.createRequest(stream.src, function(e) { var data = xhr.getResponseData(); function updateEnd(e) { runner.checkEq(sb.buffered.length, 1, 'Source buffer number'); runner.checkEq(sb.buffered.start(0), 0, 'Range start'); runner.checkApproxEq(sb.buffered.end(0), stream.duration, 'Range end'); // Try appending twice in a row -- // this should throw an INVALID_STATE_ERR exception. var caught = false; try { sb.removeEventListener('updateend', updateEnd); sb.appendBuffer(data); sb.appendBuffer(data); } catch (e) { if (e.code === e.INVALID_STATE_ERR) { runner.succeed(); } else { runner.fail('Invalid error on double append: ' + e); } caught = true; } if (!caught) { // We may have updated so fast that we didn't encounter the error. if (sb.updating) { // Not a great check due to race conditions, but will have to do. runner.fail('Implementation did not throw INVALID_STATE_ERR.'); } else { runner.succeed(); } } } sb.addEventListener('updateend', updateEnd); sb.appendBuffer(data); }); xhr.send(); }; }; /** * Ensure sourceBuffer can abort current segment and end up with correct value. */ var createAbortTest = function(stream, unused_stream) { var test = createConformanceTest( 'Abort' + stream.codec + util.MakeCapitalName(stream.mediatype), 'MSE (' + stream.codec + ')'); test.prototype.title = 'Test if we can abort the current segment.'; test.prototype.onsourceopen = function() { var runner = this.runner; var sb = this.ms.addSourceBuffer(stream.mimetype); var unused_sb = this.ms.addSourceBuffer(unused_stream.mimetype); var xhr = runner.XHRManager.createRequest(stream.src, function(e) { var responseData = xhr.getResponseData(); var abortEnded = function(e) { sb.removeEventListener('updateend', abortEnded); sb.addEventListener('update', function(e) { runner.checkEq(sb.buffered.length, 1, 'Source buffer number'); runner.checkEq(sb.buffered.start(0), 0, 'Range start'); runner.checkGr(sb.buffered.end(0), 0, 'Range end'); runner.succeed(); }); sb.appendBuffer(responseData); } var appendStarted = function(e) { sb.removeEventListener('update', appendStarted); sb.addEventListener('updateend', abortEnded); sb.abort(); } sb.addEventListener('update', appendStarted); sb.appendBuffer(responseData); }, 0, stream.size); xhr.send(); }; }; /** * Ensure timestamp offset can be set. */ var createTimestampOffsetTest = function(stream, unused_stream) { var test = createConformanceTest( 'TimestampOffset' + stream.codec + util.MakeCapitalName(stream.mediatype), 'MSE (' + stream.codec + ')'); test.prototype.title = 'Test if we can set timestamp offset.'; test.prototype.onsourceopen = function() { var runner = this.runner; var sb = this.ms.addSourceBuffer(stream.mimetype); var unused_sb = this.ms.addSourceBuffer(unused_stream.mimetype); var xhr = runner.XHRManager.createRequest(stream.src, function(e) { sb.timestampOffset = 5; sb.appendBuffer(xhr.getResponseData()); sb.addEventListener('updateend', function() { runner.checkEq(sb.buffered.length, 1, 'Source buffer number'); runner.checkEq(sb.buffered.start(0), 5, 'Range start'); runner.checkApproxEq(sb.buffered.end(0), stream.duration + 5, 'Range end'); runner.succeed(); }); }); xhr.send(); }; }; /** * Test the sourceBuffer DASH switch latency. * Validate it's less than 1 second. */ var createDASHLatencyTest = function(videoStream, audioStream) { var test = createConformanceTest('DASHLatency' + videoStream.codec, 'MSE (' + videoStream.codec + ')'); test.prototype.title = 'Test SourceBuffer DASH switch latency'; test.prototype.onsourceopen = function() { var self = this; var runner = this.runner; var videoSb = this.ms.addSourceBuffer(videoStream.mimetype); var audioSb = this.ms.addSourceBuffer(audioStream.mimetype); var video = this.video; var videoXhr = runner.XHRManager.createRequest(videoStream.src, function(e) { var videoContent = videoXhr.getResponseData(); var expectedTime = 0; var loopCount = 0; var MAX_ITER = 300; var OVERFLOW_OFFSET = 1.0; var onBufferFull = function() { var bufferSize = loopCount * videoStream.size / 1048576; self.log('Buffer size: ' + Math.round(bufferSize) + 'MB'); var DASH_MAX_LATENCY = 1; var newContentStartTime = videoSb.buffered.start(0) + 2; self.log('Source buffer updated as exceeding buffer limit'); video.addEventListener('timeupdate', function onTimeUpdate(e) { if (video.currentTime > newContentStartTime + DASH_MAX_LATENCY) { video.removeEventListener('timeupdate', onTimeUpdate); runner.succeed(); } }); video.play(); } videoSb.addEventListener('update', function onUpdate() { expectedTime += videoStream.duration; videoSb.timestampOffset = expectedTime; loopCount++; if (loopCount > MAX_ITER) { videoSb.removeEventListener('update', onUpdate); runner.fail('Failed to fill up source buffer.'); return; } // Fill up the buffer such that it overflow implementations. if (expectedTime > videoSb.buffered.end(0) + OVERFLOW_OFFSET) { videoSb.removeEventListener('update', onUpdate); onBufferFull(); } try { videoSb.appendBuffer(videoContent); } catch (e) { videoSb.removeEventListener('update', onUpdate); var QUOTA_EXCEEDED_ERROR_CODE = 22; if (e.code == QUOTA_EXCEEDED_ERROR_CODE) { onBufferFull(); } else { runner.fail(e); } } }); videoSb.appendBuffer(videoContent);; }); var audioXhr = runner.XHRManager.createRequest(audioStream.src, function(e) { var audioContent = audioXhr.getResponseData(); audioSb.appendBuffer(audioContent); videoXhr.send(); }); audioXhr.send(); }; }; /** * Ensure valid duration change after append buffer by halving the duration. */ var createDurationAfterAppendTest = function(stream, unused_stream) { var test = createConformanceTest( 'DurationAfterAppend' + stream.codec + util.MakeCapitalName(stream.mediatype), 'MSE (' + stream.codec + ')'); test.prototype.title = 'Test if the duration expands after appending data.'; test.prototype.onsourceopen = function() { var runner = this.runner; var media = this.video; var ms = this.ms; var sb = ms.addSourceBuffer(stream.mimetype); var unused_sb = ms.addSourceBuffer(unused_stream.mimetype); var self = this; var xhr = runner.XHRManager.createRequest(stream.src, function(e) { var data = xhr.getResponseData(); var updateCb = function() { var halfDuration; var durationChanged = false; var sbUpdated = false; sb.removeEventListener('updateend', updateCb); sb.abort(); if (sb.updating) { runner.fail(); return; } media.addEventListener( 'durationchange', function onDurationChange() { media.removeEventListener('durationchange', onDurationChange); self.log('Duration change complete.'); runner.checkApproxEq(ms.duration, halfDuration, 'ms.duration'); durationChanged = true; if (durationChanged && sbUpdated) { runner.succeed(); } }); halfDuration = sb.buffered.end(0) / 2; setDuration(halfDuration, ms, sb, function() { self.log('Remove() complete.'); runner.checkApproxEq(ms.duration, halfDuration, 'ms.duration'); runner.checkApproxEq(sb.buffered.end(0), halfDuration, 'sb.buffered.end(0)'); sb.addEventListener('updateend', function onUpdate() { sb.removeEventListener('updateend', onUpdate); runner.checkApproxEq(ms.duration, sb.buffered.end(0), 'ms.duration'); sbUpdated = true; if (durationChanged && sbUpdated) { runner.succeed(); } }); sb.appendBuffer(data); }); }; sb.addEventListener('updateend', updateCb); sb.appendBuffer(data); }); xhr.send(); }; }; /** * Test pause state before or after appending data to sourceBuffer. */ var createPausedTest = function(stream) { var test = createConformanceTest( 'PausedStateWith' + stream.codec + util.MakeCapitalName(stream.mediatype), 'MSE (' + stream.codec + ')'); test.prototype.title = 'Test if the paused state is correct before or ' + ' after appending data.'; test.prototype.onsourceopen = function() { var runner = this.runner; var media = this.video; var ms = this.ms; var sb = ms.addSourceBuffer(stream.mimetype); runner.checkEq(media.paused, true, 'media.paused'); var xhr = runner.XHRManager.createRequest(stream.src, function(e) { runner.checkEq(media.paused, true, 'media.paused'); sb.appendBuffer(xhr.getResponseData()); runner.checkEq(media.paused, true, 'media.paused'); sb.addEventListener('updateend', function() { runner.checkEq(media.paused, true, 'media.paused'); runner.succeed(); }); }); xhr.send(); }; }; /** * Test if video dimension is correct before or after appending data. */ var createVideoDimensionTest = function(videoStream, audioStream) { var test = createConformanceTest('VideoDimension' + videoStream.codec, 'MSE (' + videoStream.codec + ')'); test.prototype.title = 'Test if video dimension is correct before or after appending data.'; test.prototype.onsourceopen = function() { var runner = this.runner; var media = this.video; var videoChain = new ResetInit(new FixedAppendSize( new FileSource(videoStream.src, runner.XHRManager, runner.timeouts), 65536)); var videoSb = this.ms.addSourceBuffer(videoStream.mimetype); var audioSb = this.ms.addSourceBuffer(audioStream.mimetype); var self = this; runner.checkEq(media.videoWidth, 0, 'video width'); runner.checkEq(media.videoHeight, 0, 'video height'); var totalSuccess = 0; function checkSuccess() { totalSuccess++; if (totalSuccess == 2) runner.succeed(); } media.addEventListener('loadedmetadata', function(e) { self.log('loadedmetadata called'); runner.checkEq(media.videoWidth, 640, 'video width'); runner.checkEq(media.videoHeight, 360, 'video height'); checkSuccess(); }); runner.checkEq(media.readyState, media.HAVE_NOTHING, 'readyState'); var audioXhr = runner.XHRManager.createRequest(audioStream.src, function(e) { var audioContent = audioXhr.getResponseData(); audioSb.appendBuffer(audioContent); appendInit(media, videoSb, videoChain, 0, checkSuccess); }); audioXhr.send(); }; }; /** * Test if the playback state transition is correct. */ var createPlaybackStateTest = function(stream) { var test = createConformanceTest('PlaybackState' + stream.codec, 'MSE (' + stream.codec + ')'); test.prototype.title = 'Test if the playback state transition is correct.'; test.prototype.onsourceopen = function() { var runner = this.runner; var media = this.video; var videoStream = stream; var audioStream = Media.AAC.AudioTiny; var videoChain = new ResetInit(new FixedAppendSize( new FileSource(videoStream.src, runner.XHRManager, runner.timeouts), 65536)); var videoSb = this.ms.addSourceBuffer(videoStream.mimetype); var audioChain = new ResetInit(new FixedAppendSize( new FileSource(audioStream.src, runner.XHRManager, runner.timeouts), 65536)); var audioSb = this.ms.addSourceBuffer(audioStream.mimetype); var self = this; media.play(); runner.checkEq(media.currentTime, 0, 'media.currentTime'); media.pause(); runner.checkEq(media.currentTime, 0, 'media.currentTime'); appendInit(media, audioSb, audioChain, 0, function() { appendInit(media, videoSb, videoChain, 0, function() { callAfterLoadedMetaData(media, function() { media.play(); runner.checkEq(media.currentTime, 0, 'media.currentTime'); media.pause(); runner.checkEq(media.currentTime, 0, 'media.currentTime'); media.play(); appendUntil( runner.timeouts, media, audioSb, audioChain, 5, function() { appendUntil( runner.timeouts, media, videoSb, videoChain, 5, function() { playThrough(runner.timeouts, media, 1, 2, audioSb, audioChain, videoSb, videoChain, function() { var time = media.currentTime; media.pause(); runner.checkApproxEq( media.currentTime, time, 'media.currentTime'); runner.succeed(); }); }); }); }); }); }); }; }; /** * Ensure we can play a partially appended video segment. */ var createPlayPartialSegmentTest = function(stream) { var test = createConformanceTest('PlayPartial' + stream.codec + 'Segment', 'MSE (' + stream.codec + ')'); test.prototype.title = 'Test if we can play a partially appended video segment.'; test.prototype.onsourceopen = function() { var runner = this.runner; var video = this.video; var videoStream = stream; var audioStream = Media.AAC.AudioTiny; var videoSb = this.ms.addSourceBuffer(videoStream.mimetype); var audioSb = this.ms.addSourceBuffer(audioStream.mimetype); var videoXhr = runner.XHRManager.createRequest( videoStream.src, function(e) { videoSb.appendBuffer(this.getResponseData()); video.addEventListener('timeupdate', function(e) { if (!video.paused && video.currentTime >= 2) { runner.succeed(); } }); video.play(); }, 0, 1500000); var audioXhr = runner.XHRManager.createRequest( audioStream.src, function(e) { audioSb.appendBuffer(this.getResponseData()); videoXhr.send(); }, 0, 500000); audioXhr.send(); }; }; /** * Ensure we can play a partially appended audio segment. */ var createIncrementalAudioTest = function(stream) { var test = createConformanceTest('Incremental' + stream.codec + 'Audio', 'MSE (' + stream.codec + ')'); test.prototype.title = 'Test if we can play a partially appended audio segment.'; test.prototype.onsourceopen = function() { var runner = this.runner; var sb = this.ms.addSourceBuffer(stream.mimetype); var unused_sb = this.ms.addSourceBuffer(Media.VP9.mimetype); var xhr = runner.XHRManager.createRequest(stream.src, function(e) { sb.appendBuffer(xhr.getResponseData()); sb.addEventListener('updateend', function() { runner.checkEq(sb.buffered.length, 1, 'Source buffer number'); runner.checkEq(sb.buffered.start(0), 0, 'Range start'); runner.checkApproxEq( sb.buffered.end(0), stream.get(200000), 'Range end'); runner.succeed(); }); }, 0, 200000); xhr.send(); }; }; /** * Ensure we can append audio data with an explicit offset. */ var createAppendAudioOffsetTest = function(stream1, stream2) { var test = createConformanceTest('Append' + stream1.codec + 'AudioOffset', 'MSE (' + stream1.codec + ')'); test.prototype.title = 'Test if we can append audio data with an explicit offset.'; test.prototype.onsourceopen = function() { var runner = this.runner; var video = this.video; var unused_sb = this.ms.addSourceBuffer(Media.VP9.mimetype); var sb = this.ms.addSourceBuffer(stream1.mimetype); var xhr = runner.XHRManager.createRequest(stream1.src, function(e) { sb.timestampOffset = 5; sb.appendBuffer(this.getResponseData()); sb.addEventListener('updateend', function callXhr2() { sb.removeEventListener('updateend', callXhr2); xhr2.send(); }); }, 0, 200000); var xhr2 = runner.XHRManager.createRequest(stream2.src, function(e) { sb.abort(); sb.timestampOffset = 0; sb.appendBuffer(this.getResponseData()); sb.addEventListener('updateend', function() { runner.checkEq(sb.buffered.length, 1, 'Source buffer number'); runner.checkEq(sb.buffered.start(0), 0, 'Range start'); runner.checkApproxEq( sb.buffered.end(0), stream2.get('appendAudioOffset'), 'Range end'); runner.succeed(); }); }, 0, 200000); xhr.send(); }; }; /** * Ensure we can append video data with an explicit offset. */ var createAppendVideoOffsetTest = function(stream1, stream2, audioStream) { var test = createConformanceTest('Append' + stream1.codec + 'VideoOffset', 'MSE (' + stream1.codec + ')'); test.prototype.title = 'Test if we can append video data with an explicit offset.'; test.prototype.onsourceopen = function() { var self = this; var runner = this.runner; var video = this.video; var sb = this.ms.addSourceBuffer(stream1.mimetype); var audioSb = this.ms.addSourceBuffer(audioStream.mimetype); var xhr = runner.XHRManager.createRequest(stream1.src, function(e) { sb.timestampOffset = 5; sb.appendBuffer(this.getResponseData()); sb.addEventListener('update', function callXhr2() { sb.removeEventListener('update', callXhr2); xhr2.send(); }); }, 0, 200000); var xhr2 = runner.XHRManager.createRequest(stream2.src, function(e) { sb.abort(); sb.timestampOffset = 0; sb.appendBuffer(this.getResponseData()); sb.addEventListener('updateend', function() { runner.checkEq(sb.buffered.length, 1, 'Source buffer number'); runner.checkEq(sb.buffered.start(0), 0, 'Range start'); runner.checkApproxEq(sb.buffered.end(0), stream2.get('videoChangeRate'), 'Range end'); callAfterLoadedMetaData(video, function() { video.addEventListener('seeked', function(e) { self.log('seeked called'); video.addEventListener('timeupdate', function(e) { self.log('timeupdate called with ' + video.currentTime); if (!video.paused && video.currentTime >= 6) { runner.succeed(); } }); }); video.currentTime = 6; }); }); video.play(); }, 0, 400000); this.ms.duration = 100000000; // Ensure that we can seek to any position. var audioXhr = runner.XHRManager.createRequest(audioStream.src, function(e) { var audioContent = audioXhr.getResponseData(); audioSb.appendBuffer(audioContent); xhr.send(); }); audioXhr.send(); }; }; /** * Ensure we can append multiple init segments. */ var createAppendMultipleInitTest = function(stream, unused_stream) { var test = createConformanceTest( 'AppendMultipleInit' + stream.codec + util.MakeCapitalName(stream.mediatype), 'MSE (' + stream.codec + ')'); test.prototype.title = 'Test if we can append multiple init segments.'; test.prototype.onsourceopen = function() { var runner = this.runner; var media = this.video; var chain = new FileSource(stream.src, runner.XHRManager, runner.timeouts, 0, stream.size, stream.size); var src = this.ms.addSourceBuffer(stream.mimetype); var unused_sb = this.ms.addSourceBuffer(unused_stream.mimetype); var init; function getEventAppend(cb, endCb) { var chainCount = 0; return function() { if (chainCount < 10) { ++chainCount; cb(); } else { endCb(); } }; } chain.init(0, function(buf) { init = buf; chain.pull(function(buf) { var firstAppend = getEventAppend(function() { src.appendBuffer(init); }, function() { src.removeEventListener('update', firstAppend); src.addEventListener('update', function abortAppend() { src.removeEventListener('update', abortAppend); src.abort(); var end = src.buffered.end(0); var secondAppend = getEventAppend(function() { src.appendBuffer(init); }, function() { runner.checkEq(src.buffered.end(0), end, 'Range end'); runner.succeed(); }); src.addEventListener('update', secondAppend); secondAppend(); }); src.appendBuffer(buf); }); src.addEventListener('update', firstAppend); firstAppend(); }); }); }; }; /** * Test appending segments out of order. */ var createAppendOutOfOrderTest = function(stream, unused_stream) { var test = createConformanceTest( 'Append' + stream.codec + util.MakeCapitalName(stream.mediatype) + 'OutOfOrder', 'MSE (' + stream.codec + ')'); test.prototype.title = 'Test appending segments out of order.'; test.prototype.onsourceopen = function() { var runner = this.runner; var media = this.video; var chain = new FileSource(stream.src, runner.XHRManager, runner.timeouts); var sb = this.ms.addSourceBuffer(stream.mimetype); var unused_sb = this.ms.addSourceBuffer(unused_stream.mimetype); var bufs = []; var i = 0; // Append order of the segments. var appendOrder = [0, 2, 1, 4, 3]; // Number of segments given the append order, since segments get merged. var bufferedLength = [0, 1, 1, 2, 1]; sb.addEventListener('updateend', function() { runner.checkEq(sb.buffered.length, bufferedLength[i], 'Source buffer number'); if (i == 1) { runner.checkGr(sb.buffered.start(0), 0, 'Range start'); } else if (i > 0) { runner.checkEq(sb.buffered.start(0), 0, 'Range start'); } i++; if (i >= bufs.length) { runner.succeed(); } else { sb.appendBuffer(bufs[appendOrder[i]]); } }); chain.init(0, function(buf) { bufs.push(buf); chain.pull(function(buf) { bufs.push(buf); chain.pull(function(buf) { bufs.push(buf); chain.pull(function(buf) { bufs.push(buf); chain.pull(function(buf) { bufs.push(buf); sb.appendBuffer(bufs[0]); }); }); }); }); }); }; }; /** * Test SourceBuffer.buffered get updated correctly after feeding data. */ var createBufferedRangeTest = function(stream, unused_stream) { var test = createConformanceTest( 'BufferedRange' + stream.codec + util.MakeCapitalName(stream.mediatype), 'MSE (' + stream.codec + ')'); test.prototype.title = 'Test SourceBuffer.buffered get updated correctly after feeding data.'; test.prototype.onsourceopen = function() { var runner = this.runner; var media = this.video; var chain = new ResetInit( new FileSource(stream.src, runner.XHRManager, runner.timeouts)); var sb = this.ms.addSourceBuffer(stream.mimetype); var unused_sb = this.ms.addSourceBuffer(unused_stream.mimetype); runner.checkEq(sb.buffered.length, 0, 'Source buffer number'); appendInit(media, sb, chain, 0, function() { runner.checkEq(sb.buffered.length, 0, 'Source buffer number'); appendUntil(runner.timeouts, media, sb, chain, 5, function() { runner.checkEq(sb.buffered.length, 1, 'Source buffer number'); runner.checkEq(sb.buffered.start(0), 0, 'Source buffer number'); runner.checkGE(sb.buffered.end(0), 5, 'Range end'); runner.succeed(); }); }); }; }; /** * Ensure the duration on MediaSource can be set and retrieved sucessfully. */ var createMediaSourceDurationTest = function(videoStream, audioStream) { var test = createConformanceTest('MediaSourceDuration' + videoStream.codec, 'MSE (' + videoStream.codec + ')'); test.prototype.title = 'Test if the duration on MediaSource can be set ' + 'and retrieved sucessfully.'; test.prototype.onsourceopen = function() { var runner = this.runner; var media = this.video; var ms = this.ms; var videoChain = new ResetInit( new FileSource(videoStream.src, runner.XHRManager, runner.timeouts)); var videoSb = this.ms.addSourceBuffer(videoStream.mimetype); var audioSb = this.ms.addSourceBuffer(audioStream.mimetype); var self = this; var onsourceclose = function() { self.log('onsourceclose called'); runner.assert(isNaN(ms.duration)); runner.succeed(); }; var appendVideo = function() { runner.assert(isNaN(media.duration), 'Initial media duration not NaN'); media.play(); appendInit(media, videoSb, videoChain, 0, function() { appendUntil(runner.timeouts, media, videoSb, videoChain, 10, function() { setDuration(5, ms, [videoSb, audioSb], function() { runner.checkEq(ms.duration, 5, 'ms.duration'); runner.checkEq(media.duration, 5, 'media.duration'); runner.checkLE(videoSb.buffered.end(0), 5.1, 'Range end'); videoSb.abort(); videoChain.seek(0); appendInit(media, videoSb, videoChain, 0, function() { appendUntil(runner.timeouts, media, videoSb, videoChain, 10, function() { runner.checkApproxEq(ms.duration, 10, 'ms.duration'); setDuration(5, ms, [videoSb, audioSb], function() { if (videoSb.updating) { runner.fail( 'Source buffer is updating on duration change'); return; } var duration = videoSb.buffered.end(0); ms.endOfStream(); runner.checkApproxEq(ms.duration, duration, 'ms.duration', 0.01); ms.addEventListener('sourceended', function() { runner.checkApproxEq(ms.duration, duration, 'ms.duration', 0.01); runner.checkEq(media.duration, duration, 'media.duration'); ms.addEventListener('sourceclose', onsourceclose); media.removeAttribute('src'); media.load(); }); media.play(); }); }); }); }); }); }); }; var audioXhr = runner.XHRManager.createRequest(audioStream.src, function(e) { audioSb.addEventListener('updateend', function onAudioUpdate() { audioSb.removeEventListener('updateend', onAudioUpdate); appendVideo(); }); var audioContent = audioXhr.getResponseData(); audioSb.appendBuffer(audioContent); }); audioXhr.send(); }; }; /** * Validate media data with overlap is merged into one range. */ var createOverlapTest = function(stream, unused_stream) { var test = createConformanceTest( stream.codec + util.MakeCapitalName(stream.mediatype) + 'WithOverlap', 'MSE (' + stream.codec + ')'); test.prototype.title = 'Test if media data with overlap will be merged into one range.'; test.prototype.onsourceopen = function() { var runner = this.runner; var media = this.video; var chain = new ResetInit( new FileSource(stream.src, runner.XHRManager, runner.timeouts)); var sb = this.ms.addSourceBuffer(stream.mimetype); var unused_sb = this.ms.addSourceBuffer(unused_stream.mimetype); var GAP = 0.1; appendInit(media, sb, chain, 0, function() { chain.pull(function(buf) { sb.addEventListener('update', function appendOuter() { sb.removeEventListener('update', appendOuter); runner.checkEq(sb.buffered.length, 1, 'Source buffer number'); var segmentDuration = sb.buffered.end(0); sb.timestampOffset = segmentDuration - GAP; chain.seek(0); chain.pull(function(buf) { sb.addEventListener('update', function appendMiddle() { sb.removeEventListener('update', appendMiddle); chain.pull(function(buf) { sb.addEventListener('update', function appendInner() { runner.checkEq( sb.buffered.length, 1, 'Source buffer number'); runner.checkApproxEq(sb.buffered.end(0), segmentDuration * 2 - GAP, 'Range end'); runner.succeed(); }); runner.assert(safeAppend(sb, buf), 'safeAppend failed'); }); }); runner.assert(safeAppend(sb, buf), 'safeAppend failed'); }); }); runner.assert(safeAppend(sb, buf), 'safeAppend failed'); }); }); }; }; /** * Validate media data with a gap smaller than an media frame size is merged * into one buffered range. */ var createSmallGapTest = function(stream, unused_stream) { var test = createConformanceTest( stream.codec + util.MakeCapitalName(stream.mediatype) + 'WithSmallGap', 'MSE (' + stream.codec + ')'); test.prototype.title = 'Test if media data with a gap smaller than an media frame size ' + 'will be merged into one buffered range.'; test.prototype.onsourceopen = function() { var runner = this.runner; var media = this.video; var chain = new ResetInit( new FileSource(stream.src, runner.XHRManager, runner.timeouts)); var sb = this.ms.addSourceBuffer(stream.mimetype); var unused_sb = this.ms.addSourceBuffer(unused_stream.mimetype); var GAP = 0.01; appendInit(media, sb, chain, 0, function() { chain.pull(function(buf) { sb.addEventListener('update', function appendOuter() { sb.removeEventListener('update', appendOuter); runner.checkEq(sb.buffered.length, 1, 'Source buffer number'); var segmentDuration = sb.buffered.end(0); sb.timestampOffset = segmentDuration + GAP; chain.seek(0); chain.pull(function(buf) { sb.addEventListener('update', function appendMiddle() { sb.removeEventListener('update', appendMiddle); chain.pull(function(buf) { sb.addEventListener('update', function appendInner() { runner.checkEq( sb.buffered.length, 1, 'Source buffer number'); runner.checkApproxEq(sb.buffered.end(0), segmentDuration * 2 + GAP, 'Range end'); runner.succeed(); }); runner.assert(safeAppend(sb, buf), 'safeAppend failed'); }); }); runner.assert(safeAppend(sb, buf), 'safeAppend failed'); }); }); runner.assert(safeAppend(sb, buf), 'safeAppend failed'); }); }); }; }; /** * Validate media data with a gap larger than an media frame size will not be * merged into one buffered range. */ var createLargeGapTest = function(stream, unused_stream) { var test = createConformanceTest( stream.codec + util.MakeCapitalName(stream.mediatype) + 'WithLargeGap', 'MSE (' + stream.codec + ')'); test.prototype.title = 'Test if media data with a gap larger than an media frame size ' + 'will not be merged into one buffered range.'; test.prototype.onsourceopen = function() { var runner = this.runner; var media = this.video; var chain = new ResetInit( new FileSource(stream.src, runner.XHRManager, runner.timeouts)); var sb = this.ms.addSourceBuffer(stream.mimetype); var unused_sb = this.ms.addSourceBuffer(unused_stream.mimetype); var GAP = 0.3; appendInit(media, sb, chain, 0, function() { chain.pull(function(buf) { sb.addEventListener('update', function appendOuter() { sb.removeEventListener('update', appendOuter); runner.checkEq(sb.buffered.length, 1, 'Source buffer number'); var segmentDuration = sb.buffered.end(0); sb.timestampOffset = segmentDuration + GAP; chain.seek(0); chain.pull(function(buf) { sb.addEventListener('update', function appendMiddle() { sb.removeEventListener('update', appendMiddle); chain.pull(function(buf) { sb.addEventListener('update', function appendInner() { runner.checkEq( sb.buffered.length, 2, 'Source buffer number'); runner.succeed(); }); runner.assert(safeAppend(sb, buf), 'safeAppend failed'); }); }); runner.assert(safeAppend(sb, buf), 'safeAppend failed'); }); }); runner.assert(safeAppend(sb, buf), 'safeAppend failed'); }); }); }; }; /** * Validate we can seek during playing. It also tests if the implementation * properly supports seek operation fired immediately after another seek that * hasn't been completed. */ var createSeekTest = function(videoStream) { var test = createConformanceTest('Seek' + videoStream.codec, 'MSE (' + videoStream.codec + ')'); test.prototype.title = 'Test if we can seek during playing. It' + ' also tests if the implementation properly supports seek operation' + ' fired immediately after another seek that hasn\'t been completed.'; test.prototype.onsourceopen = function() { var runner = this.runner; var media = this.video; var audioStream = Media.AAC.AudioNormal; var videoChain = new ResetInit(new FileSource( videoStream.src, runner.XHRManager, runner.timeouts)); var videoSb = this.ms.addSourceBuffer(videoStream.mimetype); var audioChain = new ResetInit(new FileSource( audioStream.src, runner.XHRManager, runner.timeouts)); var audioSb = this.ms.addSourceBuffer(audioStream.mimetype); var self = this; this.ms.duration = 100000000; // Ensure that we can seek to any position. appendUntil(runner.timeouts, media, videoSb, videoChain, 20, function() { appendUntil(runner.timeouts, media, audioSb, audioChain, 20, function() { self.log('Seek to 17s'); callAfterLoadedMetaData(media, function() { media.currentTime = 17; media.play(); playThrough( runner.timeouts, media, 10, 19, videoSb, videoChain, audioSb, audioChain, function() { runner.checkGE(media.currentTime, 19, 'currentTime'); self.log('Seek to 28s'); media.currentTime = 53; media.currentTime = 58; playThrough( runner.timeouts, media, 10, 60, videoSb, videoChain, audioSb, audioChain, function() { runner.checkGE(media.currentTime, 60, 'currentTime'); self.log('Seek to 7s'); media.currentTime = 0; media.currentTime = 7; videoChain.seek(7, videoSb); audioChain.seek(7, audioSb); playThrough(runner.timeouts, media, 10, 9, videoSb, videoChain, audioSb, audioChain, function() { runner.checkGE(media.currentTime, 9, 'currentTime'); runner.succeed(); }); }); }); }); }); }); }; }; /** * Seek into and out of a buffered region. */ var createBufUnbufSeekTest = function(videoStream) { var test = createConformanceTest('BufUnbufSeek' + videoStream.codec, 'MSE (' + videoStream.codec + ')'); test.prototype.title = 'Seek into and out of a buffered region.'; test.prototype.onsourceopen = function() { var runner = this.runner; var media = this.video; var audioStream = Media.AAC.AudioNormal; var videoSb = this.ms.addSourceBuffer(videoStream.mimetype); var audioSb = this.ms.addSourceBuffer(audioStream.mimetype); var xhr = runner.XHRManager.createRequest(videoStream.src, function() { videoSb.appendBuffer(xhr.getResponseData()); var xhr2 = runner.XHRManager.createRequest(audioStream.src, function() { audioSb.appendBuffer(xhr2.getResponseData()); callAfterLoadedMetaData(media, function() { var N = 30; function loop(i) { if (i > N) { media.currentTime = 1.005; media.addEventListener('timeupdate', function(e) { if (!media.paused && media.currentTime > 3) runner.succeed(); }); return; } media.currentTime = (i++ % 2) * 1.0e6 + 1; runner.timeouts.setTimeout(loop.bind(null, i), 50); } media.play(); media.addEventListener('play', loop.bind(null, 0)); }); }, 0, 100000); xhr2.send(); }, 0, 1000000); this.ms.duration = 100000000; // Ensure that we can seek to any position. xhr.send(); }; }; /** * Ensure we can play properly when there is not enough audio or video data. * The play should resume once src data is appended. */ var createDelayedTest = function(delayed, nonDelayed) { var test = createConformanceTest( 'Delayed' + delayed.codec + util.MakeCapitalName(delayed.mediatype), 'MSE (' + delayed.codec + ')'); test.prototype.title = 'Test if we can play properly when there' + ' is not enough ' + delayed.mediatype + ' data. The play should resume once ' + delayed.mediatype + ' data is appended.'; test.prototype.onsourceopen = function() { var runner = this.runner; var media = this.video; // Chrome allows for 3 seconds of underflow for streams that have audio // but are video starved. // See code.google.com/p/chromium/issues/detail?id=423801 var underflowTime = 0.0; if (delayed.mediatype == 'video') { underflowTime = 3.0; } var chain = new FixedAppendSize( new ResetInit( new FileSource(nonDelayed.src, runner.XHRManager, runner.timeouts) ), 16384); var src = this.ms.addSourceBuffer(nonDelayed.mimetype); var delayedChain = new FixedAppendSize( new ResetInit( new FileSource(delayed.src, runner.XHRManager, runner.timeouts) ), 16384); var delayedSrc = this.ms.addSourceBuffer(delayed.mimetype); var self = this; var ontimeupdate = function(e) { if (!media.paused) { var end = delayedSrc.buffered.end(0); runner.checkLE(media.currentTime, end + 1.0 + underflowTime, 'media.currentTime (' + media.readyState + ')'); } }; appendUntil(runner.timeouts, media, src, chain, 15, function() { appendUntil(runner.timeouts, media, delayedSrc, delayedChain, 8, function() { var end = delayedSrc.buffered.end(0); self.log('Start play when there is only ' + end + ' seconds of ' + test.prototype.desc + ' data.'); media.play(); media.addEventListener('timeupdate', ontimeupdate); waitUntil(runner.timeouts, media, end + 3, function() { runner.checkLE(media.currentTime, end + 1.0 + underflowTime, 'media.currentTime'); runner.checkGr(media.currentTime, end - 1.0 - underflowTime, 'media.currentTime'); runner.succeed(); }); }); }); }; }; // Opus Specific tests. createAppendTest(Media.Opus.SantaHigh, Media.VP9.Video1MB); createAbortTest(Media.Opus.SantaHigh, Media.VP9.Video1MB); createTimestampOffsetTest(Media.Opus.CarLow, Media.VP9.Video1MB); createDurationAfterAppendTest(Media.Opus.CarLow, Media.VP9.Video1MB); createPausedTest(Media.Opus.CarLow); createIncrementalAudioTest(Media.Opus.CarMed); createAppendAudioOffsetTest(Media.Opus.CarMed, Media.Opus.CarHigh); createAppendMultipleInitTest(Media.Opus.CarLow, Media.VP9.Video1MB); createAppendOutOfOrderTest(Media.Opus.CarMed, Media.VP9.Video1MB); createBufferedRangeTest(Media.Opus.CarMed, Media.VP9.Video1MB); createOverlapTest(Media.Opus.CarMed, Media.VP9.Video1MB); createSmallGapTest(Media.Opus.CarMed, Media.VP9.Video1MB); createLargeGapTest(Media.Opus.CarMed, Media.VP9.Video1MB); createDelayedTest(Media.Opus.CarMed, Media.VP9.VideoNormal); // AAC Specific tests. createAppendTest(Media.AAC.Audio1MB, Media.H264.Video1MB); createAbortTest(Media.AAC.Audio1MB, Media.H264.Video1MB); createTimestampOffsetTest(Media.AAC.Audio1MB, Media.H264.Video1MB); createDurationAfterAppendTest(Media.AAC.Audio1MB, Media.H264.Video1MB); createPausedTest(Media.AAC.Audio1MB); createIncrementalAudioTest(Media.AAC.AudioNormal, Media.H264.Video1MB); createAppendAudioOffsetTest(Media.AAC.AudioNormal, Media.AAC.AudioHuge); createAppendMultipleInitTest(Media.AAC.Audio1MB, Media.H264.Video1MB); createAppendOutOfOrderTest(Media.AAC.AudioNormal, Media.H264.Video1MB); createBufferedRangeTest(Media.AAC.AudioNormal, Media.H264.Video1MB); createOverlapTest(Media.AAC.AudioNormal, Media.H264.Video1MB); createSmallGapTest(Media.AAC.AudioNormal, Media.H264.Video1MB); createLargeGapTest(Media.AAC.AudioNormal, Media.H264.Video1MB); createDelayedTest(Media.AAC.AudioNormal, Media.VP9.VideoNormal); // VP9 Specific tests. createAppendTest(Media.VP9.Video1MB, Media.AAC.Audio1MB); createAbortTest(Media.VP9.Video1MB, Media.AAC.Audio1MB); createTimestampOffsetTest(Media.VP9.Video1MB, Media.AAC.Audio1MB); createDASHLatencyTest(Media.VP9.VideoTiny, Media.AAC.Audio1MB); createDurationAfterAppendTest(Media.VP9.Video1MB, Media.AAC.Audio1MB); createPausedTest(Media.VP9.Video1MB); createVideoDimensionTest(Media.VP9.VideoNormal, Media.AAC.AudioNormal); createPlaybackStateTest(Media.VP9.VideoNormal); createPlayPartialSegmentTest(Media.VP9.VideoTiny); createAppendVideoOffsetTest(Media.VP9.VideoNormal, Media.VP9.VideoTiny, Media.AAC.AudioNormal); createAppendMultipleInitTest(Media.VP9.Video1MB, Media.AAC.Audio1MB); createAppendOutOfOrderTest(Media.VP9.VideoNormal, Media.AAC.AudioNormal); createBufferedRangeTest(Media.VP9.VideoNormal, Media.AAC.AudioNormal); createMediaSourceDurationTest(Media.VP9.VideoNormal, Media.AAC.AudioNormal); createOverlapTest(Media.VP9.VideoNormal, Media.AAC.AudioNormal); createSmallGapTest(Media.VP9.VideoNormal, Media.AAC.AudioNormal); createLargeGapTest(Media.VP9.VideoNormal, Media.AAC.AudioNormal); createSeekTest(Media.VP9.VideoNormal); createBufUnbufSeekTest(Media.VP9.VideoNormal); createDelayedTest(Media.VP9.VideoNormal, Media.AAC.AudioNormal); // H264 Specific tests. createAppendTest(Media.H264.Video1MB, Media.AAC.Audio1MB); createAbortTest(Media.H264.Video1MB, Media.AAC.Audio1MB); createTimestampOffsetTest(Media.H264.Video1MB, Media.AAC.Audio1MB); createDASHLatencyTest(Media.H264.VideoTiny, Media.AAC.Audio1MB); createDurationAfterAppendTest(Media.H264.Video1MB, Media.AAC.Audio1MB); createPausedTest(Media.H264.Video1MB); createVideoDimensionTest(Media.H264.VideoNormal, Media.AAC.Audio1MB); createPlaybackStateTest(Media.H264.VideoNormal); createPlayPartialSegmentTest(Media.H264.VideoTiny); createAppendVideoOffsetTest(Media.H264.VideoNormal, Media.H264.VideoTiny, Media.AAC.Audio1MB); createAppendMultipleInitTest(Media.H264.Video1MB, Media.AAC.Audio1MB); createAppendOutOfOrderTest(Media.H264.CarMedium, Media.AAC.Audio1MB); createBufferedRangeTest(Media.H264.VideoNormal, Media.AAC.Audio1MB); createMediaSourceDurationTest(Media.H264.VideoNormal, Media.AAC.Audio1MB); createOverlapTest(Media.H264.VideoNormal, Media.AAC.Audio1MB); createSmallGapTest(Media.H264.VideoNormal, Media.AAC.Audio1MB); createLargeGapTest(Media.H264.VideoNormal, Media.AAC.Audio1MB); createSeekTest(Media.H264.VideoNormal); createBufUnbufSeekTest(Media.H264.VideoNormal); createDelayedTest(Media.H264.VideoNormal, Media.AAC.AudioNormal); /** * Ensure AudioContext is supported. */ var testWAAContext = createConformanceTest('WAAPresence', 'MSE Web Audio API'); testWAAContext.prototype.title = 'Test if AudioContext is supported'; testWAAContext.prototype.start = function(runner, video) { var Ctor = window.AudioContext || window.webkitAudioContext; if (!Ctor) return runner.fail('No AudioContext object available.'); var ctx = new Ctor(); if (!ctx) return runner.fail('Found AudioContext but could not create one'); runner.succeed(); }; /** * Validate AudioContext#createMediaElementSource supports specified video and * audio mimetype. */ var createCreateMESTest = function(audioStream, videoStream) { var test = createConformanceTest( audioStream.codec + '/' + videoStream.codec + 'CreateMediaElementSource', 'MSE Web Audio API (Optional)', false); test.prototype.title = 'Test if AudioContext#createMediaElementSource supports mimetype ' + audioStream.mimetype + '/' + videoStream.mimetype; test.prototype.onsourceopen = function() { var runner = this.runner; var video = this.video; try { var audioSb = this.ms.addSourceBuffer(audioStream.mimetype); var videoSb = this.ms.addSourceBuffer(videoStream.mimetype); } catch (e) { runner.fail(e.message); return; } var Ctor = window.AudioContext || window.webkitAudioContext; var ctx = new Ctor(); var audioXhr = runner.XHRManager.createRequest(audioStream.src, function(e) { var data = audioXhr.getResponseData(); function updateEnd(e) { runner.checkEq(audioSb.buffered.length, 1, 'Source buffer number'); runner.checkEq(audioSb.buffered.start(0), 0, 'Range start'); runner.checkApproxEq( audioSb.buffered.end(0), audioStream.duration, 'Range end'); audioSb.removeEventListener('updateend', updateEnd); video.play(); } audioSb.addEventListener('updateend', updateEnd); audioSb.appendBuffer(data); }); var videoXhr = runner.XHRManager.createRequest(videoStream.src, function(e) { var data = videoXhr.getResponseData(); videoSb.appendBuffer(data); audioXhr.send(); }); videoXhr.send(); video.addEventListener('timeupdate', function onTimeUpdate() { if (!video.paused && video.currentTime >= 5) { video.removeEventListener('timeupdate', onTimeUpdate); try { runner.log('Creating MES'); var source = ctx.createMediaElementSource(video); } catch (e) { runner.fail(e); return; } finally { ctx.close(); } runner.checkNE(source, null, 'MediaElementSource'); runner.succeed(); } }); } } createCreateMESTest(Media.Opus.CarLow, Media.VP9.VideoNormal); createCreateMESTest(Media.AAC.Audio1MB, Media.VP9.VideoNormal); createCreateMESTest(Media.AAC.Audio1MB, Media.H264.VideoNormal); /** * Test media with mismatched frame duration and segment timing. */ var frameTestOnSourceOpen = function() { var runner = this.runner; var media = this.video; var audioStream = Media.AAC.AudioNormal; var videoChain = new FixedAppendSize(new ResetInit( new FileSource(this.filename, runner.XHRManager, runner.timeouts))); var videoSb = this.ms.addSourceBuffer(Media.H264.mimetype); var audioChain = new FixedAppendSize(new ResetInit( new FileSource(audioStream.src, runner.XHRManager, runner.timeouts))); var audioSb = this.ms.addSourceBuffer(audioStream.mimetype); media.play(); playThrough(runner.timeouts, media, 5, 18, videoSb, videoChain, audioSb, audioChain, runner.succeed.bind(runner)); }; var testFrameGaps = createConformanceTest('H264FrameGaps', 'Media'); testFrameGaps.prototype.title = 'Test media with frame durations of 24FPS ' + 'but segment timing corresponding to 23.976FPS'; testFrameGaps.prototype.filename = Media.H264.FrameGap.src; testFrameGaps.prototype.onsourceopen = frameTestOnSourceOpen; var testFrameOverlaps = createConformanceTest('H264FrameOverlaps', 'Media'); testFrameOverlaps.prototype.title = 'Test media with frame durations of ' + '23.976FPS but segment timing corresponding to 24FPS'; testFrameOverlaps.prototype.filename = Media.H264.FrameOverlap.src; testFrameOverlaps.prototype.onsourceopen = frameTestOnSourceOpen; /** * Test different audio codec with 5.1 surround sound (six channel surround * sound audio system). */ var createAudio51Test = function(audioStream) { var test = createConformanceTest(audioStream.codec + '5.1', 'Media'); test.prototype.title = 'Test 5.1-channel ' + audioStream.codec; test.prototype.onsourceopen = function() { var runner = this.runner; var media = this.video; var videoStream = Media.VP9.VideoNormal; var audioSb = this.ms.addSourceBuffer(audioStream.mimetype); var videoSb = this.ms.addSourceBuffer(videoStream.mimetype); var xhr = runner.XHRManager.createRequest(audioStream.src, function(e) { audioSb.appendBuffer(xhr.getResponseData()); var xhr2 = runner.XHRManager.createRequest(videoStream.src, function(e) { videoSb.appendBuffer(xhr2.getResponseData()); media.play(); media.addEventListener('timeupdate', function(e) { if (!media.paused && media.currentTime > 2) { runner.succeed(); } }); }, 0, 3000000); xhr2.send(); }); xhr.send(); } } createAudio51Test(Media.Opus.Audio51); createAudio51Test(Media.AAC.Audio51); /** * Test playback of HE-AAC (High-Efficiency Advanced Audio Coding) with * specified type of SBR signaling. */ var createHeAacTest = function(audioStream) { var test = createConformanceTest('HE-AAC/' + audioStream.get('sbrSignaling') + 'SBR', 'Media'); test.prototype.title = 'Test playback of HE-AAC with ' + audioStream.get('sbrSignaling') + ' SBR signaling.'; test.prototype.onsourceopen = function() { var runner = this.runner; var media = this.video; var ms = this.ms; var videoStream = Media.VP9.Video1MB; var audioSb = this.ms.addSourceBuffer(audioStream.mimetype); var videoSb = this.ms.addSourceBuffer(videoStream.mimetype); var xhr = runner.XHRManager.createRequest(audioStream.src, function(e) { audioSb.addEventListener('update', function() { var xhr2 = runner.XHRManager.createRequest(videoStream.src, function(e) { videoSb.addEventListener('update', function() { ms.endOfStream(); media.addEventListener('ended', function(e) { if (media.currentTime > audioStream.duration + 1) { runner.fail(); } else { runner.checkApproxEq(media.currentTime, audioStream.duration, 'media.currentTime'); runner.succeed(); } }); media.play(); }); videoSb.appendBuffer(xhr2.getResponseData()); }, 0, videoStream.size); xhr2.send(); }); audioSb.appendBuffer(xhr.getResponseData()); }, 0, audioStream.size); xhr.send(); } } createHeAacTest(Media.AAC.AudioLowExplicitHE); createHeAacTest(Media.AAC.AudioLowImplicitHE); return {tests: tests, info: info, fields: fields, viewType: 'default'}; };