From 2ec64442e2b43fdfdf5a20f63d4367dc3d531892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Velad=20Galv=C3=A1n?= Date: Mon, 3 Jun 2024 10:10:39 +0200 Subject: [PATCH] feat(HLS): Add support for EXT-X-DATERANGE (#6718) Closes https://github.com/shaka-project/shaka-player/issues/3523 --- externs/shaka/manifest_parser.js | 7 +- externs/shaka/player.js | 6 +- lib/hls/hls_parser.js | 151 ++++++++++++++- lib/hls/manifest_text_parser.js | 1 + lib/offline/storage.js | 1 + lib/player.js | 14 +- .../dash_parser_content_protection_unit.js | 1 + test/dash/dash_parser_live_unit.js | 1 + test/dash/dash_parser_manifest_unit.js | 1 + test/dash/dash_parser_patch_unit.js | 1 + test/dash/dash_parser_segment_base_unit.js | 1 + test/dash/dash_parser_segment_list_unit.js | 1 + .../dash/dash_parser_segment_template_unit.js | 1 + test/hls/hls_live_unit.js | 1 + test/hls/hls_parser_unit.js | 182 ++++++++++++++++++ test/mss/mss_parser_unit.js | 1 + test/test/util/dash_parser_util.js | 2 + test/test/util/mss_parser_util.js | 2 + test/util/content_steering_manager_unit.js | 1 + 19 files changed, 365 insertions(+), 11 deletions(-) diff --git a/externs/shaka/manifest_parser.js b/externs/shaka/manifest_parser.js index 66b8a7896..b8bd7f7ba 100644 --- a/externs/shaka/manifest_parser.js +++ b/externs/shaka/manifest_parser.js @@ -128,7 +128,9 @@ shaka.extern.ManifestParser = class { * updateDuration: function(), * newDrmInfo: function(shaka.extern.Stream), * onManifestUpdated: function(), - * getBandwidthEstimate: function():number + * getBandwidthEstimate: function():number, + * onMetadata: function(string, number, ?number, + * !Array.) * }} * * @description @@ -167,6 +169,9 @@ shaka.extern.ManifestParser = class { * Should be called when the manifest is updated. * @property {function():number} getBandwidthEstimate * Get the estimated bandwidth in bits per second. + * @property {function(string, number, ?number, + * !Array.)} onMetadata + * Called when an metadata is found in the manifest. * @exportDoc */ shaka.extern.ManifestParser.PlayerInterface; diff --git a/externs/shaka/player.js b/externs/shaka/player.js index 63b6f23ad..c5db8b237 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -1071,9 +1071,9 @@ shaka.extern.DashManifestConfiguration; * @property {boolean} ignoreManifestProgramDateTime * If true, the HLS parser will ignore the * EXT-X-PROGRAM-DATE-TIME tags in the manifest and use media - * sequence numbers instead. - * Meant for streams where EXT-X-PROGRAM-DATE-TIME is incorrect - * or malformed. + * sequence numbers instead. It also causes EXT-X-DATERANGE tags to be + * ignored. Meant for streams where EXT-X-PROGRAM-DATE-TIME is + * incorrect or malformed. * Defaults to false. * @property {!Array.} ignoreManifestProgramDateTimeForTypes * An array of strings representing types for which diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 1bf5facae..47f648cc2 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -247,6 +247,9 @@ shaka.hls.HlsParser = class { /** @private {boolean} */ this.needsClosedCaptionsDetection_ = true; + + /** @private {Set.} */ + this.dateRangeIdsEmitted_ = new Set(); } @@ -316,6 +319,7 @@ shaka.hls.HlsParser = class { this.aesKeyMap_.clear(); this.identityKeyMap_.clear(); this.identityKidMap_.clear(); + this.dateRangeIdsEmitted_.clear(); if (this.contentSteeringManager_) { this.contentSteeringManager_.destroy(); @@ -760,6 +764,9 @@ shaka.hls.HlsParser = class { return [uri]; }; + /** @type {?string} */ + let mediaPlaylistType = null; + // Parsing a media playlist results in a single-variant stream. if (playlist.type == shaka.hls.PlaylistType.MEDIA) { this.needsClosedCaptionsDetection_ = false; @@ -775,7 +782,7 @@ shaka.hls.HlsParser = class { // find from the master playlist (e.g. from values on EXT-X-MEDIA tags). const basicInfo = await this.getMediaPlaylistBasicInfo_( playlist, getUris, mediaVariables); - const type = basicInfo.type; + mediaPlaylistType = basicInfo.type; const mimeType = basicInfo.mimeType; const codecs = basicInfo.codecs; const languageValue = basicInfo.language; @@ -800,11 +807,12 @@ shaka.hls.HlsParser = class { // Make the stream info, with those values. const streamInfo = await this.convertParsedPlaylistIntoStreamInfo_( this.globalId_++, mediaVariables, playlist, getUris, uri, codecs, - type, languageValue, primary, name, channelsCount, closedCaptions, - characteristics, forced, sampleRate, spatialAudio, mimeType); + mediaPlaylistType, languageValue, primary, name, channelsCount, + closedCaptions, characteristics, forced, sampleRate, spatialAudio, + mimeType); this.uriToStreamInfosMap_.set(uri, streamInfo); - if (type == 'video') { + if (mediaPlaylistType == 'video') { this.addVideoAttributes_(streamInfo.stream, width, height, /* frameRate= */ null, videoRange, /* videoLayout= */ null, colorGamut); @@ -816,8 +824,8 @@ shaka.hls.HlsParser = class { language: this.getLanguage_(languageValue), disabledUntilTime: 0, primary: true, - audio: type == 'audio' ? streamInfo.stream : null, - video: type == 'video' ? streamInfo.stream : null, + audio: mediaPlaylistType == 'audio' ? streamInfo.stream : null, + video: mediaPlaylistType == 'video' ? streamInfo.stream : null, bandwidth: streamInfo.stream.bandwidth || 0, allowedByApplication: true, allowedByKeySystem: true, @@ -880,6 +888,10 @@ shaka.hls.HlsParser = class { const streamInfos = Array.from(this.uriToStreamInfosMap_.values()); this.finalizeStreams_(streamInfos); this.determineDuration_(); + + goog.asserts.assert(mediaPlaylistType, + 'mediaPlaylistType should be non-null'); + this.processDateRangeTags_(playlist.tags, mediaPlaylistType); } this.manifest_ = { @@ -2436,6 +2448,8 @@ shaka.hls.HlsParser = class { this.finalizeStreams_([streamInfo]); } } + + this.processDateRangeTags_(playlist.tags, stream.type); }; /** @type {Promise} */ @@ -3616,6 +3630,131 @@ shaka.hls.HlsParser = class { return [startByte, endByte]; } + /** + * @param {!Array.} tags + * @param {string} contentType + * @private + */ + processDateRangeTags_(tags, contentType) { + const initialProgramDateTime = + this.presentationTimeline_.getInitialProgramDateTime(); + if (!initialProgramDateTime || + this.ignoreManifestProgramDateTimeFor_(contentType)) { + return; + } + let dateRangeTags = + shaka.hls.Utils.filterTagsByName(tags, 'EXT-X-DATERANGE'); + dateRangeTags = dateRangeTags.sort((a, b) => { + const aStartDateValue = a.getRequiredAttrValue('START-DATE'); + const bStartDateValue = b.getRequiredAttrValue('START-DATE'); + if (aStartDateValue < bStartDateValue) { + return -1; + } + if (aStartDateValue > bStartDateValue) { + return 1; + } + return 0; + }); + for (let i = 0; i < dateRangeTags.length; i++) { + const tag = dateRangeTags[i]; + const id = tag.getRequiredAttrValue('ID'); + if (this.dateRangeIdsEmitted_.has(id)) { + continue; + } + const startDateValue = tag.getRequiredAttrValue('START-DATE'); + const startDate = shaka.util.TXml.parseDate(startDateValue); + if (isNaN(startDate)) { + // Invalid START-DATE + continue; + } + goog.asserts.assert(startDate != null, 'Start date should not be null!'); + const startTime = Math.max(0, startDate - initialProgramDateTime); + + let endTime = null; + const endDateValue = tag.getAttributeValue('END-DATE'); + if (endDateValue) { + const endDate = shaka.util.TXml.parseDate(endDateValue); + if (!isNaN(endDate)) { + goog.asserts.assert(endDate != null, 'End date should not be null!'); + endTime = Math.max(0, endDate - initialProgramDateTime); + } + } + if (endTime == null) { + const durationValue = tag.getAttributeValue('DURATION'); + if (durationValue) { + const duration = parseFloat(durationValue); + if (!isNaN(duration)) { + endTime = startTime + duration; + } + } + } + const type = tag.getAttributeValue('CLASS') || 'com.apple.quicktime.HLS'; + + const endOnNext = tag.getAttributeValue('END-ON-NEXT') == 'YES'; + if (endTime == null && endOnNext) { + for (let j = i + 1; j < dateRangeTags.length; j++) { + const otherDateRangeType = + dateRangeTags[j].getAttributeValue('CLASS') || + 'com.apple.quicktime.HLS'; + if (type != otherDateRangeType) { + continue; + } + const otherDateRangeStartDateValue = + dateRangeTags[j].getRequiredAttrValue('START-DATE'); + const otherDateRangeStartDate = + shaka.util.TXml.parseDate(otherDateRangeStartDateValue); + if (isNaN(otherDateRangeStartDate)) { + // Invalid START-DATE + continue; + } + if (otherDateRangeStartDate && otherDateRangeStartDate > startDate) { + endTime = Math.max(0, + otherDateRangeStartDate - initialProgramDateTime); + break; + } + } + if (endTime == null) { + // Since we cannot know when it ends, we omit it for now and in the + // future with an update we will be able to have more information. + continue; + } + } + + // Exclude these attributes from the metadata since they already go into + // other fields (eg: startTime or endTime) or are not necessary.. + const excludedAttributes = [ + 'ID', + 'CLASS', + 'START-DATE', + 'END-DATE', + 'DURATION', + 'END-ON-NEXT', + ]; + + /* @type {!Array.} */ + const values = []; + for (const attribute of tag.attributes) { + if (excludedAttributes.includes(attribute.name)) { + continue; + } + const metadataFrame = { + key: attribute.name, + description: '', + data: attribute.value, + mimeType: null, + pictureType: null, + }; + values.push(metadataFrame); + } + + if (values.length) { + this.playerInterface_.onMetadata(type, startTime, endTime, values); + } + + this.dateRangeIdsEmitted_.add(id); + } + } + /** * Parses shaka.hls.Segment objects into shaka.media.SegmentReferences and * get the bandwidth necessary for this segments If it's defined in the diff --git a/lib/hls/manifest_text_parser.js b/lib/hls/manifest_text_parser.js index a106d08b8..0f21539ad 100644 --- a/lib/hls/manifest_text_parser.js +++ b/lib/hls/manifest_text_parser.js @@ -295,6 +295,7 @@ shaka.hls.ManifestTextParser.MEDIA_PLAYLIST_TAGS = [ 'EXT-X-SERVER-CONTROL', 'EXT-X-SKIP', 'EXT-X-PART-INF', + 'EXT-X-DATERANGE', ]; diff --git a/lib/offline/storage.js b/lib/offline/storage.js index c9a01b29d..977c67afe 100644 --- a/lib/offline/storage.js +++ b/lib/offline/storage.js @@ -1178,6 +1178,7 @@ shaka.offline.Storage = class { newDrmInfo: (stream) => {}, onManifestUpdated: () => {}, getBandwidthEstimate: () => config.abr.defaultBandwidthEstimate, + onMetadata: () => {}, }; parser.configure(config.manifest); diff --git a/lib/player.js b/lib/player.js index a081f2136..95bc490d7 100644 --- a/lib/player.js +++ b/lib/player.js @@ -331,7 +331,7 @@ goog.requireType('shaka.media.PresentationTimeline'); * The time that describes the end of the range of the metadata to which * the cue applies. * @property {string} metadataType - * Type of metadata. Eg: org.id3 or org.mp4ra + * Type of metadata. Eg: 'org.id3' or 'com.apple.quicktime.HLS' * @property {shaka.extern.MetadataFrame} payload * The metadata itself * @exportDoc @@ -2034,6 +2034,18 @@ shaka.Player = class extends shaka.util.FakeEventTarget { }); }, getBandwidthEstimate: () => this.abrManager_.getBandwidthEstimate(), + onMetadata: (type, startTime, endTime, values) => { + let metadataType = type; + if (type == 'com.apple.hls.interstitial') { + metadataType = 'com.apple.quicktime.HLS'; + } + for (const payload of values) { + preloadManager.addQueuedOperation(false, () => { + this.dispatchMetadataEvent_( + startTime, endTime, metadataType, payload); + }); + } + }, }; const regionTimeline = new shaka.media.RegionTimeline(() => this.seekRange()); diff --git a/test/dash/dash_parser_content_protection_unit.js b/test/dash/dash_parser_content_protection_unit.js index 62492b15a..ffa2f65a8 100644 --- a/test/dash/dash_parser_content_protection_unit.js +++ b/test/dash/dash_parser_content_protection_unit.js @@ -46,6 +46,7 @@ describe('DashParser ContentProtection', () => { newDrmInfo: (stream) => {}, onManifestUpdated: () => {}, getBandwidthEstimate: () => 1e6, + onMetadata: () => {}, }; const actual = await dashParser.start( diff --git a/test/dash/dash_parser_live_unit.js b/test/dash/dash_parser_live_unit.js index 13cb10951..1138739b4 100644 --- a/test/dash/dash_parser_live_unit.js +++ b/test/dash/dash_parser_live_unit.js @@ -38,6 +38,7 @@ describe('DashParser Live', () => { newDrmInfo: (stream) => {}, onManifestUpdated: () => {}, getBandwidthEstimate: () => 1e6, + onMetadata: () => {}, }; }); diff --git a/test/dash/dash_parser_manifest_unit.js b/test/dash/dash_parser_manifest_unit.js index d7679fdf4..9b1c81f98 100644 --- a/test/dash/dash_parser_manifest_unit.js +++ b/test/dash/dash_parser_manifest_unit.js @@ -56,6 +56,7 @@ describe('DashParser Manifest', () => { newDrmInfo: (stream) => {}, onManifestUpdated: () => {}, getBandwidthEstimate: () => 1e6, + onMetadata: () => {}, }; }); diff --git a/test/dash/dash_parser_patch_unit.js b/test/dash/dash_parser_patch_unit.js index d4a5c75e3..16df28884 100644 --- a/test/dash/dash_parser_patch_unit.js +++ b/test/dash/dash_parser_patch_unit.js @@ -46,6 +46,7 @@ describe('DashParser Patch', () => { newDrmInfo: (stream) => {}, onManifestUpdated: () => {}, getBandwidthEstimate: () => 1e6, + onMetadata: () => {}, }; Date.now = () => publishTime.getTime() + 10; diff --git a/test/dash/dash_parser_segment_base_unit.js b/test/dash/dash_parser_segment_base_unit.js index d87165e37..1ff2e4d69 100644 --- a/test/dash/dash_parser_segment_base_unit.js +++ b/test/dash/dash_parser_segment_base_unit.js @@ -42,6 +42,7 @@ describe('DashParser SegmentBase', () => { newDrmInfo: (stream) => {}, onManifestUpdated: () => {}, getBandwidthEstimate: () => 1e6, + onMetadata: () => {}, }; }); diff --git a/test/dash/dash_parser_segment_list_unit.js b/test/dash/dash_parser_segment_list_unit.js index 5bb26754e..bea28e646 100644 --- a/test/dash/dash_parser_segment_list_unit.js +++ b/test/dash/dash_parser_segment_list_unit.js @@ -352,6 +352,7 @@ describe('DashParser SegmentList', () => { newDrmInfo: (stream) => {}, onManifestUpdated: () => {}, getBandwidthEstimate: () => 1e6, + onMetadata: () => {}, }; try { const manifest = await dashParser.start('dummy://foo', playerInterface); diff --git a/test/dash/dash_parser_segment_template_unit.js b/test/dash/dash_parser_segment_template_unit.js index 14ab181fd..b4600a591 100644 --- a/test/dash/dash_parser_segment_template_unit.js +++ b/test/dash/dash_parser_segment_template_unit.js @@ -51,6 +51,7 @@ describe('DashParser SegmentTemplate', () => { newDrmInfo: (stream) => {}, onManifestUpdated: () => {}, getBandwidthEstimate: () => 1e6, + onMetadata: () => {}, }; }); diff --git a/test/hls/hls_live_unit.js b/test/hls/hls_live_unit.js index da487bf68..51bd98fa5 100644 --- a/test/hls/hls_live_unit.js +++ b/test/hls/hls_live_unit.js @@ -81,6 +81,7 @@ describe('HlsParser live', () => { newDrmInfo: (stream) => {}, onManifestUpdated: () => {}, getBandwidthEstimate: () => 1e6, + onMetadata: () => {}, }; parser = new shaka.hls.HlsParser(); diff --git a/test/hls/hls_parser_unit.js b/test/hls/hls_parser_unit.js index c13293aa0..c7f42d001 100644 --- a/test/hls/hls_parser_unit.js +++ b/test/hls/hls_parser_unit.js @@ -29,6 +29,8 @@ describe('HlsParser', () => { let onEventSpy; /** @type {!jasmine.Spy} */ let newDrmInfoSpy; + /** @type {!jasmine.Spy} */ + let onMetadataSpy; /** @type {shaka.extern.ManifestParser.PlayerInterface} */ let playerInterface; /** @type {shaka.extern.ManifestConfiguration} */ @@ -76,6 +78,7 @@ describe('HlsParser', () => { sequenceMode = config.hls.sequenceMode; onEventSpy = jasmine.createSpy('onEvent'); newDrmInfoSpy = jasmine.createSpy('newDrmInfo'); + onMetadataSpy = jasmine.createSpy('onMetadata'); playerInterface = { modifyManifestRequest: (request, manifestInfo) => {}, modifySegmentRequest: (request, segmentInfo) => {}, @@ -92,6 +95,7 @@ describe('HlsParser', () => { newDrmInfo: shaka.test.Util.spyFunc(newDrmInfoSpy), onManifestUpdated: () => {}, getBandwidthEstimate: () => 1e6, + onMetadata: shaka.test.Util.spyFunc(onMetadataSpy), }; parser = new shaka.hls.HlsParser(); @@ -5498,4 +5502,182 @@ describe('HlsParser', () => { expect(videoUri0).toBe('http://master/b/main.mp4'); expect(videoUri1).toBe('http://master/a/main.mp4'); }); + + describe('EXT-X-DATERANGE', () => { + it('supports multiples tags', async () => { + const mediaPlaylist = [ + '#EXTM3U\n', + '#EXT-X-TARGETDURATION:5\n', + '#EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:00.00Z\n', + '#EXTINF:5,\n', + 'video1.ts\n', + '#EXT-X-DATERANGE:ID="0",START-DATE="2000-01-01T00:00:00.00Z",', + 'DURATION=1,X-SHAKA="FOREVER"\n', + '#EXT-X-DATERANGE:ID="1",START-DATE="2000-01-01T00:00:05.00Z",', + 'END-DATE="2000-01-01T00:00:06.00Z",X-SHAKA="FOREVER"\n', + '#EXT-X-DATERANGE:ID="2",START-DATE="2000-01-01T00:00:10.00Z",', + 'X-SHAKA="FOREVER"\n', + ].join(''); + + fakeNetEngine + .setResponseText('test:/master', mediaPlaylist) + .setResponseValue('test:/video1.ts', tsSegmentData); + + await parser.start('test:/master', playerInterface); + + const metadataType = 'com.apple.quicktime.HLS'; + const value = { + key: 'X-SHAKA', + data: 'FOREVER', + }; + expect(onMetadataSpy).toHaveBeenCalledTimes(3); + expect(onMetadataSpy).toHaveBeenCalledWith(metadataType, 0, 1, + [jasmine.objectContaining(value)]); + expect(onMetadataSpy).toHaveBeenCalledWith(metadataType, 5, 6, + [jasmine.objectContaining(value)]); + expect(onMetadataSpy).toHaveBeenCalledWith(metadataType, 10, null, + [jasmine.objectContaining(value)]); + }); + + it('supports END-ON-NEXT', async () => { + const mediaPlaylist = [ + '#EXTM3U\n', + '#EXT-X-TARGETDURATION:5\n', + '#EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:00.00Z\n', + '#EXTINF:5,\n', + 'video1.ts\n', + '#EXT-X-DATERANGE:ID="0",START-DATE="2000-01-01T00:00:00.00Z",', + 'END-ON-NEXT=YES,X-SHAKA="FOREVER"\n', + '#EXT-X-DATERANGE:ID="1",START-DATE="2000-01-01T00:00:05.00Z",', + 'END-ON-NEXT=YES,X-SHAKA="FOREVER"\n', + ].join(''); + + fakeNetEngine + .setResponseText('test:/master', mediaPlaylist) + .setResponseValue('test:/video1.ts', tsSegmentData); + + await parser.start('test:/master', playerInterface); + + const metadataType = 'com.apple.quicktime.HLS'; + const value = { + key: 'X-SHAKA', + data: 'FOREVER', + }; + expect(onMetadataSpy).toHaveBeenCalledTimes(1); + expect(onMetadataSpy).toHaveBeenCalledWith(metadataType, 0, 5, + [jasmine.objectContaining(value)]); + }); + + it('skip duplicate IDs', async () => { + const mediaPlaylist = [ + '#EXTM3U\n', + '#EXT-X-TARGETDURATION:5\n', + '#EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:00.00Z\n', + '#EXTINF:5,\n', + 'video1.ts\n', + '#EXT-X-DATERANGE:ID="0",START-DATE="2000-01-01T00:00:00.00Z",', + 'DURATION=1,X-SHAKA="FOREVER"\n', + '#EXT-X-DATERANGE:ID="0",START-DATE="2000-01-01T00:00:00.00Z",', + 'DURATION=1,X-SHAKA="FOREVER"\n', + '#EXT-X-DATERANGE:ID="0",START-DATE="2000-01-01T00:00:00.00Z",', + 'DURATION=1,X-SHAKA="FOREVER"\n', + ].join(''); + + fakeNetEngine + .setResponseText('test:/master', mediaPlaylist) + .setResponseValue('test:/video1.ts', tsSegmentData); + + await parser.start('test:/master', playerInterface); + + const metadataType = 'com.apple.quicktime.HLS'; + const value = { + key: 'X-SHAKA', + data: 'FOREVER', + }; + expect(onMetadataSpy).toHaveBeenCalledTimes(1); + expect(onMetadataSpy).toHaveBeenCalledWith(metadataType, 0, 1, + [jasmine.objectContaining(value)]); + }); + + it('with no EXT-X-PROGRAM-DATE-TIME', async () => { + const mediaPlaylist = [ + '#EXTM3U\n', + '#EXT-X-TARGETDURATION:5\n', + '#EXTINF:5,\n', + 'video1.ts\n', + '#EXT-X-DATERANGE:ID="0",START-DATE="2000-01-01T00:00:00.00Z",', + 'DURATION=1,X-SHAKA="FOREVER"\n', + ].join(''); + + fakeNetEngine + .setResponseText('test:/master', mediaPlaylist) + .setResponseValue('test:/video1.ts', tsSegmentData); + + await parser.start('test:/master', playerInterface); + + expect(onMetadataSpy).not.toHaveBeenCalled(); + }); + + it('ignores without useful value', async () => { + const mediaPlaylist = [ + '#EXTM3U\n', + '#EXT-X-TARGETDURATION:5\n', + '#EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:00.00Z\n', + '#EXTINF:5,\n', + 'video1.ts\n', + '#EXT-X-DATERANGE:ID="0",START-DATE="2000-01-01T00:00:00.00Z",', + 'DURATION=1\n', + ].join(''); + + fakeNetEngine + .setResponseText('test:/master', mediaPlaylist) + .setResponseValue('test:/video1.ts', tsSegmentData); + + await parser.start('test:/master', playerInterface); + + expect(onMetadataSpy).not.toHaveBeenCalled(); + }); + + it('supports interstitial', async () => { + const mediaPlaylist = [ + '#EXTM3U\n', + '#EXT-X-TARGETDURATION:5\n', + '#EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:00.00Z\n', + '#EXTINF:5,\n', + 'video1.ts\n', + '#EXT-X-DATERANGE:ID="1",CLASS="com.apple.hls.interstitial",', + 'START-DATE="2000-01-01T00:00:05.00Z",DURATION=30.0,', + 'X-ASSET-URI="fake",CUE="PRE,ONCE",X-RESTRICT="SKIP,JUMP",', + 'X-SNAP="IN"\n', + ].join(''); + + fakeNetEngine + .setResponseText('test:/master', mediaPlaylist) + .setResponseValue('test:/video1.ts', tsSegmentData); + + await parser.start('test:/master', playerInterface); + + const metadataType = 'com.apple.hls.interstitial'; + const values = [ + jasmine.objectContaining({ + key: 'X-ASSET-URI', + data: 'fake', + }), + jasmine.objectContaining({ + key: 'CUE', + data: 'PRE,ONCE', + }), + jasmine.objectContaining({ + key: 'X-RESTRICT', + data: 'SKIP,JUMP', + }), + jasmine.objectContaining({ + key: 'X-SNAP', + data: 'IN', + }), + ]; + expect(onMetadataSpy).toHaveBeenCalledTimes(1); + expect(onMetadataSpy).toHaveBeenCalledWith(metadataType, 5, 35, values); + }); + }); }); diff --git a/test/mss/mss_parser_unit.js b/test/mss/mss_parser_unit.js index 117056c8f..b1222916b 100644 --- a/test/mss/mss_parser_unit.js +++ b/test/mss/mss_parser_unit.js @@ -81,6 +81,7 @@ describe('MssParser Manifest', () => { newDrmInfo: (stream) => {}, onManifestUpdated: () => {}, getBandwidthEstimate: () => 1e6, + onMetadata: () => {}, }; }); diff --git a/test/test/util/dash_parser_util.js b/test/test/util/dash_parser_util.js index 32fc6a41a..c4be721b4 100644 --- a/test/test/util/dash_parser_util.js +++ b/test/test/util/dash_parser_util.js @@ -47,6 +47,7 @@ shaka.test.Dash = class { newDrmInfo: (stream) => {}, onManifestUpdated: () => {}, getBandwidthEstimate: () => 1e6, + onMetadata: () => {}, }; try { const manifest = await dashParser.start('dummy://foo', playerInterface); @@ -89,6 +90,7 @@ shaka.test.Dash = class { newDrmInfo: (stream) => {}, onManifestUpdated: () => {}, getBandwidthEstimate: () => 1e6, + onMetadata: () => {}, }; try { diff --git a/test/test/util/mss_parser_util.js b/test/test/util/mss_parser_util.js index 3225f2f41..0773a2a4b 100644 --- a/test/test/util/mss_parser_util.js +++ b/test/test/util/mss_parser_util.js @@ -47,6 +47,7 @@ shaka.test.Mss = class { newDrmInfo: (stream) => {}, onManifestUpdated: () => {}, getBandwidthEstimate: () => 1e6, + onMetadata: () => {}, }; const manifest = await mssParser.start('dummy://foo', playerInterface); const stream = manifest.variants[0].audio; @@ -85,6 +86,7 @@ shaka.test.Mss = class { newDrmInfo: (stream) => {}, onManifestUpdated: () => {}, getBandwidthEstimate: () => 1e6, + onMetadata: () => {}, }; const p = mssParser.start('dummy://foo', playerInterface); await expectAsync(p).toBeRejectedWith( diff --git a/test/util/content_steering_manager_unit.js b/test/util/content_steering_manager_unit.js index 2df28d3a1..400a20dc5 100644 --- a/test/util/content_steering_manager_unit.js +++ b/test/util/content_steering_manager_unit.js @@ -29,6 +29,7 @@ describe('ContentSteeringManager', () => { newDrmInfo: fail, onManifestUpdated: fail, getBandwidthEstimate: () => 1e6, + onMetadata: () => {}, }; const config = shaka.util.PlayerConfiguration.createDefault().manifest; manager = new shaka.util.ContentSteeringManager(playerInterface);