| // Copyright © 2018 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). |
| |
| function findSupportedChangeTypeTestTypes(cb) { |
| // Changetype test media metadata. |
| // type: fully specified mime type (and codecs substring if the bytestream |
| // format does not forbid codecs parameter). This is required for use with |
| // isTypeSupported, and if supported, should work with both addSourceBuffer |
| // and changeType (unless implementation has restrictions). |
| // |
| // relaxed_type: possibly ambiguous mime type/subtype without any codecs |
| // substring. This is the same as type minus any codecs substring. |
| // |
| // mime_subtype: the subtype of the mime type in type and relaxed_type. Across |
| // types registered in the bytestream format registry |
| // (https://www.w3.org/TR/mse-byte-stream-format-registry/), this is |
| // currently sufficient to describe uniquely which test media share the same |
| // bytestream format for use in implicit changeType testing. |
| // |
| // is_video: All test media currently is single track. This describes whether |
| // or not the track is video. |
| // |
| // url: Relative location of the test media file. |
| // |
| // The next two items enable more reliable test media splicing test logic that |
| // prevents buffered range gaps at the splice points. |
| // start_time: Some test media begins at a time later than 0.0 seconds. This |
| // is the start time of the media. |
| // keyframe_interval: Some test media contains out-of-order PTS versus DTS |
| // coded frames. In those cases, a constant keyframe_interval is needed to |
| // prevent severely truncating out-of-order GOPs at splice points. |
| let CHANGE_TYPE_MEDIA_LIST = [ |
| { |
| type: 'video/webm; codecs="vp8"', |
| relaxed_type: 'video/webm', |
| mime_subtype: 'webm', |
| is_video: true, |
| url: 'webm/test-v-128k-320x240-24fps-8kfr.webm', |
| start_time: 0.0 |
| // keyframe_interval: N/A since DTS==PTS so overlap-removal of |
| // non-keyframe should not produce a buffered range gap. |
| }, |
| { |
| type: 'video/webm; codecs="vp9"', |
| relaxed_type: 'video/webm', |
| mime_subtype: 'webm', |
| is_video: true, |
| url: 'webm/test-vp9.webm', |
| start_time: 0.0 |
| // keyframe_interval: N/A since DTS==PTS so overlap-removal of |
| // non-keyframe should not produce a buffered range gap. |
| }, |
| { |
| type: 'video/mp4; codecs="avc1.4D4001"', |
| relaxed_type: 'video/mp4', |
| mime_subtype: 'mp4', |
| is_video: true, |
| url: 'mp4/test-v-128k-320x240-24fps-8kfr.mp4', |
| start_time: 0.083333, |
| keyframe_interval: 0.333333 |
| }, |
| { |
| type: 'audio/webm; codecs="vorbis"', |
| relaxed_type: 'audio/webm', |
| mime_subtype: 'webm', |
| is_video: false, |
| url: 'webm/test-a-128k-44100Hz-1ch.webm', |
| start_time: 0.0 |
| // keyframe_interval: N/A since DTS==PTS so overlap-removal of |
| // non-keyframe should not produce a buffered range gap. Also, all frames |
| // in this media are key-frames (it is audio). |
| }, |
| { |
| type: 'audio/mp4; codecs="mp4a.40.2"', |
| relaxed_type: 'audio/mp4', |
| mime_subtype: 'mp4', |
| is_video: false, |
| url: 'mp4/test-a-128k-44100Hz-1ch.mp4', |
| start_time: 0.0 |
| // keyframe_interval: N/A since DTS==PTS so overlap-removal of |
| // non-keyframe should not produce a buffered range gap. Also, all frames |
| // in this media are key-frames (it is audio). |
| }, |
| { |
| type: 'audio/mpeg', |
| relaxed_type: 'audio/mpeg', |
| mime_subtype: 'mpeg', |
| is_video: false, |
| url: 'mp3/sound_5.mp3', |
| start_time: 0.0 |
| // keyframe_interval: N/A since DTS==PTS so overlap-removal of |
| // non-keyframe should not produce a buffered range gap. Also, all frames |
| // in this media are key-frames (it is audio). |
| } |
| ]; |
| |
| let audio_result = []; |
| let video_result = []; |
| |
| for (let i = 0; i < CHANGE_TYPE_MEDIA_LIST.length; ++i) { |
| let media = CHANGE_TYPE_MEDIA_LIST[i]; |
| if (window.MediaSource && MediaSource.isTypeSupported(media.type)) { |
| if (media.is_video === true) { |
| video_result.push(media); |
| } else { |
| audio_result.push(media); |
| } |
| } |
| } |
| |
| cb(audio_result, video_result); |
| } |
| |
| function appendBuffer(test, sourceBuffer, data) { |
| test.expectEvent(sourceBuffer, "update"); |
| test.expectEvent(sourceBuffer, "updateend"); |
| sourceBuffer.appendBuffer(data); |
| } |
| |
| function trimBuffered(test, mediaElement, sourceBuffer, minimumPreviousDuration, newDuration, skip_duration_prechecks) { |
| if (!skip_duration_prechecks) { |
| assert_less_than(newDuration, minimumPreviousDuration); |
| assert_less_than(minimumPreviousDuration, mediaElement.duration); |
| } |
| test.expectEvent(sourceBuffer, "update"); |
| test.expectEvent(sourceBuffer, "updateend"); |
| sourceBuffer.remove(newDuration, Infinity); |
| } |
| |
| function trimDuration(test, mediaElement, mediaSource, newDuration, skip_duration_prechecks) { |
| if (!skip_duration_prechecks) { |
| assert_less_than(newDuration, mediaElement.duration); |
| } |
| test.expectEvent(mediaElement, "durationchange"); |
| mediaSource.duration = newDuration; |
| } |
| |
| function runChangeTypeTest(test, mediaElement, mediaSource, metadataA, typeA, dataA, metadataB, typeB, dataB, |
| implicit_changetype, negative_test) { |
| // Some streams, like the MP4 video stream, contain presentation times for |
| // frames out of order versus their decode times. If we overlap-append the |
| // latter part of such a stream's GOP presentation interval, a significant |
| // portion of decode-dependent non-keyframes with earlier presentation |
| // intervals could be removed and a presentation time buffered range gap could |
| // be introduced. Therefore, we test overlap appends with the overlaps |
| // occurring very near to a keyframe's presentation time to reduce the |
| // possibility of such a gap. None of the test media is SAP-Type-2, so we |
| // don't take any extra care to avoid gaps that may occur when |
| // splice-overlapping such GOP sequences that aren't SAP-Type-1. |
| // TODO(wolenetz): https://github.com/w3c/media-source/issues/160 could |
| // greatly simplify this problem by allowing us play through these small gaps. |
| // |
| // typeA and typeB may be underspecified for use with isTypeSupported, but |
| // this helper does not use isTypeSupported. typeA and typeB must work (even |
| // if missing codec specific substrings) with addSourceBuffer (just typeA) and |
| // changeType (both typeA and typeB). |
| // |
| // See also mediaSourceChangeTypeTest's options argument for the meanings of |
| // implicit_changetype and negative_test. |
| |
| function findSafeOffset(targetTime, overlappedMediaMetadata, overlappedStartTime, overlappingMediaMetadata) { |
| assert_greater_than_equal(targetTime, overlappedStartTime); |
| |
| let offset = targetTime; |
| if ("start_time" in overlappingMediaMetadata) { |
| offset -= overlappingMediaMetadata["start_time"]; |
| } |
| |
| // If the media being overlapped is not out-of-order decode, then we can |
| // safely use the supplied times. |
| if (!("keyframe_interval" in overlappedMediaMetadata)) { |
| return { "offset": offset, "adjustedTime": targetTime }; |
| } |
| |
| // Otherwise, we're overlapping media that needs care to prevent introducing |
| // a gap. Adjust offset and adjustedTime to make the overlapping media start |
| // at the next overlapped media keyframe at or after targetTime. |
| let gopsToRetain = Math.ceil((targetTime - overlappedStartTime) / overlappedMediaMetadata["keyframe_interval"]); |
| let adjustedTime = overlappedStartTime + gopsToRetain * overlappedMediaMetadata["keyframe_interval"]; |
| |
| assert_greater_than_equal(adjustedTime, targetTime); |
| offset += adjustedTime - targetTime; |
| return { "offset": offset, "adjustedTime": adjustedTime }; |
| } |
| |
| // Note, none of the current negative changeType tests should fail the initial addSourceBuffer. |
| let sourceBuffer = mediaSource.addSourceBuffer(typeA); |
| |
| // Add error event listeners to sourceBuffer. The caller of this helper may |
| // also have installed error event listeners on mediaElement. |
| if (negative_test) { |
| sourceBuffer.addEventListener("error", test.step_func_done()); |
| } else { |
| sourceBuffer.addEventListener("error", test.unreached_func("Unexpected event 'error'")); |
| } |
| |
| // In either negative test or not, the first appendBuffer should succeed. |
| appendBuffer(test, sourceBuffer, dataA); |
| let lastStart = metadataA["start_time"]; |
| if (lastStart == null) { |
| lastStart = 0.0; |
| } |
| |
| // changeType A->B and append the first media of B effectively at 0.5 seconds |
| // (or at the first keyframe in A at or after 0.5 seconds if it has |
| // keyframe_interval defined). |
| test.waitForExpectedEvents(() => { |
| let safeOffset = findSafeOffset(0.5, metadataA, lastStart, metadataB); |
| lastStart = safeOffset["adjustedTime"]; |
| if (!implicit_changetype) { |
| try { sourceBuffer.changeType(typeB); } catch(err) { |
| if (negative_test) |
| test.done(); |
| else |
| throw err; |
| } |
| } |
| sourceBuffer.timestampOffset = safeOffset["offset"]; |
| appendBuffer(test, sourceBuffer, dataB); |
| }); |
| |
| // changeType B->B and append B starting at 1.0 seconds (or at the first |
| // keyframe in B at or after 1.0 seconds if it has keyframe_interval defined). |
| test.waitForExpectedEvents(() => { |
| assert_less_than(lastStart, 1.0); |
| let safeOffset = findSafeOffset(1.0, metadataB, lastStart, metadataB); |
| lastStart = safeOffset["adjustedTime"]; |
| if (!implicit_changetype) { |
| try { sourceBuffer.changeType(typeB); } catch(err) { |
| if (negative_test) |
| test.done(); |
| else |
| throw err; |
| } |
| } |
| sourceBuffer.timestampOffset = safeOffset["offset"]; |
| appendBuffer(test, sourceBuffer, dataB); |
| }); |
| |
| // changeType B->A and append A starting at 1.5 seconds (or at the first |
| // keyframe in B at or after 1.5 seconds if it has keyframe_interval defined). |
| test.waitForExpectedEvents(() => { |
| assert_less_than(lastStart, 1.5); |
| let safeOffset = findSafeOffset(1.5, metadataB, lastStart, metadataA); |
| // Retain the previous lastStart because the next block will append data |
| // which begins between that start time and this block's start time. |
| if (!implicit_changetype) { |
| try { sourceBuffer.changeType(typeA); } catch(err) { |
| if (negative_test) |
| test.done(); |
| else |
| throw err; |
| } |
| } |
| sourceBuffer.timestampOffset = safeOffset["offset"]; |
| appendBuffer(test, sourceBuffer, dataA); |
| }); |
| |
| // changeType A->A and append A starting at 1.3 seconds (or at the first |
| // keyframe in B at or after 1.3 seconds if it has keyframe_interval defined). |
| test.waitForExpectedEvents(() => { |
| assert_less_than(lastStart, 1.3); |
| // Our next append will begin by overlapping some of metadataB, then some of |
| // metadataA. |
| let safeOffset = findSafeOffset(1.3, metadataB, lastStart, metadataA); |
| if (!implicit_changetype) { |
| try { sourceBuffer.changeType(typeA); } catch(err) { |
| if (negative_test) |
| test.done(); |
| else |
| throw err; |
| } |
| } |
| sourceBuffer.timestampOffset = safeOffset["offset"]; |
| appendBuffer(test, sourceBuffer, dataA); |
| }); |
| |
| // Trim duration to 2 seconds, then play through to end. |
| test.waitForExpectedEvents(() => { |
| // If negative testing, then skip fragile assertions. |
| trimBuffered(test, mediaElement, sourceBuffer, 2.1, 2, negative_test); |
| }); |
| |
| test.waitForExpectedEvents(() => { |
| // If negative testing, then skip fragile assertions. |
| trimDuration(test, mediaElement, mediaSource, 2, negative_test); |
| }); |
| |
| test.waitForExpectedEvents(() => { |
| assert_equals(mediaElement.currentTime, 0); |
| test.expectEvent(mediaSource, "sourceended"); |
| test.expectEvent(mediaElement, "play"); |
| test.expectEvent(mediaElement, "ended"); |
| mediaSource.endOfStream(); |
| mediaElement.play(); |
| }); |
| |
| test.waitForExpectedEvents(() => { |
| if (negative_test) |
| assert_unreached("Received 'ended' while negative testing."); |
| else |
| test.done(); |
| }); |
| } |
| |
| // options.use_relaxed_mime_types : boolean (defaults to false). |
| // If true, the initial addSourceBuffer and any changeType calls will use the |
| // relaxed_type in metadataA and metadataB instead of the full type in the |
| // metadata. |
| // options.implicit_changetype : boolean (defaults to false). |
| // If true, no changeType calls will be used. Instead, the test media files |
| // are expected to begin with an initialization segment and end at a segment |
| // boundary (no abort() call is issued by this test to reset the |
| // SourceBuffer's parser). |
| // options.negative_test : boolean (defaults to false). |
| // If true, the test is expected to hit error amongst one of the following |
| // areas: addSourceBuffer, appendBuffer (synchronous or asynchronous error), |
| // changeType, playback to end of buffered media. If 'ended' is received |
| // without error otherwise already occurring, then fail the test. Otherwise, |
| // pass the test on receipt of error. Continue to consider timeouts as test |
| // failures. |
| function mediaSourceChangeTypeTest(metadataA, metadataB, description, options = {}) { |
| mediasource_test((test, mediaElement, mediaSource) => { |
| let typeA = metadataA.type; |
| let typeB = metadataB.type; |
| if (options.hasOwnProperty("use_relaxed_mime_types") && |
| options.use_relaxed_mime_types === true) { |
| typeA = metadataA.relaxed_type; |
| typeB = metadataB.relaxed_type; |
| } |
| let implicit_changetype = options.hasOwnProperty("implicit_changetype") && |
| options.implicit_changetype === true; |
| let negative_test = options.hasOwnProperty("negative_test") && |
| options.negative_test === true; |
| |
| mediaElement.pause(); |
| if (negative_test) { |
| mediaElement.addEventListener("error", test.step_func_done()); |
| } else { |
| mediaElement.addEventListener("error", |
| test.unreached_func("Unexpected event 'error'")); |
| } |
| MediaSourceUtil.loadBinaryData(test, metadataA.url, (dataA) => { |
| MediaSourceUtil.loadBinaryData(test, metadataB.url, (dataB) => { |
| runChangeTypeTest( |
| test, mediaElement, mediaSource, |
| metadataA, typeA, dataA, metadataB, typeB, dataB, |
| implicit_changetype, negative_test); |
| }); |
| }); |
| }, description); |
| } |