diff --git a/.eslintrc.js b/.eslintrc.js index da69aa7772..6e66cff9d9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -201,7 +201,7 @@ module.exports = { }, { 'selector': 'CatchClause', - 'message': 'Use expect.toFail or expectAsync.toBeRejected', + 'message': 'Use expect.toThrow or expectAsync.toBeRejected', }, { 'selector': 'CallExpression[callee.name=expect] >' + diff --git a/build/types/core b/build/types/core index f9810aa5ec..068a4e778e 100644 --- a/build/types/core +++ b/build/types/core @@ -12,7 +12,6 @@ +../../lib/deprecate/enforcer.js +../../lib/deprecate/version.js -+../../lib/media/active_stream_map.js +../../lib/media/adaptation_set.js +../../lib/media/adaptation_set_criteria.js +../../lib/media/buffering_observer.js @@ -22,7 +21,6 @@ +../../lib/media/manifest_parser.js +../../lib/media/media_source_engine.js +../../lib/media/mp4_segment_index_parser.js -+../../lib/media/period_observer.js +../../lib/media/play_rate_controller.js +../../lib/media/playhead.js +../../lib/media/playhead_observer.js diff --git a/build/types/offline b/build/types/offline index f27f6c0f83..4a6a930e55 100644 --- a/build/types/offline +++ b/build/types/offline @@ -9,6 +9,7 @@ +../../lib/offline/indexeddb/storage_mechanism.js +../../lib/offline/indexeddb/v1_storage_cell.js +../../lib/offline/indexeddb/v2_storage_cell.js ++../../lib/offline/indexeddb/v5_storage_cell.js +../../lib/offline/manifest_converter.js +../../lib/offline/offline_manifest_parser.js +../../lib/offline/offline_scheme.js diff --git a/externs/shaka/manifest.js b/externs/shaka/manifest.js index 22a5bf2a8e..466f9f3762 100644 --- a/externs/shaka/manifest.js +++ b/externs/shaka/manifest.js @@ -12,7 +12,8 @@ /** * @typedef {{ * presentationTimeline: !shaka.media.PresentationTimeline, - * periods: !Array., + * variants: !Array., + * textStreams: !Array., * offlineSessionIds: !Array., * minBufferTime: number * }} @@ -36,8 +37,6 @@ *

* *

- * The presentation timeline is divided into one or more Periods, and each of - * these Periods contains its own collection of Variants and text streams. * A variant is a combination of an audio and a video streams that can be played * together. *

@@ -54,9 +53,12 @@ * @property {!shaka.media.PresentationTimeline} presentationTimeline * Required.
* The presentation timeline. - * @property {!Array.} periods + * @property {!Array.} variants + * Required.
+ * The presentation's Variants. There must be at least one Variant. + * @property {!Array.} textStreams * Required.
- * The presentation's Periods. There must be at least one Period. + * The presentation's text streams. * @property {!Array.} offlineSessionIds * Defaults to [].
* An array of EME sessions to load for offline playback. @@ -71,35 +73,6 @@ shaka.extern.Manifest; -/** - * @typedef {{ - * startTime: number, - * variants: !Array., - * textStreams: !Array. - * }} - * - * @description - * A Period object contains the Streams for part of the presentation. - * - * @property {number} startTime - * Required.
- * The Period's start time, in seconds, relative to the start of the - * presentation. The first Period must begin at the start of the - * presentation. The Period ends immediately before the next Period's start - * time or exactly at the end of the presentation timeline. Periods which - * begin after the end of the presentation timeline are ignored. - * @property {!Array.} variants - * Required.
- * The Period's Variants. There must be at least one Variant. - * @property {!Array.} textStreams - * Required.
- * The Period's text streams. - * - * @exportDoc - */ -shaka.extern.Period; - - /** * @typedef {{ * initData: !Uint8Array, @@ -209,9 +182,9 @@ shaka.extern.DrmInfo; * See {@link http://www.iso.org/iso/home/standards/language_codes.htm} * @property {boolean} primary * Defaults to false.
- * True indicates that the player should use this Variant over others in the - * same Period. The player may still use another Variant to meet application - * preferences. + * True indicates that the player should use this Variant over others if user + * preferences cannot be met. The player may still use another Variant to + * meet user preferences. * @property {?shaka.extern.Stream} audio * The audio stream of the variant. * @property {?shaka.extern.Stream} video @@ -261,7 +234,7 @@ shaka.extern.CreateSegmentIndexFunction; * height: (number|undefined), * kind: (string|undefined), * encrypted: boolean, - * keyId: ?string, + * keyIds: !Array., * language: string, * label: ?string, * type: string, @@ -321,10 +294,11 @@ shaka.extern.CreateSegmentIndexFunction; * @property {boolean} encrypted * Defaults to false.
* True if the stream is encrypted. - * @property {?string} keyId - * Defaults to null (i.e., unencrypted or key ID unknown).
- * The stream's key ID as a lowercase hex string. This key ID identifies the - * encryption key that the browser (key system) can use to decrypt the stream. + * @property {!Array.} keyIds + * Defaults to empty (i.e., unencrypted or key ID unknown).
+ * The stream's key IDs as lowercase hex strings. These key IDs identify the + * encryption keys that the browser (key system) can use to decrypt the + * stream. * @property {string} language * The Stream's language, specified as a language code.
* Audio stream's language must be identical to the language of the containing @@ -336,9 +310,9 @@ shaka.extern.CreateSegmentIndexFunction; * Content type (e.g. 'video', 'audio' or 'text') * @property {boolean} primary * Defaults to false.
- * True indicates that the player should prefer this Stream over others - * in the same Period. The player may still use another Stream to meet - * application preferences. + * True indicates that the player should use this Stream over others if user + * preferences cannot be met. The player may still use another Variant to + * meet user preferences. * @property {?shaka.extern.Stream} trickModeVideo * Video streams only.
* An alternate video stream to use for trick mode playback. diff --git a/externs/shaka/manifest_parser.js b/externs/shaka/manifest_parser.js index a3a6723e97..31116cad07 100644 --- a/externs/shaka/manifest_parser.js +++ b/externs/shaka/manifest_parser.js @@ -99,8 +99,7 @@ shaka.extern.ManifestParser = class { /** * @typedef {{ * networkingEngine: !shaka.net.NetworkingEngine, - * filterNewPeriod: function(shaka.extern.Period), - * filterAllPeriods: function(!Array.), + * filter: function(shaka.extern.Manifest), * onTimelineRegionAdded: function(shaka.extern.TimelineRegionInfo), * onEvent: function(!Event), * onError: function(!shaka.util.Error) @@ -114,10 +113,9 @@ shaka.extern.ManifestParser = class { * * @property {!shaka.net.NetworkingEngine} networkingEngine * The networking engine to use for network requests. - * @property {function(shaka.extern.Period)} filterNewPeriod - * Should be called on a new Period so that it can be filtered. - * @property {function(!Array.)} filterAllPeriods - * Should be called on all Periods so that they can be filtered. + * @property {function(shaka.extern.Manifest)} filter + * Should be called when new variants or text streams are added to the + * Manifest. * @property {function(shaka.extern.TimelineRegionInfo)} onTimelineRegionAdded * Should be called when a new timeline region is added. * @property {function(!Event)} onEvent diff --git a/externs/shaka/offline.js b/externs/shaka/offline.js index 0ae5d5e6b9..aa4125a0a2 100644 --- a/externs/shaka/offline.js +++ b/externs/shaka/offline.js @@ -49,8 +49,7 @@ shaka.extern.OfflineSupport; * The time that the encrypted license expires, in milliseconds. If the media * is clear or the license never expires, this will equal Infinity. * @property {!Array.} tracks - * The tracks that are stored. This only lists those found in the first - * Period. + * The tracks that are stored. * @property {Object} appMetadata * The metadata passed to store(). * @exportDoc @@ -64,7 +63,7 @@ shaka.extern.StoredContent; * duration: number, * size: number, * expiration: number, - * periods: !Array., + * streams: !Array., * sessionIds: !Array., * drmInfo: ?shaka.extern.DrmInfo, * appMetadata: Object @@ -80,8 +79,8 @@ shaka.extern.StoredContent; * The license expiration, in milliseconds; or Infinity if not applicable. * Note that upon JSON serialization, Infinity becomes null, and must be * converted back upon loading from storage. - * @property {!Array.} periods - * The Periods that are stored. + * @property {!Array.} streams + * The Streams that are stored. * @property {!Array.} sessionIds * The DRM offline session IDs for the media. * @property {?shaka.extern.DrmInfo} drmInfo @@ -92,26 +91,11 @@ shaka.extern.StoredContent; shaka.extern.ManifestDB; -/** - * @typedef {{ - * startTime: number, - * streams: !Array. - * }} - * - * @property {number} startTime - * The start time of the period, in seconds. - * @property {!Array.} streams - * The streams that define the Period. - */ -shaka.extern.PeriodDB; - - /** * @typedef {{ * id: number, * originalId: ?string, * primary: boolean, - * presentationTimeOffset: number, * contentType: string, * mimeType: string, * codecs: string, @@ -122,11 +106,14 @@ shaka.extern.PeriodDB; * label: ?string, * width: ?number, * height: ?number, - * initSegmentKey: ?number, * encrypted: boolean, - * keyId: ?string, + * keyIds: !Array., * segments: !Array., - * variantIds: !Array. + * variantIds: !Array., + * roles: !Array., + * channelsCount: ?number, + * audioSamplingRate: ?number, + * closedCaptions: Map. * }} * * @property {number} id @@ -136,9 +123,6 @@ shaka.extern.PeriodDB; * DASH, this is the "id" attribute of the Representation element. * @property {boolean} primary * Whether the stream set was primary. - * @property {number} presentationTimeOffset - * The presentation time offset of the stream, in seconds. Note that this is - * the inverse of the timestampOffset as defined in the manifest types. * @property {string} contentType * The type of the stream, 'audio', 'text', or 'video'. * @property {string} mimeType @@ -159,31 +143,55 @@ shaka.extern.PeriodDB; * The width of the stream; null for audio/text. * @property {?number} height * The height of the stream; null for audio/text. - * @property {?number} initSegmentKey - * The storage key where the init segment is found; null if no init segment. * @property {boolean} encrypted * Whether this stream is encrypted. - * @property {?string} keyId - * The key ID this stream is encrypted with. + * @property {!Array.} keyIds + * The key IDs this stream is encrypted with. * @property {!Array.} segments * An array of segments that make up the stream. * @property {!Array.} variantIds * An array of ids of variants the stream is a part of. + * @property {!Array.} roles + * The roles of the stream as they appear on the manifest, + * e.g. 'main', 'caption', or 'commentary'. + * @property {?number} channelsCount + * The channel count information for the audio stream. + * @property {?number} audioSamplingRate + * Specifies the maximum sampling rate of the content. + * @property {Map.} closedCaptions + * A map containing the description of closed captions, with the caption + * channel number (CC1 | CC2 | CC3 | CC4) as the key and the language code + * as the value. If the channel number is not provided by the description, + * we'll set an 0-based index as the key. + * Example: {'CC1': 'eng'; 'CC3': 'swe'}, or {'1', 'eng'; '2': 'swe'}, etc. */ shaka.extern.StreamDB; /** * @typedef {{ + * initSegmentKey: ?number, * startTime: number, * endTime: number, + * appendWindowStart: number, + * appendWindowEnd: number, + * timestampOffset: number, * dataKey: number * }} * + * @property {?number} initSegmentKey + * The storage key where the init segment is found; null if no init segment. * @property {number} startTime - * The start time of the segment, in seconds from the start of the Period. + * The start time of the segment in the presentation timeline. * @property {number} endTime - * The end time of the segment, in seconds from the start of the Period. + * The end time of the segment in the presentation timeline. + * @property {number} appendWindowStart + * A start timestamp before which media samples will be truncated. + * @property {number} appendWindowEnd + * An end timestamp beyond which media samples will be truncated. + * @property {number} timestampOffset + * An offset which MediaSource will add to the segment's media timestamps + * during ingestion, to align to the presentation timeline. * @property {number} dataKey * The key to the data in storage. */ diff --git a/externs/shaka/offline_compat_v2.js b/externs/shaka/offline_compat_v2.js new file mode 100644 index 0000000000..67e699916a --- /dev/null +++ b/externs/shaka/offline_compat_v2.js @@ -0,0 +1,151 @@ +/** @license + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + + +/** + * @externs + */ + +/** + * @typedef {{ + * originalManifestUri: string, + * duration: number, + * size: number, + * expiration: number, + * periods: !Array., + * sessionIds: !Array., + * drmInfo: ?shaka.extern.DrmInfo, + * appMetadata: Object + * }} + * + * @property {string} originalManifestUri + * The URI that the manifest was originally loaded from. + * @property {number} duration + * The total duration of the media, in seconds. + * @property {number} size + * The total size of all stored segments, in bytes. + * @property {number} expiration + * The license expiration, in milliseconds; or Infinity if not applicable. + * Note that upon JSON serialization, Infinity becomes null, and must be + * converted back upon loading from storage. + * @property {!Array.} periods + * The Periods that are stored. + * @property {!Array.} sessionIds + * The DRM offline session IDs for the media. + * @property {?shaka.extern.DrmInfo} drmInfo + * The DRM info used to initialize EME. + * @property {Object} appMetadata + * A metadata object passed from the application. + */ +shaka.extern.ManifestDBV2; + + +/** + * @typedef {{ + * startTime: number, + * streams: !Array. + * }} + * + * @property {number} startTime + * The start time of the period, in seconds. + * @property {!Array.} streams + * The streams that define the Period. + */ +shaka.extern.PeriodDBV2; + + +/** + * @typedef {{ + * id: number, + * originalId: ?string, + * primary: boolean, + * presentationTimeOffset: number, + * contentType: string, + * mimeType: string, + * codecs: string, + * frameRate: (number|undefined), + * pixelAspectRatio: (string|undefined), + * kind: (string|undefined), + * language: string, + * label: ?string, + * width: ?number, + * height: ?number, + * initSegmentKey: ?number, + * encrypted: boolean, + * keyId: ?string, + * segments: !Array., + * variantIds: !Array. + * }} + * + * @property {number} id + * The unique id of the stream. + * @property {?string} originalId + * The original ID, if any, that appeared in the manifest. For example, in + * DASH, this is the "id" attribute of the Representation element. + * @property {boolean} primary + * Whether the stream set was primary. + * @property {number} presentationTimeOffset + * The presentation time offset of the stream, in seconds. Note that this is + * the inverse of the timestampOffset as defined in the manifest types. + * @property {string} contentType + * The type of the stream, 'audio', 'text', or 'video'. + * @property {string} mimeType + * The MIME type of the stream. + * @property {string} codecs + * The codecs of the stream. + * @property {(number|undefined)} frameRate + * The Stream's framerate in frames per second. + * @property {(string|undefined)} pixelAspectRatio + * The Stream's pixel aspect ratio + * @property {(string|undefined)} kind + * The kind of text stream; undefined for audio/video. + * @property {string} language + * The language of the stream; '' for video. + * @property {?string} label + * The label of the stream; '' for video. + * @property {?number} width + * The width of the stream; null for audio/text. + * @property {?number} height + * The height of the stream; null for audio/text. + * @property {?number} initSegmentKey + * The storage key where the init segment is found; null if no init segment. + * @property {boolean} encrypted + * Whether this stream is encrypted. + * @property {?string} keyId + * The key ID this stream is encrypted with. + * @property {!Array.} segments + * An array of segments that make up the stream. + * @property {!Array.} variantIds + * An array of ids of variants the stream is a part of. + */ +shaka.extern.StreamDBV2; + + +/** + * @typedef {{ + * startTime: number, + * endTime: number, + * dataKey: number + * }} + * + * @property {number} startTime + * The start time of the segment, in seconds from the start of the Period. + * @property {number} endTime + * The end time of the segment, in seconds from the start of the Period. + * @property {number} dataKey + * The key to the data in storage. + */ +shaka.extern.SegmentDBV2; + + +/** + * @typedef {{ + * data: !ArrayBuffer + * }} + * + * @property {!ArrayBuffer} data + * The data contents of the segment. + */ +shaka.extern.SegmentDataDBV2; diff --git a/externs/shaka/player.js b/externs/shaka/player.js index 7459ebb27c..f632898276 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -827,8 +827,7 @@ shaka.extern.AbrConfiguration; * trackSelectionCallback * Called inside store() to determine which tracks to save from a * manifest. It is passed an array of Tracks from the manifest and it should - * return an array of the tracks to store. This is called for each Period in - * the manifest (in order). + * return an array of the tracks to store. * @property {function(shaka.extern.StoredContent,number)} progressCallback * Called inside store() to give progress info back to the app. * It is given the current manifest being stored and the progress of it being diff --git a/lib/abr/simple_abr_manager.js b/lib/abr/simple_abr_manager.js index b567051dd2..498200b9bc 100644 --- a/lib/abr/simple_abr_manager.js +++ b/lib/abr/simple_abr_manager.js @@ -256,11 +256,13 @@ shaka.abr.SimpleAbrManager = class { this.config_.defaultBandwidthEstimate); const currentBandwidthKbps = Math.round(bandwidthEstimate / 1000.0); - shaka.log.debug( - 'Calling switch_(), bandwidth=' + currentBandwidthKbps + ' kbps'); - // If any of these chosen streams are already chosen, Player will filter - // them out before passing the choices on to StreamingEngine. - this.switch_(chosenVariant); + if (chosenVariant) { + shaka.log.debug( + 'Calling switch_(), bandwidth=' + currentBandwidthKbps + ' kbps'); + // If any of these chosen streams are already chosen, Player will filter + // them out before passing the choices on to StreamingEngine. + this.switch_(chosenVariant); + } } diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index e9a3ca4bbc..38ca0d7812 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -27,6 +27,7 @@ goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.MimeUtils'); goog.require('shaka.util.Networking'); goog.require('shaka.util.OperationManager'); +goog.require('shaka.util.Periods'); goog.require('shaka.util.StringUtils'); goog.require('shaka.util.XmlUtils'); @@ -52,9 +53,6 @@ shaka.dash.DashParser = class { /** @private {?shaka.extern.Manifest} */ this.manifest_ = null; - /** @private {!Array.} */ - this.periodIds_ = []; - /** @private {number} */ this.globalId_ = 1; @@ -141,7 +139,6 @@ shaka.dash.DashParser = class { this.config_ = null; this.manifestUris_ = []; this.manifest_ = null; - this.periodIds_ = []; this.segmentIndexMap_ = {}; if (this.updateTimer_ != null) { @@ -384,7 +381,10 @@ shaka.dash.DashParser = class { if (!this.manifest_) { this.manifest_ = { presentationTimeline: presentationTimeline, - periods: periods, + variants: shaka.util.Periods.stitchVariants( + periods.map((period) => period.variants)), + textStreams: shaka.util.Periods.stitchTextStreams( + periods.map((period) => period.textStreams)), offlineSessionIds: [], minBufferTime: minBufferTime || 0, }; @@ -401,6 +401,9 @@ shaka.dash.DashParser = class { presentationTimeline.setClockOffset(offset); } } + + goog.asserts.assert(this.manifest_, 'Manifest should exist by now!'); + this.playerInterface_.filter(this.manifest_); } /** @@ -412,7 +415,7 @@ shaka.dash.DashParser = class { * @param {!Array.} baseUris * @param {!Element} mpd * @return {{ - * periods: !Array., + * periods: !Array., * duration: ?number, * durationDerivedFromPeriods: boolean * }} @@ -475,24 +478,6 @@ shaka.dash.DashParser = class { const period = this.parsePeriod_(context, baseUris, info); periods.push(period); - // If the period ID is new, add it to the list. This must be done for - // both the initial manifest parse and for updates. - // See https://github.com/google/shaka-player/issues/963 - const periodId = context.period.id; - goog.asserts.assert(periodId, 'Period IDs should not be null!'); - if (!this.periodIds_.includes(periodId)) { - this.periodIds_.push(periodId); - - // If this is an update, call filterNewPeriod and add it to the - // manifest. - // If this is the first parse of the manifest (this.manifest_ == null), - // filterAllPeriods will be called later. - if (this.manifest_) { - this.playerInterface_.filterNewPeriod(period); - this.manifest_.periods.push(period); - } - } - if (periodDuration == null) { if (next) { // If the duration is still null and we aren't at the end, then we @@ -510,11 +495,6 @@ shaka.dash.DashParser = class { prevEnd = start + periodDuration; } // end of period parsing loop - // Call filterAllPeriods if this is the initial parse. - if (this.manifest_ == null) { - this.playerInterface_.filterAllPeriods(periods); - } - if (presentationDuration != null) { if (prevEnd != presentationDuration) { shaka.log.warning( @@ -544,7 +524,7 @@ shaka.dash.DashParser = class { * @param {shaka.dash.DashParser.Context} context * @param {!Array.} baseUris * @param {shaka.dash.DashParser.PeriodInfo} periodInfo - * @return {shaka.extern.Period} + * @return {shaka.dash.DashParser.PeriodStreams} * @private */ parsePeriod_(context, baseUris, periodInfo) { @@ -657,7 +637,6 @@ shaka.dash.DashParser = class { } return { - startTime: periodInfo.start, textStreams: textStreams, variants: variants, }; @@ -951,9 +930,7 @@ shaka.dash.DashParser = class { // key ID from the manifest in the wrapped license request. // Thus, it should be put in drmInfo to be accessible to request filters. for (const drmInfo of contentProtection.drmInfos) { - if (stream.keyId) { - drmInfo.keyIds.push(stream.keyId); - } + drmInfo.keyIds.push(...stream.keyIds); } } @@ -1060,6 +1037,7 @@ shaka.dash.DashParser = class { const keyId = shaka.dash.ContentProtection.parseFromRepresentation( contentProtectionElems, contentProtection, this.config_.dash.ignoreDrmInfo); + const keyIds = keyId ? [keyId] : []; // Detect the presence of E-AC3 JOC audio content, using DD+JOC signaling. // See: ETSI TS 103 420 V1.2.1 (2018-10) @@ -1092,20 +1070,20 @@ shaka.dash.DashParser = class { bandwidth: context.bandwidth, width: context.representation.width, height: context.representation.height, - kind: kind, + kind, encrypted: contentProtection.drmInfos.length > 0, - keyId: keyId, - language: language, - label: label, + keyIds, + language, + label, type: context.adaptationSet.contentType, primary: isPrimary, trickModeVideo: null, emsgSchemeIdUris: context.representation.emsgSchemeIdUris, - roles: roles, + roles, channelsCount: context.representation.numChannels, audioSamplingRate: context.representation.audioSamplingRate, - closedCaptions: closedCaptions, + closedCaptions, }; return stream; } @@ -1736,6 +1714,23 @@ shaka.dash.DashParser.Context; shaka.dash.DashParser.PeriodInfo; +/** + * @typedef {{ + * textStreams: !Array., + * variants: !Array. + * }} + * + * @description + * Contains the variants and text streams from one DASH period. + * + * @property {!Array.} textStreams + * The text streams from one Period. + * @property {!Array.} variants + * The variants from one Period. + */ +shaka.dash.DashParser.PeriodStreams; + + /** * @typedef {{ * id: string, diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 376445caee..80ee5de206 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -326,6 +326,8 @@ shaka.hls.HlsParser = class { * @private */ async parseManifest_(data) { + const Utils = shaka.hls.Utils; + goog.asserts.assert(this.masterPlaylistUri_, 'Master playlist URI must be set before calling parseManifest_!'); @@ -341,7 +343,20 @@ shaka.hls.HlsParser = class { shaka.util.Error.Code.HLS_MASTER_PLAYLIST_NOT_PROVIDED); } - const period = await this.createPeriod_(playlist.tags); + /** @type {!Array.} */ + const mediaTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-MEDIA'); + /** @type {!Array.} */ + const variantTags = Utils.filterTagsByName( + playlist.tags, 'EXT-X-STREAM-INF'); + + this.parseCodecs_(variantTags); + + // Parse audio and video media tags first, so that we can extract segment + // start time from audio/video streams and reuse for text streams. + await this.createStreamInfosFromMediaTags_(mediaTags); + this.parseClosedCaptions_(mediaTags); + const variants = await this.createVariantsForTags_(variantTags); + const textStreams = await this.parseTexts_(mediaTags); // Make sure that the parser has not been destroyed. if (!this.playerInterface_) { @@ -351,7 +366,7 @@ shaka.hls.HlsParser = class { shaka.util.Error.Code.OPERATION_ABORTED); } - if (this.aesEncrypted_ && period.variants.length == 0) { + if (this.aesEncrypted_ && variants.length == 0) { // We do not support AES-128 encryption with HLS yet. Variants is null // when the playlist is encrypted with AES-128. shaka.log.info('No stream is created, because we don\'t support AES-128', @@ -362,10 +377,6 @@ shaka.hls.HlsParser = class { shaka.util.Error.Code.HLS_AES_128_ENCRYPTION_NOT_SUPPORTED); } - // HLS has no notion of periods. We're treating the whole presentation as - // one period. - this.playerInterface_.filterAllPeriods([period]); - // Find the min and max timestamp of the earliest segment in all streams. // Find the minimum duration of all streams as well. let minFirstTimestamp = Infinity; @@ -427,7 +438,7 @@ shaka.hls.HlsParser = class { for (const streamInfo of this.uriToStreamInfosMap_.values()) { // The segments were created with actual media times, rather than - // period-aligned times, so offset them all now. + // presentation-aligned times, so offset them all now. streamInfo.stream.segmentIndex.offset(-minFirstTimestamp); // Finally, fit the segments to the playlist duration. streamInfo.stream.segmentIndex.fit(/* periodStart= */ 0, minDuration); @@ -436,40 +447,12 @@ shaka.hls.HlsParser = class { this.manifest_ = { presentationTimeline: this.presentationTimeline_, - periods: [period], + variants, + textStreams, offlineSessionIds: [], minBufferTime: 0, }; - } - - /** - * Parses the playlist tags and creates a Period object. - * - * @param {!Array.} tags All tags from the playlist. - * @return {!Promise.} - * @private - */ - async createPeriod_(tags) { - const Utils = shaka.hls.Utils; - /** @type {!Array.} */ - const mediaTags = Utils.filterTagsByName(tags, 'EXT-X-MEDIA'); - /** @type {!Array.} */ - const variantTags = Utils.filterTagsByName(tags, 'EXT-X-STREAM-INF'); - - this.parseCodecs_(variantTags); - - // Parse audio and video media tags first, so that we can extract segment - // start time from audio/video streams and reuse for text streams. - await this.createStreamInfosFromMediaTags_(mediaTags); - this.parseClosedCaptions_(mediaTags); - const variants = await this.createVariantsForTags_(variantTags); - const textStreams = await this.parseTexts_(mediaTags); - - return { - startTime: 0, - variants: variants, - textStreams: textStreams, - }; + this.playerInterface_.filter(this.manifest_); } /** @@ -1204,7 +1187,7 @@ shaka.hls.HlsParser = class { codecs: codecs, kind: kind, encrypted: encrypted, - keyId: keyId, + keyIds: [keyId], language: language, label: name, // For historical reasons, since before "originalId". type: type, diff --git a/lib/media/active_stream_map.js b/lib/media/active_stream_map.js deleted file mode 100644 index f00225e0da..0000000000 --- a/lib/media/active_stream_map.js +++ /dev/null @@ -1,111 +0,0 @@ -/** @license - * Copyright 2016 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -goog.provide('shaka.media.ActiveStreamMap'); - - -/** - * A structure used to track which streams were last used in any given period. - * - * @final - */ -shaka.media.ActiveStreamMap = class { - constructor() { - /** - * A mapping between a period and the content last streamed in that period. - * - * @private {!Map.} - */ - this.history_ = new Map(); - } - - /** - * Clear the history. - */ - clear() { - // Clear the map to release references to the periods (the key). This - // assumes that the references will be broken by doing this. - this.history_.clear(); - } - - /** - * Set the variant that was last playing in |period|. Setting it to |null| is - * the same as saying "we were playing no variant in this period". - * - * @param {shaka.extern.Period} period - * @param {?shaka.extern.Variant} variant - */ - useVariant(period, variant) { - this.getFrameFor_(period).variant = variant; - } - - /** - * Set the text stream that was last displayed in |period|. Setting it to - * |null| is the same as saying "we were displaying no text in this period". - * - * @param {shaka.extern.Period} period - * @param {?shaka.extern.Stream} stream - */ - useText(period, stream) { - this.getFrameFor_(period).text = stream; - } - - /** - * Get the variant that was playing in the given period. If no variant was - * playing this period or the period had not started playing, then |null| will - * be returned. - * - * @param {shaka.extern.Period} period - * @return {?shaka.extern.Variant} - */ - getVariant(period) { - return this.getFrameFor_(period).variant; - } - - /** - * Get the text stream that was playing in the given period. If no text - * stream was playing this period or the period had not started playing, then - * |null| will be returned. - * - * @param {shaka.extern.Period} period - * @return {?shaka.extern.Stream} - */ - getText(period) { - return this.getFrameFor_(period).text; - } - - /** - * Get the frame for a period. This will ensure that a frame exists for the - * given period. - * - * @param {shaka.extern.Period} period - * @return {!shaka.media.ActiveStreamMap.Frame} - * @private - */ - getFrameFor_(period) { - if (!this.history_.has(period)) { - const frame = new shaka.media.ActiveStreamMap.Frame(); - this.history_.set(period, frame); - } - - return this.history_.get(period); - } -}; - - -/** - * A structure used to track which streams were played during a specific - * time frame. - * - * @final - */ -shaka.media.ActiveStreamMap.Frame = class { - constructor() { - /** @type {?shaka.extern.Variant} */ - this.variant = null; - /** @type {?shaka.extern.Stream} */ - this.text = null; - } -}; diff --git a/lib/media/period_observer.js b/lib/media/period_observer.js deleted file mode 100644 index 99c033acba..0000000000 --- a/lib/media/period_observer.js +++ /dev/null @@ -1,103 +0,0 @@ -/** @license - * Copyright 2016 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -goog.provide('shaka.media.PeriodObserver'); - -goog.require('shaka.media.IPlayheadObserver'); -goog.require('shaka.util.Periods'); - - -/** - * The period observer keeps track of which period we are in and calls the - * |onPeriodChange| callback whenever we change periods. - * - * @implements {shaka.media.IPlayheadObserver} - * @final - */ -shaka.media.PeriodObserver = class { - /** - * The period observer needs an always-up-to-date collection of periods, - * and right now the only way to have that is to reference the manifest. - * - * @param {shaka.extern.Manifest} manifest - */ - constructor(manifest) { - /** @private {?shaka.extern.Manifest} */ - this.manifest_ = manifest; - - /** - * This will be which period we think the playhead is currently in. If it is - * |null|, it means we don't know. We say "we think" because this may become - * out-of-date between updates. - * - * @private {?shaka.extern.Period} - */ - this.currentPeriod_ = null; - - /** - * The callback for when we change periods. To avoid null-checks, assign it - * a no-op when there is no external callback assigned to it. When we move - * into a new period, this callback will be called with the new period. - * - * @private {function(shaka.extern.Period)} - */ - this.onChangedPeriods_ = (period) => {}; - } - - /** @override */ - release() { - // Break all internal references. - this.manifest_ = null; - this.currentPeriod_ = null; - this.onChangedPeriods_ = (period) => {}; - } - - /** @override */ - poll(positionInSeconds, wasSeeking) { - // We detect changes in period by comparing where we think we are against - // where we actually are. - const expectedPeriod = this.currentPeriod_; - const actualPeriod = this.findCurrentPeriod_(positionInSeconds); - if (expectedPeriod != actualPeriod) { - this.onChangedPeriods_(actualPeriod); - } - // Make sure we are up-to-date. - this.currentPeriod_ = actualPeriod; - } - - /** - * Set all callbacks. This will override any previous calls to |setListeners|. - * - * @param {function(shaka.extern.Period)} onChangedPeriods - * The callback for when we move to a new period. - */ - setListeners(onChangedPeriods) { - this.onChangedPeriods_ = onChangedPeriods; - } - - /** - * Find which period we are most likely in based on the current manifest and - * current time. The value here may be different than |this.currentPeriod_|, - * if that is true, it means we changed periods since the last time we updated - * |this.currentPeriod_|. - * - * @param {number} currentTimeSeconds - * @return {shaka.extern.Period} - * @private - */ - findCurrentPeriod_(currentTimeSeconds) { - const periods = this.manifest_.periods; - - const found = shaka.util.Periods.findPeriodForTime( - periods, - currentTimeSeconds); - - // Fallback to periods[0] so that it can never be null. If we join a live - // stream, periods[0].startTime may be non-zero. We can't guarantee that - // video.currentTime will always be inside the seek range so it may be - // possible to call findCurrentPeriod_(beforeFirstPeriod). - return found || periods[0]; - } -}; diff --git a/lib/media/playhead.js b/lib/media/playhead.js index 4bb480b020..aa4674108a 100644 --- a/lib/media/playhead.js +++ b/lib/media/playhead.js @@ -71,7 +71,7 @@ shaka.media.SrcEqualsPlayhead = class { /** @private {shaka.util.EventManager} */ this.eventManager_ = new shaka.util.EventManager(); - // We listen for the loaded-metadata-event so that we know when we can + // We listen for the loaded-data-event so that we know when we can // interact with |currentTime|. const onLoaded = () => { if (this.startTime_ == null) { diff --git a/lib/media/segment_index.js b/lib/media/segment_index.js index 5067a9d85a..a9361e5afb 100644 --- a/lib/media/segment_index.js +++ b/lib/media/segment_index.js @@ -208,27 +208,27 @@ shaka.media.SegmentIndex = class { /** - * Also expands or contracts the last SegmentReference so it ends at the end - * of its Period. + * Drops references that start after windowEnd, or end before windowStart, + * and contracts the last reference so that it ends at windowEnd. * * Do not call on the last period of a live presentation (unknown duration). * It is okay to call on the other periods of a live presentation, where the * duration is known and another period has been added. * - * @param {number} periodStart - * @param {?number} periodEnd + * @param {number} windowStart + * @param {?number} windowEnd * @export */ - fit(periodStart, periodEnd) { - goog.asserts.assert(periodEnd != null, - 'Period duration must be known for static content!'); - goog.asserts.assert(periodEnd != Infinity, - 'Period duration must be finite for static content!'); + fit(windowStart, windowEnd) { + goog.asserts.assert(windowEnd != null, + 'Content duration must be known for static content!'); + goog.asserts.assert(windowEnd != Infinity, + 'Content duration must be finite for static content!'); // Trim out references we will never use. while (this.references_.length) { const lastReference = this.references_[this.references_.length - 1]; - if (lastReference.startTime >= periodEnd) { + if (lastReference.startTime >= windowEnd) { this.references_.pop(); } else { break; @@ -237,7 +237,7 @@ shaka.media.SegmentIndex = class { while (this.references_.length) { const firstReference = this.references_[0]; - if (firstReference.endTime <= periodStart) { + if (firstReference.endTime <= windowStart) { this.references_.shift(); this.numEvicted_++; } else { @@ -254,7 +254,7 @@ shaka.media.SegmentIndex = class { this.references_[this.references_.length - 1] = new shaka.media.SegmentReference( lastReference.startTime, - /* endTime= */ periodEnd, + /* endTime= */ windowEnd, lastReference.getUris, lastReference.startByte, lastReference.endByte, diff --git a/lib/media/streaming_engine.js b/lib/media/streaming_engine.js index 59a569cc88..d4dab0eb2c 100644 --- a/lib/media/streaming_engine.js +++ b/lib/media/streaming_engine.js @@ -15,38 +15,28 @@ goog.require('shaka.util.Destroyer'); goog.require('shaka.util.Error'); goog.require('shaka.util.FakeEvent'); goog.require('shaka.util.IDestroyable'); -goog.require('shaka.util.Iterables'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.MimeUtils'); goog.require('shaka.util.Mp4Parser'); goog.require('shaka.util.Networking'); -goog.require('shaka.util.Periods'); /** * @summary Creates a Streaming Engine. * The StreamingEngine is responsible for setting up the Manifest's Streams * (i.e., for calling each Stream's createSegmentIndex() function), for - * downloading segments, for co-ordinating audio, video, and text buffering, - * and for handling Period transitions. The StreamingEngine provides an - * interface to switch between Streams, but it does not choose which Streams to - * switch to. - * - * The StreamingEngine notifies its owner when it needs to buffer a new Period, - * so its owner can choose which Streams within that Period to initially - * buffer. Moreover, the StreamingEngine also notifies its owner when any - * Stream within the current Period may be switched to, so its owner can switch - * bitrates, resolutions, or languages. + * downloading segments, for co-ordinating audio, video, and text buffering. + * The StreamingEngine provides an interface to switch between Streams, but it + * does not choose which Streams to switch to. * * The StreamingEngine does not need to be notified about changes to the * Manifest's SegmentIndexes; however, it does need to be notified when new - * Periods are added to the Manifest, so it can set up that Period's Streams. + * Variants are added to the Manifest. * - * To start the StreamingEngine the owner must first call configure() followed - * by init(). The StreamingEngine will then call onChooseStreams(p) when it - * needs to buffer Period p; it will then switch to the Streams returned from - * that function. The StreamingEngine will call onCanSwitch() when any - * Stream within the current Period may be switched to. + * To start the StreamingEngine the owner must first call configure(), followed + * by one call to switchVariant(), one optional call to switchTextStream(), and + * finally a call to start(). After start() resolves, switch*() can be used + * freely. * * The owner must call seeked() each time the playhead moves to a new location * within the presentation timeline; however, the owner may forego calling @@ -72,16 +62,22 @@ shaka.media.StreamingEngine = class { /** @private {number} */ this.bufferingGoalScale_ = 1; + /** @private {?shaka.extern.Variant} */ + this.currentVariant_ = null; + + /** @private {?shaka.extern.Stream} */ + this.currentTextStream_ = null; + /** * Maps a content type, e.g., 'audio', 'video', or 'text', to a MediaState. * * @private {!Map.} + * !shaka.media.StreamingEngine.MediaState_>} */ this.mediaStates_ = new Map(); /** - * Set to true once one segment of each content type has been buffered. + * Set to true once the initial media states have been created. * * @private {boolean} */ @@ -102,17 +98,6 @@ shaka.media.StreamingEngine = class { */ this.fatalError_ = false; - /** - * Set to true when a request to unload text stream comes in. This is used - * since loading new text stream is async, the request of unloading text - * stream might come in before setting up new text stream is finished. - * @private {boolean} - */ - this.unloadingTextStream_ = false; - - /** @private {number} */ - this.textStreamSequenceId_ = 0; - /** @private {!shaka.util.Destroyer} */ this.destroyer_ = new shaka.util.Destroyer(() => this.doDestroy_()); } @@ -142,7 +127,7 @@ shaka.media.StreamingEngine = class { /** * Called by the Player to provide an updated configuration any time it - * changes. Must be called at least once before init(). + * changes. Must be called at least once before start(). * * @param {shaka.extern.StreamingConfiguration} config */ @@ -173,27 +158,10 @@ shaka.media.StreamingEngine = class { /** * Initialize and start streaming. * - * By calling this method, streaming engine will choose the initial streams by - * calling out to |onChooseStreams| followed by |onCanSwitch|. When streaming - * engine switches periods, it will call |onChooseStreams| followed by - * |onCanSwitch|. - * - * Asking streaming engine to switch streams between |onChooseStreams| and - * |onChangeSwitch| is not supported. - * - * After the StreamingEngine calls onChooseStreams(p) for the first time, it - * will begin setting up the Streams returned from that function and - * subsequently switch to them. However, the StreamingEngine will not begin - * setting up any other Streams until at least one segment from each of the - * initial set of Streams has been buffered (this reduces startup latency). - * - * After the StreamingEngine completes this startup phase it will begin - * setting up each Period's Streams (while buffering in parrallel). - * - * When the StreamingEngine needs to buffer the next Period it will have - * already set up that Period's Streams. So, when the StreamingEngine calls - * onChooseStreams(p) after the first time, the StreamingEngine will - * immediately switch to the Streams returned from that function. + * By calling this method, StreamingEngine will start streaming the variant + * chosen by a prior call to switchVariant(), and optionally, the text stream + * chosen by a prior call to switchTextStream(). Once the Promise resolves, + * switch*() may be called freely. * * @return {!Promise} */ @@ -201,164 +169,66 @@ shaka.media.StreamingEngine = class { goog.asserts.assert(this.config_, 'StreamingEngine configure() must be called before init()!'); - // Determine which Period we must buffer. - const presentationTime = this.playerInterface_.getPresentationTime(); - const needPeriodIndex = this.findPeriodForTime_(presentationTime); - - // Get the initial set of Streams. - const initialStreams = this.playerInterface_.onChooseStreams( - this.manifest_.periods[needPeriodIndex]); - if (!initialStreams.variant && !initialStreams.text) { - shaka.log.error('init: no Streams chosen'); - throw new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.STREAMING, - shaka.util.Error.Code.INVALID_STREAMS_CHOSEN); - } - - // Setup the initial set of Streams and then begin each update cycle. After - // startup completes onUpdate_() will set up the remaining Periods. - await this.initStreams_( - initialStreams.variant ? initialStreams.variant.audio : null, - initialStreams.variant ? initialStreams.variant.video : null, - initialStreams.text, - presentationTime); + // Setup the initial set of Streams and then begin each update cycle. + await this.initStreams_(); this.destroyer_.ensureNotDestroyed(); shaka.log.debug('init: completed initial Stream setup'); - - // Subtlety: onInitialStreamsSetup() may call switch() or seeked(), so we - // must schedule an update beforehand so |updateTimer| is set. - if (this.playerInterface_ && this.playerInterface_.onInitialStreamsSetup) { - shaka.log.v1('init: calling onInitialStreamsSetup()...'); - this.playerInterface_.onInitialStreamsSetup(); - } + this.startupComplete_ = true; } - /** - * Gets the Period in which we are currently buffering. This might be - * different from the Period which contains the Playhead. - * @return {?shaka.extern.Period} + * Get the current variant we are streaming. Returns null if nothing is + * streaming. + * @return {?shaka.extern.Variant} */ - getBufferingPeriod() { - const ContentType = shaka.util.ManifestParserUtils.ContentType; - - const video = this.mediaStates_.get(ContentType.VIDEO); - if (video) { - return this.manifest_.periods[video.needPeriodIndex]; - } - - const audio = this.mediaStates_.get(ContentType.AUDIO); - if (audio) { - return this.manifest_.periods[audio.needPeriodIndex]; - } - - return null; + getCurrentVariant() { + return this.currentVariant_; } - /** - * Get the audio stream which we are currently buffering. Returns null if - * there is no audio streaming. + * Get the text stream we are streaming. Returns null if there is no text + * streaming. * @return {?shaka.extern.Stream} */ - getBufferingAudio() { - const ContentType = shaka.util.ManifestParserUtils.ContentType; - return this.getStream_(ContentType.AUDIO); + getCurrentTextStream() { + return this.currentTextStream_; } - /** - * Get the video stream which we are currently buffering. Returns null if - * there is no video streaming. - * @return {?shaka.extern.Stream} - */ - getBufferingVideo() { - const ContentType = shaka.util.ManifestParserUtils.ContentType; - return this.getStream_(ContentType.VIDEO); - } - - - /** - * Get the text stream which we are currently buffering. Returns null if - * there is no text streaming. - * @return {?shaka.extern.Stream} - */ - getBufferingText() { - const ContentType = shaka.util.ManifestParserUtils.ContentType; - return this.getStream_(ContentType.TEXT); - } - - /** - * Get the stream of the given type which we are currently buffering. Returns - * null if there is no stream for the given type. - * @param {shaka.util.ManifestParserUtils.ContentType} type - * @return {?shaka.extern.Stream} - * @private - */ - getStream_(type) { - const state = this.mediaStates_.get(type); - - if (state) { - // Don't tell the caller about trick play streams. If we're in trick - // play, return the stream we will go back to after we exit trick play. - return state.restoreStreamAfterTrickPlay || state.stream; - } else { - return null; - } - } - - /** - * Notifies StreamingEngine that a new text stream was added to the manifest. - * This initializes the given stream. This returns a Promise that resolves - * when the stream has been set up, and a media state has been created. + * Start streaming text, creating a new media state. * * @param {shaka.extern.Stream} stream * @return {!Promise} + * @private */ - async loadNewTextStream(stream) { + async loadNewTextStream_(stream) { const ContentType = shaka.util.ManifestParserUtils.ContentType; + goog.asserts.assert(!this.mediaStates_.has(ContentType.TEXT), + 'Should not call loadNewTextStream_ while streaming text!'); - // Clear MediaSource's buffered text, so that the new text stream will - // properly replace the old buffered text. - await this.playerInterface_.mediaSourceEngine.clear(ContentType.TEXT); - - // Since setupStreams_() is async, if the user hides/shows captions quickly, - // there would be a race condition that a new text media state is created - // but the old media state is not yet deleted. - // The Sequence Id is to avoid that race condition. - this.textStreamSequenceId_++; - this.unloadingTextStream_ = false; - const currentSequenceId = this.textStreamSequenceId_; - - const mediaSourceEngine = this.playerInterface_.mediaSourceEngine; - - const streamMap = new Map(); - const streamSet = new Set(); - - streamMap.set(ContentType.TEXT, stream); - streamSet.add(stream); + try { + // Clear MediaSource's buffered text, so that the new text stream will + // properly replace the old buffered text. + // TODO: Should this happen in unloadTextStream() instead? + await this.playerInterface_.mediaSourceEngine.clear(ContentType.TEXT); + } catch (error) { + if (this.playerInterface_) { + this.playerInterface_.onError(error); + } + } - await mediaSourceEngine.init(streamMap, /* forceTansmuxTS= */ false); - this.destroyer_.ensureNotDestroyed(); + const mimeType = shaka.util.MimeUtils.getFullType( + stream.mimeType, stream.codecs); + this.playerInterface_.mediaSourceEngine.reinitText(mimeType); const textDisplayer = this.playerInterface_.mediaSourceEngine.getTextDisplayer(); - const streamText = textDisplayer.isTextVisible() || this.config_.alwaysStreamText; - const presentationTime = this.playerInterface_.getPresentationTime(); - const needPeriodIndex = this.findPeriodForTime_(presentationTime); - const state = this.createMediaState_( - stream, - needPeriodIndex, - /* resumeAt= */ 0); - - if ((this.textStreamSequenceId_ == currentSequenceId) && - !this.mediaStates_.has(ContentType.TEXT) && - !this.unloadingTextStream_ && streamText) { + if (streamText) { + const state = this.createMediaState_(stream); this.mediaStates_.set(ContentType.TEXT, state); this.scheduleUpdate_(state, 0); } @@ -370,7 +240,6 @@ shaka.media.StreamingEngine = class { */ unloadTextStream() { const ContentType = shaka.util.ManifestParserUtils.ContentType; - this.unloadingTextStream_ = true; const state = this.mediaStates_.get(ContentType.TEXT); if (state) { @@ -430,37 +299,46 @@ shaka.media.StreamingEngine = class { /** * @param {shaka.extern.Variant} variant - * @param {boolean} clearBuffer - * @param {number} safeMargin - * @return {boolean} Whether we actually switched streams. + * @param {boolean=} clearBuffer + * @param {number=} safeMargin */ - switchVariant(variant, clearBuffer, safeMargin) { - let ret = false; + switchVariant(variant, clearBuffer = false, safeMargin = 0) { + this.currentVariant_ = variant; + + if (!this.startupComplete_) { + // The selected variant will be used in start(). + return; + } + if (variant.video) { - const changed = this.switchInternal_( + this.switchInternal_( variant.video, /* clearBuffer= */ clearBuffer, /* safeMargin= */ safeMargin, /* force= */ false); - ret = ret || changed; } if (variant.audio) { - const changed = this.switchInternal_( + this.switchInternal_( variant.audio, /* clearBuffer= */ clearBuffer, /* safeMargin= */ safeMargin, /* force= */ false); - ret = ret || changed; } - return ret; } /** * @param {shaka.extern.Stream} textStream - * @return {boolean} Whether we actually switched streams. */ switchTextStream(textStream) { + this.currentTextStream_ = textStream; + + if (!this.startupComplete_) { + // The selected text stream will be used in start(). + return; + } + const ContentType = shaka.util.ManifestParserUtils.ContentType; goog.asserts.assert(textStream && textStream.type == ContentType.TEXT, 'Wrong stream type passed to switchTextStream!'); - return this.switchInternal_( + + this.switchInternal_( textStream, /* clearBuffer= */ true, /* safeMargin= */ 0, /* force= */ false); } @@ -479,15 +357,13 @@ shaka.media.StreamingEngine = class { /** - * Switches to the given Stream. |stream| may be from any Variant or any - * Period. + * Switches to the given Stream. |stream| may be from any Variant. * * @param {shaka.extern.Stream} stream * @param {boolean} clearBuffer * @param {number} safeMargin * @param {boolean} force * If true, reload the text stream even if it did not change. - * @return {boolean} * @private */ switchInternal_(stream, clearBuffer, safeMargin, force) { @@ -495,34 +371,14 @@ shaka.media.StreamingEngine = class { const type = /** @type {!ContentType} */(stream.type); const mediaState = this.mediaStates_.get(type); - if (!mediaState && stream.type == ContentType.TEXT && - this.config_.ignoreTextStreamFailures) { - this.loadNewTextStream(stream); - return true; + if (!mediaState && stream.type == ContentType.TEXT) { + this.loadNewTextStream_(stream); + return; } + goog.asserts.assert(mediaState, 'switch: expected mediaState to exist'); if (!mediaState) { - return false; - } - - // If we are selecting a stream from a different Period, then we need to - // handle a Period transition. Simply ignore the given stream, assuming that - // Player will select the same track in onChooseStreams. - const periodIndex = this.findPeriodContainingStream_(stream); - const mediaStates = Array.from(this.mediaStates_.values()); - const needSamePeriod = mediaStates.every((ms) => { - return ms.needPeriodIndex == mediaState.needPeriodIndex; - }); - if (clearBuffer && periodIndex != mediaState.needPeriodIndex && - needSamePeriod) { - shaka.log.debug('switch: switching to stream in another Period; ' + - 'clearing buffer and changing Periods'); - // handlePeriodTransition_ will be called on the next update because the - // current Period won't match the playhead Period. - for (const mediaState of this.mediaStates_.values()) { - this.forceClearBuffer_(mediaState); - } - return true; + return; } if (mediaState.restoreStreamAfterTrickPlay) { @@ -545,7 +401,7 @@ shaka.media.StreamingEngine = class { if (mediaState.stream == stream && !force) { const streamTag = shaka.media.StreamingEngine.logPrefix_(mediaState); shaka.log.debug('switch: Stream ' + streamTag + ' already active'); - return false; + return; } if (stream.type == ContentType.TEXT) { @@ -558,7 +414,6 @@ shaka.media.StreamingEngine = class { } mediaState.stream = stream; - mediaState.needInitSegment = true; if (mediaState.stream.segmentIndex) { mediaState.segmentIterator = mediaState.stream.segmentIndex[Symbol.iterator](); @@ -600,7 +455,6 @@ shaka.media.StreamingEngine = class { this.playerInterface_.onError(error); } }); - return true; } @@ -719,7 +573,6 @@ shaka.media.StreamingEngine = class { * within the presentation timeline. */ seeked() { - const Iterables = shaka.util.Iterables; const presentationTime = this.playerInterface_.getPresentationTime(); const smallGapLimit = this.config_.smallGapLimit; const newTimeIsBuffered = (type) => { @@ -728,40 +581,15 @@ shaka.media.StreamingEngine = class { }; let streamCleared = false; - const atPeriodIndex = this.findPeriodForTime_(presentationTime); - const allSeekingWithinSamePeriod = Iterables.every( - this.mediaStates_.values(), - (state) => state.needPeriodIndex == atPeriodIndex); - if (allSeekingWithinSamePeriod) { - // If seeking to the same period you were in before, clear buffers - // individually as desired. - for (const type of this.mediaStates_.keys()) { - const bufferEnd = - this.playerInterface_.mediaSourceEngine.bufferEnd(type); - const somethingBuffered = bufferEnd != null; - // Don't clear the buffer unless something is buffered. This extra - // check prevents extra, useless calls to clear the buffer. - if (somethingBuffered && !newTimeIsBuffered(type)) { - // This stream exists, and isn't buffered. - this.forceClearBuffer_(this.mediaStates_.get(type)); - streamCleared = true; - } - } - } else { - // Only treat this as a buffered seek if every media state has a buffer. - // For example, if we have buffered text but not video, we should still - // clear every buffer so all media states need the same Period. - const isAllBuffered = Iterables.every( - this.mediaStates_.keys(), newTimeIsBuffered); - if (!isAllBuffered) { - // This was an unbuffered seek for at least one stream, so clear all - // buffers. - // Don't clear only some of the buffers because we can become stalled - // since the media states are waiting for different Periods. - shaka.log.debug('(all): seeked: unbuffered seek: clearing all buffers'); - for (const mediaState of this.mediaStates_.values()) { - this.forceClearBuffer_(mediaState); - } + for (const type of this.mediaStates_.keys()) { + const bufferEnd = + this.playerInterface_.mediaSourceEngine.bufferEnd(type); + const somethingBuffered = bufferEnd != null; + // Don't clear the buffer unless something is buffered. This extra + // check prevents extra, useless calls to clear the buffer. + if (somethingBuffered && !newTimeIsBuffered(type)) { + // This stream exists, and isn't buffered. + this.forceClearBuffer_(this.mediaStates_.get(type)); streamCleared = true; } } @@ -814,7 +642,7 @@ shaka.media.StreamingEngine = class { shaka.log.debug(logPrefix, 'clear: nothing buffered'); if (mediaState.updateTimer == null) { // Note: an update cycle stops when we buffer to the end of the - // presentation or Period, or when we raise an error. + // presentation, or when we raise an error. this.scheduleUpdate_(mediaState, 0); } return; @@ -833,27 +661,25 @@ shaka.media.StreamingEngine = class { /** - * Initializes the given streams and media states if required. This will - * schedule updates for the given types. + * Initializes the initial streams and media states. This will schedule + * updates for the given types. * - * @param {?shaka.extern.Stream} audio - * @param {?shaka.extern.Stream} video - * @param {?shaka.extern.Stream} text - * @param {number} resumeAt * @return {!Promise} * @private */ - async initStreams_(audio, video, text, resumeAt) { + async initStreams_() { + const ContentType = shaka.util.ManifestParserUtils.ContentType; + goog.asserts.assert(this.config_, 'StreamingEngine configure() must be called before init()!'); - // Determine which Period we must buffer. - const presentationTime = this.playerInterface_.getPresentationTime(); - const needPeriodIndex = this.findPeriodForTime_(presentationTime); - - // Init/re-init MediaSourceEngine. Note that a re-init is only valid for - // text. - const ContentType = shaka.util.ManifestParserUtils.ContentType; + if (!this.currentVariant_) { + shaka.log.error('init: no Streams chosen'); + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.STREAMING, + shaka.util.Error.Code.STREAMING_ENGINE_STARTUP_INVALID_STATE); + } /** * @type {!Map.} */ const streams = new Set(); - if (audio) { - streamsByType.set(ContentType.AUDIO, audio); - streams.add(audio); + if (this.currentVariant_.audio) { + streamsByType.set(ContentType.AUDIO, this.currentVariant_.audio); + streams.add(this.currentVariant_.audio); } - if (video) { - streamsByType.set(ContentType.VIDEO, video); - streams.add(video); + if (this.currentVariant_.video) { + streamsByType.set(ContentType.VIDEO, this.currentVariant_.video); + streams.add(this.currentVariant_.video); } - if (text) { - streamsByType.set(ContentType.TEXT, text); - streams.add(text); + if (this.currentTextStream_) { + streamsByType.set(ContentType.TEXT, this.currentTextStream_); + streams.add(this.currentTextStream_); } // Init MediaSourceEngine. @@ -890,8 +716,7 @@ shaka.media.StreamingEngine = class { for (const type of streamsByType.keys()) { const stream = streamsByType.get(type); if (!this.mediaStates_.has(type)) { - const state = this.createMediaState_( - stream, needPeriodIndex, resumeAt); + const state = this.createMediaState_(stream); this.mediaStates_.set(type, state); this.scheduleUpdate_(state, 0); } @@ -903,12 +728,10 @@ shaka.media.StreamingEngine = class { * Creates a media state. * * @param {shaka.extern.Stream} stream - * @param {number} needPeriodIndex - * @param {number} resumeAt * @return {shaka.media.StreamingEngine.MediaState_} * @private */ - createMediaState_(stream, needPeriodIndex, resumeAt) { + createMediaState_(stream) { const segmentIterator = stream.segmentIndex ? stream.segmentIndex[Symbol.iterator]() : null; return /** @type {shaka.media.StreamingEngine.MediaState_} */ ({ @@ -918,9 +741,10 @@ shaka.media.StreamingEngine = class { segmentIterator, lastSegmentReference: null, lastInitSegmentReference: null, + lastTimestampOffset: null, + lastAppendWindowStart: null, + lastAppendWindowEnd: null, restoreStreamAfterTrickPlay: null, - needInitSegment: true, - needPeriodIndex, endOfStream: false, performingUpdate: false, updateTimer: null, @@ -930,7 +754,6 @@ shaka.media.StreamingEngine = class { clearingBuffer: false, recovering: false, hasError: false, - resumeAt: resumeAt || 0, operation: null, }); } @@ -1027,9 +850,6 @@ shaka.media.StreamingEngine = class { const mediaStates = Array.from(this.mediaStates_.values()); - // Check if we've buffered to the end of the Period. - this.handlePeriodTransition_(mediaState); - // Check if we've buffered to the end of the presentation. We delay adding // the audio and video media states, so it is possible for the text stream // to be the only state and buffer to the end. So we need to wait until we @@ -1088,10 +908,6 @@ shaka.media.StreamingEngine = class { const timeNeeded = this.getTimeNeeded_(mediaState, presentationTime); shaka.log.v2(logPrefix, 'timeNeeded=' + timeNeeded); - const currentPeriodIndex = - this.findPeriodContainingStream_(mediaState.stream); - const needPeriodIndex = this.findPeriodForTime_(timeNeeded); - // Get the amount of content we have buffered, accounting for drift. This // is only used to determine if we have meet the buffering goal. This // should be the same method that PlayheadObserver uses. @@ -1133,21 +949,6 @@ shaka.media.StreamingEngine = class { } mediaState.endOfStream = false; - // Check if we've buffered to the end of the Period. This should be done - // before checking segment availability because the new Period may become - // available once it's switched to. Note that we don't use the non-existence - // of SegmentReferences as an indicator to determine Period boundaries - // because a SegmentIndex can provide SegmentReferences outside its Period. - mediaState.needPeriodIndex = needPeriodIndex; - if (needPeriodIndex != currentPeriodIndex) { - shaka.log.debug(logPrefix, - 'need Period ' + needPeriodIndex, - 'presentationTime=' + presentationTime, - 'timeNeeded=' + timeNeeded, - 'currentPeriodIndex=' + currentPeriodIndex); - return null; - } - // If we've buffered to the buffering goal then schedule an update. if (bufferedAhead >= scaledBufferingGoal) { shaka.log.v2(logPrefix, 'buffering goal met'); @@ -1196,7 +997,6 @@ shaka.media.StreamingEngine = class { return 1; } - mediaState.resumeAt = 0; const p = this.fetchAndAppend_(mediaState, presentationTime, reference); p.catch(() => {}); // TODO(#1993): Handle asynchronous errors. return null; @@ -1220,9 +1020,9 @@ shaka.media.StreamingEngine = class { // the next timestamp we need is actually larger than |bufferEnd|. // 2. There may be drift (the timestamps in the segments are ahead/behind // of the timestamps in the manifest), but we need drift-free times - // when comparing times against presentation and Period boundaries. + // when comparing times against the presentation timeline. if (!mediaState.lastStream || !mediaState.lastSegmentReference) { - return Math.max(presentationTime, mediaState.resumeAt); + return presentationTime; } return mediaState.lastSegmentReference.endTime; @@ -1289,12 +1089,7 @@ shaka.media.StreamingEngine = class { 'lookupTime:', lookupTime, 'presentationTime:', presentationTime); - // Fall back to exact presentation time. - // TODO(#1339): Remove fall back to exact time after period flattening. - // This is only needed now because across periods, we have different - // segment indexes. - const ref = mediaState.segmentIterator.seek(lookupTime) || - mediaState.segmentIterator.seek(presentationTime); + const ref = mediaState.segmentIterator.seek(lookupTime); if (ref == null) { shaka.log.warning(logPrefix, 'cannot find segment', 'lookupTime:', lookupTime, @@ -1360,17 +1155,12 @@ shaka.media.StreamingEngine = class { // Subtlety: The playhead may move while asynchronous update operations are // in progress, so we should avoid calling playhead.getTime() in any // callbacks. Furthermore, switch() may be called at any time, so we should - // also avoid using mediaState.stream or mediaState.needInitSegment in any - // callbacks. + // also avoid using mediaState.stream in any callbacks. const stream = mediaState.stream; - const initSourceBuffer = this.initSourceBuffer_(mediaState, reference); - mediaState.performingUpdate = true; - // We may set |needInitSegment| to true in switch(), so set it to false - // here, since we want it to remain true if switch() is called. - mediaState.needInitSegment = false; + const initSourceBuffer = this.initSourceBuffer_(mediaState, reference); shaka.log.v2(logPrefix, 'fetching segment'); const fetchSegment = this.fetch_(mediaState, reference); @@ -1401,11 +1191,6 @@ shaka.media.StreamingEngine = class { // Update right away. this.scheduleUpdate_(mediaState, 0); - - // Subtlety: handleStartup_() calls onStartupComplete() which may call - // switch() or seeked(), so we must schedule an update beforehand so - // |updateTimer| is set. - this.handleStartup_(mediaState, stream); } catch (error) { this.destroyer_.ensureNotDestroyed(error); if (this.fatalError_) { @@ -1541,10 +1326,10 @@ shaka.media.StreamingEngine = class { /** - * Sets the given MediaState's associated SourceBuffer's timestamp offset and - * init segment if either are required. If an error occurs then neither the - * timestamp offset or init segment are unset, since another call to switch() - * will end up superseding them. + * Sets the given MediaState's associated SourceBuffer's timestamp offset, + * append window, and init segment if they have changed. If an error occurs + * then neither the timestamp offset or init segment are unset, since another + * call to switch() will end up superseding them. * * @param {shaka.media.StreamingEngine.MediaState_} mediaState * @param {!shaka.media.SegmentReference} reference @@ -1555,7 +1340,10 @@ shaka.media.StreamingEngine = class { const StreamingEngine = shaka.media.StreamingEngine; const logPrefix = StreamingEngine.logPrefix_(mediaState); - // Rounding issues can cause us to remove the first frame of the Period, so + /** @type {!Array.} */ + const operations = []; + + // Rounding issues can cause us to remove the first frame of a Period, so // reduce the window start time slightly. const appendWindowStart = Math.max(0, reference.appendWindowStart - @@ -1567,68 +1355,64 @@ shaka.media.StreamingEngine = class { reference.startTime <= appendWindowEnd, logPrefix + ' segment should start before append window end'); - // TODO: Remove needInitSegment. Currently, this both signals the need for - // a different init segment (switches, period transitions) and protects - // against unnecessary calls to setStreamProperties. If we can solve calls - // to setStreamProperties another way, then we could finally drop - // needInitSegment. - if (!mediaState.needInitSegment) { - return; - } - - // If we need an init segment, then the Stream switched, so we've either - // changed bitrates, Periods, or both. If we've changed Periods then we must - // set a new timestamp offset and append window end. Note that by setting - // these values here, we avoid having to co-ordinate ongoing updates, which - // we would have to do if we instead set them in switch(). const timestampOffset = reference.timestampOffset; - shaka.log.v1(logPrefix, 'setting timestamp offset to ' + timestampOffset); - shaka.log.v1(logPrefix, - 'setting append window start to ' + appendWindowStart); - shaka.log.v1(logPrefix, 'setting append window end to ' + appendWindowEnd); - const setStreamProperties = - this.playerInterface_.mediaSourceEngine.setStreamProperties( - mediaState.type, timestampOffset, appendWindowStart, - appendWindowEnd); - - if (reference.initSegmentReference == mediaState.lastInitSegmentReference) { - // The SourceBuffer already has the correct init segment appended. - await setStreamProperties; - return; - } - - mediaState.lastInitSegmentReference = reference.initSegmentReference; - - if (!reference.initSegmentReference) { - // The Stream is self initializing. - await setStreamProperties; - return; - } - - shaka.log.v1(logPrefix, 'fetching init segment'); - - goog.asserts.assert( - reference.initSegmentReference, 'Should have init segment'); - const fetchInit = - this.fetch_(mediaState, reference.initSegmentReference); - const append = async () => { - try { - const initSegment = await fetchInit; - this.destroyer_.ensureNotDestroyed(); - shaka.log.v1(logPrefix, 'appending init segment'); - const hasClosedCaptions = mediaState.stream.closedCaptions && - mediaState.stream.closedCaptions.size > 0; - await this.playerInterface_.mediaSourceEngine.appendBuffer( - mediaState.type, initSegment, /* startTime= */ null, - /* endTime= */ null, hasClosedCaptions); - } catch (error) { - mediaState.needInitSegment = true; - mediaState.lastInitSegmentReference = null; - throw error; + if (timestampOffset != mediaState.lastTimestampOffset || + appendWindowStart != mediaState.lastAppendWindowStart || + appendWindowEnd != mediaState.lastAppendWindowEnd) { + shaka.log.v1(logPrefix, 'setting timestamp offset to ' + timestampOffset); + shaka.log.v1(logPrefix, + 'setting append window start to ' + appendWindowStart); + shaka.log.v1(logPrefix, + 'setting append window end to ' + appendWindowEnd); + + const setProperties = async () => { + try { + mediaState.lastAppendWindowStart = appendWindowStart; + mediaState.lastAppendWindowEnd = appendWindowEnd; + mediaState.lastTimestampOffset = timestampOffset; + + await this.playerInterface_.mediaSourceEngine.setStreamProperties( + mediaState.type, timestampOffset, appendWindowStart, + appendWindowEnd); + } catch (error) { + mediaState.lastAppendWindowStart = null; + mediaState.lastAppendWindowEnd = null; + mediaState.lastTimestampOffset = null; + + throw error; + } + }; + operations.push(setProperties()); + } + + if (reference.initSegmentReference != mediaState.lastInitSegmentReference) { + mediaState.lastInitSegmentReference = reference.initSegmentReference; + + if (reference.initSegmentReference) { + shaka.log.v1(logPrefix, 'fetching init segment'); + + const fetchInit = + this.fetch_(mediaState, reference.initSegmentReference); + const append = async () => { + try { + const initSegment = await fetchInit; + this.destroyer_.ensureNotDestroyed(); + shaka.log.v1(logPrefix, 'appending init segment'); + const hasClosedCaptions = mediaState.stream.closedCaptions && + mediaState.stream.closedCaptions.size > 0; + await this.playerInterface_.mediaSourceEngine.appendBuffer( + mediaState.type, initSegment, /* startTime= */ null, + /* endTime= */ null, hasClosedCaptions); + } catch (error) { + mediaState.lastInitSegmentReference = null; + throw error; + } + }; + operations.push(append()); } - }; + } - await Promise.all([setStreamProperties, append()]); + await Promise.all(operations); } @@ -1671,7 +1455,8 @@ shaka.media.StreamingEngine = class { this.destroyer_.ensureNotDestroyed(); shaka.log.v2(logPrefix, 'appended media segment'); - // We must use |stream| because switch() may have been called. + // We must use |stream| instead of |mediaState.stream| because switch() may + // have been called. mediaState.lastStream = stream; mediaState.lastSegmentReference = reference; // Only advance the segmentIterator if the stream hasn't changed. @@ -1789,261 +1574,6 @@ shaka.media.StreamingEngine = class { } - /** - * Sets up all known Periods when startup completes; otherwise, does nothing. - * - * @param {shaka.media.StreamingEngine.MediaState_} mediaState The last - * MediaState updated. - * @param {shaka.extern.Stream} stream - * @private - */ - handleStartup_(mediaState, stream) { - const ContentType = shaka.util.ManifestParserUtils.ContentType; - if (this.startupComplete_) { - return; - } - - const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState); - - // If the only media state is text, then we may have loaded text before - // any media content. Marking as complete early will break MediaSource. - // See #1696. - const mediaStates = Array.from(this.mediaStates_.values()); - if (mediaStates.length != 1 || mediaStates[0].type != ContentType.TEXT) { - this.startupComplete_ = mediaStates.every((ms) => { - // Startup completes once we have buffered at least one segment from - // each MediaState, not counting text. - if (ms.type == ContentType.TEXT) { - return true; - } - return !ms.waitingToClearBuffer && - !ms.clearingBuffer && - ms.lastSegmentReference; - }); - } - - if (!this.startupComplete_) { - return; - } - - shaka.log.debug(logPrefix, 'startup complete'); - - // We must use |stream| because switch() may have been called. - const currentPeriodIndex = this.findPeriodContainingStream_(stream); - - goog.asserts.assert( - mediaStates.every((ms) => { - // It is possible for one stream (usually text) to buffer the whole - // Period and need the next one. - return ms.needPeriodIndex == currentPeriodIndex || - ms.needPeriodIndex == currentPeriodIndex + 1; - }), - logPrefix + ' expected all MediaStates to need same Period'); - - // Since period setup is no longer required, call onCanSwitch() once - // startup is complete. - this.playerInterface_.onCanSwitch(); - - if (this.playerInterface_.onStartupComplete) { - shaka.log.v1(logPrefix, 'calling onStartupComplete()...'); - this.playerInterface_.onStartupComplete(); - } - } - - - /** - * Calls onChooseStreams() when necessary. - * - * @param {shaka.media.StreamingEngine.MediaState_} mediaState The last - * MediaState updated. - * @private - */ - handlePeriodTransition_(mediaState) { - const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState); - const ContentType = shaka.util.ManifestParserUtils.ContentType; - - const currentPeriodIndex = - this.findPeriodContainingStream_(mediaState.stream); - if (mediaState.needPeriodIndex == currentPeriodIndex) { - return; - } - - const needPeriodIndex = mediaState.needPeriodIndex; - - /** @type {Array.} */ - const mediaStates = Array.from(this.mediaStates_.values()); - - // For a Period transition to work, all media states must need the same - // Period. If a stream needs a different Period than the one it currently - // has, it will try to transition or stop updates assuming that another - // streamwill handle it. - // This only works when all streams either need the same Period or are still - // performing updates. - goog.asserts.assert( - mediaStates.every((ms) => { - return ms.needPeriodIndex == needPeriodIndex || ms.hasError || - !shaka.media.StreamingEngine.isIdle_(ms) || - shaka.media.StreamingEngine.isEmbeddedText_(ms); - }), 'All MediaStates should need the same Period or be performing' + - 'updates.'); - - // Only call onChooseStreams() when all MediaStates need the same Period. - const needSamePeriod = mediaStates.every((ms) => { - // Ignore embedded text streams since they are based on the video stream. - return ms.needPeriodIndex == needPeriodIndex || - shaka.media.StreamingEngine.isEmbeddedText_(ms); - }); - if (!needSamePeriod) { - shaka.log.debug( - logPrefix, 'not all MediaStates need Period ' + needPeriodIndex); - return; - } - - // Only call onChooseStreams() once per Period transition. - const allAreIdle = mediaStates.every(shaka.media.StreamingEngine.isIdle_); - if (!allAreIdle) { - shaka.log.debug( - logPrefix, - 'all MediaStates need Period ' + needPeriodIndex + ', ' + - 'but not all MediaStates are idle'); - return; - } - - shaka.log.debug(logPrefix, 'all need Period ' + needPeriodIndex); - - // Ensure the Period which we need to buffer is set up and then call - // onChooseStreams(). - try { - // If we seek during a Period transition, we can start another transition. - // So we need to verify that: - // 1. We are still in need of the same Period. - // 2. All streams are still idle. - // 3. The current stream is not in the needed Period (another transition - // handled it). - const allReady = mediaStates.every((ms) => { - const isIdle = shaka.media.StreamingEngine.isIdle_(ms); - const currentPeriodIndex = this.findPeriodContainingStream_(ms.stream); - if (shaka.media.StreamingEngine.isEmbeddedText_(ms)) { - // Embedded text tracks don't do Period transitions. - return true; - } - return isIdle && ms.needPeriodIndex == needPeriodIndex && - currentPeriodIndex != needPeriodIndex; - }); - if (!allReady) { - // TODO: Write unit tests for this case. - shaka.log.debug(logPrefix, 'ignoring transition to Period', - needPeriodIndex, 'since another is happening'); - return; - } - - const needPeriod = this.manifest_.periods[needPeriodIndex]; - - shaka.log.v1(logPrefix, 'calling onChooseStreams()...'); - const chosenStreams = this.playerInterface_.onChooseStreams(needPeriod); - - /** @type {!Map.} */ - const streamsByType = new Map(); - if (chosenStreams.variant && chosenStreams.variant.video) { - streamsByType.set(ContentType.VIDEO, chosenStreams.variant.video); - } - if (chosenStreams.variant && chosenStreams.variant.audio) { - streamsByType.set(ContentType.AUDIO, chosenStreams.variant.audio); - } - if (chosenStreams.text) { - streamsByType.set(ContentType.TEXT, chosenStreams.text); - } - - // Vet |streamsByType| before switching. - for (const type of this.mediaStates_.keys()) { - if (streamsByType.has(type) || type == ContentType.TEXT) { - continue; - } - - shaka.log.error(logPrefix, - 'invalid Streams chosen: missing ' + type + ' Stream'); - this.playerInterface_.onError(new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.STREAMING, - shaka.util.Error.Code.INVALID_STREAMS_CHOSEN)); - return; - } - - // Because we are going to modify the map, we need to create a copy of the - // keys, so copy the iterable to an array first. - for (const type of Array.from(streamsByType.keys())) { - if (this.mediaStates_.has(type)) { - continue; - } - - if (type == ContentType.TEXT) { - // initStreams_ will switch streams and schedule an update. - this.initStreams_( - /* audio= */ null, - /* video= */ null, - /* text= */ streamsByType.get(ContentType.TEXT), - needPeriod.startTime); - streamsByType.delete(type); - continue; - } - - shaka.log.error(logPrefix, - 'invalid Streams chosen: unusable ' + type + ' Stream'); - this.playerInterface_.onError(new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.STREAMING, - shaka.util.Error.Code.INVALID_STREAMS_CHOSEN)); - return; - } - - // Because we are going to modify the map, we need to create a copy of the - // keys, so copy the iterable to an array first. - const copyOfStateTypes = Array.from(this.mediaStates_.keys()); - for (const type of copyOfStateTypes) { - const state = this.mediaStates_.get(type); - const stream = streamsByType.get(type); - if (stream) { - const wasEmbeddedText = - shaka.media.StreamingEngine.isEmbeddedText_(state); - if (wasEmbeddedText) { - // If this was an embedded text track, we'll need to update the - // needPeriodIndex so it doesn't try to do a Period transition once - // we switch. - state.needPeriodIndex = needPeriodIndex; - state.resumeAt = needPeriod.startTime; - } - - this.switchInternal_( - stream, - /* clearBuffer= */ false, - /* safeMargin= */ 0, - /* force= */ false); - - // Don't schedule an update when changing from embedded text to - // another embedded text since the update will try to load existing - // captions, which are already loaded. - // - // But we do want to schedule an update if we switch to a non-embedded - // text track of if we didn't have an embedded text track before. - if (!wasEmbeddedText || - !shaka.media.StreamingEngine.isEmbeddedText_(state)) { - const mediaState = this.mediaStates_.get(type); - this.scheduleUpdate_(mediaState, 0); - } - } else { - goog.asserts.assert(type == ContentType.TEXT, - 'Invalid streams chosen'); - this.mediaStates_.delete(type); - } - } - - // All streams for the new period are active, so call onCanSwitch(). - shaka.log.v1(logPrefix, 'calling onCanSwitch()...'); - this.playerInterface_.onCanSwitch(); - } catch (e) {} - } - /** * @param {shaka.media.StreamingEngine.MediaState_} mediaState * @return {boolean} @@ -2057,69 +1587,6 @@ shaka.media.StreamingEngine = class { } - /** - * @param {shaka.media.StreamingEngine.MediaState_} mediaState - * @return {boolean} True if the given MediaState is idle; otherwise, return - * false. - * @private - */ - static isIdle_(mediaState) { - return !mediaState.performingUpdate && - (mediaState.updateTimer == null) && - !mediaState.waitingToClearBuffer && - !mediaState.clearingBuffer; - } - - - /** - * Get the index in the manifest of the period that contains the given - * presentation time. If |time| is before all periods, this will default to - * returning the first period. - * - * @param {number} time The presentation time in seconds. - * @return {number} - * @private - */ - findPeriodForTime_(time) { - const ManifestParserUtils = shaka.util.ManifestParserUtils; - const threshold = ManifestParserUtils.GAP_OVERLAP_TOLERANCE_SECONDS; - - // The last segment may end right before the end of the Period because of - // rounding issues so we bias forward a little. - const adjustedTime = time + threshold; - - const period = shaka.util.Periods.findPeriodForTime( - /* periods= */ this.manifest_.periods, - /* time= */ adjustedTime); - - return period ? this.manifest_.periods.indexOf(period) : 0; - } - - - /** - * See if |stream| can be found in our manifest and return the period index. - * If |stream| cannot be found, -1 will be returned. - * - * @param {!shaka.extern.Stream} stream - * @return {number} - * @private - */ - findPeriodContainingStream_(stream) { - return this.manifest_.periods.findIndex((period) => { - for (const variant of period.variants) { - if (variant.audio == stream || variant.video == stream) { - return true; - } - if (variant.video && variant.video.trickModeVideo == stream) { - return true; - } - } - - return period.textStreams.includes(stream); - }); - } - - /** * Fetches the given segment. * @@ -2276,35 +1743,16 @@ shaka.media.StreamingEngine = class { }; -/** - * @typedef {{ - * variant: (?shaka.extern.Variant|undefined), - * text: ?shaka.extern.Stream - * }} - * - * @property {(?shaka.extern.Variant|undefined)} variant - * The chosen variant. May be omitted for text re-init. - * @property {?shaka.extern.Stream} text - * The chosen text stream. - */ -shaka.media.StreamingEngine.ChosenStreams; - - /** * @typedef {{ * getPresentationTime: function():number, * getBandwidthEstimate: function():number, * mediaSourceEngine: !shaka.media.MediaSourceEngine, * netEngine: shaka.net.NetworkingEngine, - * onChooseStreams: function(!shaka.extern.Period): - * shaka.media.StreamingEngine.ChosenStreams, - * onCanSwitch: function(), * onError: function(!shaka.util.Error), * onEvent: function(!Event), * onManifestUpdate: function(), - * onSegmentAppended: function(), - * onInitialStreamsSetup: (function()|undefined), - * onStartupComplete: (function()|undefined) + * onSegmentAppended: function() * }} * * @property {function():number} getPresentationTime @@ -2316,15 +1764,6 @@ shaka.media.StreamingEngine.ChosenStreams; * The MediaSourceEngine. The caller retains ownership. * @property {shaka.net.NetworkingEngine} netEngine * The NetworkingEngine instance to use. The caller retains ownership. - * @property {function(!shaka.extern.Period): - * shaka.media.StreamingEngine.ChosenStreams} onChooseStreams - * Called by StreamingEngine when the given Period needs to be buffered. - * StreamingEngine will switch to the variant and text stream returned from - * this function. - * The owner cannot call switch() directly until the StreamingEngine calls - * onCanSwitch(). - * @property {function()} onCanSwitch - * Called by StreamingEngine when switching is permitted. * @property {function(!shaka.util.Error)} onError * Called when an error occurs. If the error is recoverable (see * {@link shaka.util.Error}) then the caller may invoke either @@ -2335,12 +1774,6 @@ shaka.media.StreamingEngine.ChosenStreams; * Called when an embedded 'emsg' box should trigger a manifest update. * @property {function()} onSegmentAppended * Called after a segment is successfully appended to a MediaSource. - * @property {(function()|undefined)} onInitialStreamsSetup - * Optional callback which is called when the initial set of Streams have been - * setup. Intended to be used by tests. - * @property {(function()|undefined)} onStartupComplete - * Optional callback which is called when startup has completed. Intended to - * be used by tests. */ shaka.media.StreamingEngine.PlayerInterface; @@ -2353,9 +1786,10 @@ shaka.media.StreamingEngine.PlayerInterface; * segmentIterator: shaka.media.SegmentIterator, * lastSegmentReference: shaka.media.SegmentReference, * lastInitSegmentReference: shaka.media.InitSegmentReference, + * lastTimestampOffset: ?number, + * lastAppendWindowStart: ?number, + * lastAppendWindowEnd: ?number, * restoreStreamAfterTrickPlay: ?shaka.extern.Stream, - * needInitSegment: boolean, - * needPeriodIndex: number, * endOfStream: boolean, * performingUpdate: boolean, * updateTimer: shaka.util.DelayedTick, @@ -2365,7 +1799,6 @@ shaka.media.StreamingEngine.PlayerInterface; * clearingBuffer: boolean, * recovering: boolean, * hasError: boolean, - * resumeAt: number, * operation: shaka.net.NetworkingEngine.PendingRequest * }} * @@ -2386,16 +1819,17 @@ shaka.media.StreamingEngine.PlayerInterface; * The SegmentReference of the last segment that was appended. * @property {shaka.media.InitSegmentReference} lastInitSegmentReference * The InitSegmentReference of the last init segment that was appended. + * @property {?number} lastTimestampOffset + * The last timestamp offset given to MediaSourceEngine for this type. + * @property {?number} lastAppendWindowStart + * The last append window start given to MediaSourceEngine for this type. + * @property {?number} lastAppendWindowEnd + * The last append window end given to MediaSourceEngine for this type. * @property {?shaka.extern.Stream} restoreStreamAfterTrickPlay * The Stream to restore after trick play mode is turned off. - * @property {boolean} needInitSegment - * True indicates that |stream|'s init segment must be inserted before the - * next media segment is appended. * @property {boolean} endOfStream * True indicates that the end of the buffer has hit the end of the * presentation. - * @property {number} needPeriodIndex - * The index of the Period which needs to be buffered. * @property {boolean} performingUpdate * True indicates that an update is in progress. * @property {shaka.util.DelayedTick} updateTimer @@ -2415,10 +1849,6 @@ shaka.media.StreamingEngine.PlayerInterface; * @property {boolean} hasError * True indicates that the stream has encountered an error and has stopped * updating. - * @property {number} resumeAt - * An override for the time to start performing updates at. If the playhead - * is behind this time, update_() will still start fetching segments from - * this time. If the playhead is ahead of the time, this field is ignored. * @property {shaka.net.NetworkingEngine.PendingRequest} operation * Operation with the number of bytes to be downloaded. */ diff --git a/lib/offline/download_manager.js b/lib/offline/download_manager.js index 13bffeeed4..391ed7d56b 100644 --- a/lib/offline/download_manager.js +++ b/lib/offline/download_manager.js @@ -88,6 +88,7 @@ shaka.offline.DownloadManager = class { * @param {function(BufferSource):!Promise} onDownloaded * The callback for when this request has been downloaded. Downloading for * |group| will pause until the promise returned by |onDownloaded| resolves. + * @return {!Promise} Resolved when this request is complete. */ queue(groupId, request, estimatedByteLength, isInitSegment, onDownloaded) { this.destroyer_.ensureNotDestroyed(); @@ -97,7 +98,7 @@ shaka.offline.DownloadManager = class { const group = this.groups_.get(groupId) || Promise.resolve(); // Add another download to the group. - this.groups_.set(groupId, group.then(async () => { + const newPromise = group.then(async () => { const response = await this.fetchSegment_(request); // Make sure we stop downloading if we have been destroyed. @@ -127,7 +128,31 @@ shaka.offline.DownloadManager = class { this.estimator_.getTotalDownloaded()); return onDownloaded(response); - })); + }); + + this.groups_.set(groupId, newPromise); + return newPromise; + } + + /** + * Add additional async work to the group work queue. + * + * @param {number} groupId + * The group to add this group to. If the group does not exist, a new + * group will be created. + * @param {function():!Promise} callback + * The callback for the async work. Downloading for this group will be + * blocked until the Promise returned by |callback| resolves. + * @return {!Promise} Resolved when this work is complete. + */ + queueWork(groupId, callback) { + this.destroyer_.ensureNotDestroyed(); + const group = this.groups_.get(groupId) || Promise.resolve(); + const newPromise = group.then(async () => { + await callback(); + }); + this.groups_.set(groupId, newPromise); + return newPromise; } /** diff --git a/lib/offline/indexeddb/storage_mechanism.js b/lib/offline/indexeddb/storage_mechanism.js index 5319cd574f..8b85f26b97 100644 --- a/lib/offline/indexeddb/storage_mechanism.js +++ b/lib/offline/indexeddb/storage_mechanism.js @@ -11,6 +11,7 @@ goog.require('shaka.offline.StorageMuxer'); goog.require('shaka.offline.indexeddb.EmeSessionStorageCell'); goog.require('shaka.offline.indexeddb.V1StorageCell'); goog.require('shaka.offline.indexeddb.V2StorageCell'); +goog.require('shaka.offline.indexeddb.V5StorageCell'); goog.require('shaka.util.Error'); goog.require('shaka.util.PublicPromise'); @@ -37,6 +38,8 @@ shaka.offline.indexeddb.StorageMechanism = class { this.v2_ = null; /** @private {shaka.extern.StorageCell} */ this.v3_ = null; + /** @private {shaka.extern.StorageCell} */ + this.v5_ = null; /** @private {shaka.extern.EmeSessionStorageCell} */ this.sessions_ = null; } @@ -56,8 +59,12 @@ shaka.offline.indexeddb.StorageMechanism = class { this.v1_ = shaka.offline.indexeddb.StorageMechanism.createV1_(db); this.v2_ = shaka.offline.indexeddb.StorageMechanism.createV2_(db); this.v3_ = shaka.offline.indexeddb.StorageMechanism.createV3_(db); + // NOTE: V4 of the database was when we introduced a special table to + // store EME session IDs. It has no separate storage cell, so we skip to + // V5. + this.v5_ = shaka.offline.indexeddb.StorageMechanism.createV5_(db); this.sessions_ = - shaka.offline.indexeddb.StorageMechanism.createEmeSession_(db); + shaka.offline.indexeddb.StorageMechanism.createEmeSessionCell_(db); p.resolve(); }; open.onupgradeneeded = (event) => { @@ -91,6 +98,9 @@ shaka.offline.indexeddb.StorageMechanism = class { if (this.v3_) { await this.v3_.destroy(); } + if (this.v5_) { + await this.v5_.destroy(); + } if (this.sessions_) { await this.sessions_.destroy(); } @@ -116,6 +126,9 @@ shaka.offline.indexeddb.StorageMechanism = class { if (this.v3_) { map.set('v3', this.v3_); } + if (this.v5_) { + map.set('v5', this.v5_); + } return map; } @@ -143,6 +156,9 @@ shaka.offline.indexeddb.StorageMechanism = class { if (this.v3_) { await this.v3_.destroy(); } + if (this.v5_) { + await this.v5_.destroy(); + } // |db_| will only be null if the muxer was not initialized. We need to // close the connection in order delete the database without it being @@ -158,6 +174,7 @@ shaka.offline.indexeddb.StorageMechanism = class { this.v1_ = null; this.v2_ = null; this.v3_ = null; + this.v5_ = null; await this.init(); } @@ -229,12 +246,33 @@ shaka.offline.indexeddb.StorageMechanism = class { return null; } + /** + * @param {!IDBDatabase} db + * @return {shaka.extern.StorageCell} + * @private + */ + static createV5_(db) { + const StorageMechanism = shaka.offline.indexeddb.StorageMechanism; + const segmentStore = StorageMechanism.V5_SEGMENT_STORE; + const manifestStore = StorageMechanism.V5_MANIFEST_STORE; + const stores = db.objectStoreNames; + if (stores.contains(manifestStore) && stores.contains(segmentStore)) { + shaka.log.debug('Mounting v5 idb storage cell'); + + return new shaka.offline.indexeddb.V5StorageCell( + db, + segmentStore, + manifestStore); + } + return null; + } + /** * @param {!IDBDatabase} db * @return {shaka.extern.EmeSessionStorageCell} * @private */ - static createEmeSession_(db) { + static createEmeSessionCell_(db) { const StorageMechanism = shaka.offline.indexeddb.StorageMechanism; const store = StorageMechanism.SESSION_ID_STORE; if (db.objectStoreNames.contains(store)) { @@ -250,8 +288,8 @@ shaka.offline.indexeddb.StorageMechanism = class { */ createStores_(db) { const storeNames = [ - shaka.offline.indexeddb.StorageMechanism.V3_SEGMENT_STORE, - shaka.offline.indexeddb.StorageMechanism.V3_MANIFEST_STORE, + shaka.offline.indexeddb.StorageMechanism.V5_SEGMENT_STORE, + shaka.offline.indexeddb.StorageMechanism.V5_MANIFEST_STORE, shaka.offline.indexeddb.StorageMechanism.SESSION_ID_STORE, ]; @@ -299,7 +337,7 @@ shaka.offline.indexeddb.StorageMechanism = class { /** @const {string} */ shaka.offline.indexeddb.StorageMechanism.DB_NAME = 'shaka_offline_db'; /** @const {number} */ -shaka.offline.indexeddb.StorageMechanism.VERSION = 4; +shaka.offline.indexeddb.StorageMechanism.VERSION = 5; /** @const {string} */ shaka.offline.indexeddb.StorageMechanism.V1_SEGMENT_STORE = 'segment'; /** @const {string} */ @@ -307,12 +345,16 @@ shaka.offline.indexeddb.StorageMechanism.V2_SEGMENT_STORE = 'segment-v2'; /** @const {string} */ shaka.offline.indexeddb.StorageMechanism.V3_SEGMENT_STORE = 'segment-v3'; /** @const {string} */ +shaka.offline.indexeddb.StorageMechanism.V5_SEGMENT_STORE = 'segment-v5'; +/** @const {string} */ shaka.offline.indexeddb.StorageMechanism.V1_MANIFEST_STORE = 'manifest'; /** @const {string} */ shaka.offline.indexeddb.StorageMechanism.V2_MANIFEST_STORE = 'manifest-v2'; /** @const {string} */ shaka.offline.indexeddb.StorageMechanism.V3_MANIFEST_STORE = 'manifest-v3'; /** @const {string} */ +shaka.offline.indexeddb.StorageMechanism.V5_MANIFEST_STORE = 'manifest-v5'; +/** @const {string} */ shaka.offline.indexeddb.StorageMechanism.SESSION_ID_STORE = 'session-ids'; diff --git a/lib/offline/indexeddb/v1_storage_cell.js b/lib/offline/indexeddb/v1_storage_cell.js index 9dd68a1f5f..ecdb0225b1 100644 --- a/lib/offline/indexeddb/v1_storage_cell.js +++ b/lib/offline/indexeddb/v1_storage_cell.js @@ -10,6 +10,7 @@ goog.require('shaka.log'); goog.require('shaka.offline.indexeddb.BaseStorageCell'); goog.require('shaka.util.Error'); goog.require('shaka.util.ManifestParserUtils'); +goog.require('shaka.util.Periods'); goog.require('shaka.util.PublicPromise'); @@ -72,12 +73,24 @@ shaka.offline.indexeddb.V1StorageCell = class convertManifest(old) { const V1StorageCell = shaka.offline.indexeddb.V1StorageCell; + const streamsPerPeriod = []; + + for (let i = 0; i < old.periods.length; ++i) { + // The last period ends at the end of the presentation. + const periodEnd = i == old.periods.length - 1 ? + old.duration : old.periods[i + 1].startTime; + const duration = periodEnd - old.periods[i].startTime; + const streams = V1StorageCell.convertPeriod_(old.periods[i], duration); + + streamsPerPeriod.push(streams); + } + return { originalManifestUri: old.originalManifestUri, duration: old.duration, size: old.size, expiration: old.expiration == null ? Infinity : old.expiration, - periods: old.periods.map(V1StorageCell.convertPeriod_), + streams: shaka.util.Periods.stitchStreamDBs(streamsPerPeriod), sessionIds: old.sessionIds, drmInfo: old.drmInfo, appMetadata: old.appMetadata, @@ -86,10 +99,11 @@ shaka.offline.indexeddb.V1StorageCell = class /** * @param {shaka.extern.PeriodDBV1} old - * @return {shaka.extern.PeriodDB} + * @param {number} periodDuration + * @return {!Array.} * @private */ - static convertPeriod_(old) { + static convertPeriod_(old, periodDuration) { const V1StorageCell = shaka.offline.indexeddb.V1StorageCell; // In the case that this is really old (like really old, like dinosaurs @@ -102,28 +116,35 @@ shaka.offline.indexeddb.V1StorageCell = class goog.asserts.assert(stream.variantIds, message); } - return { - startTime: old.startTime, - streams: old.streams.map(V1StorageCell.convertStream_), - }; + return old.streams.map((stream) => V1StorageCell.convertStream_( + stream, old.startTime, periodDuration)); } /** * @param {shaka.extern.StreamDBV1} old + * @param {number} periodStart + * @param {number} periodDuration * @return {shaka.extern.StreamDB} * @private */ - static convertStream_(old) { + static convertStream_(old, periodStart, periodDuration) { const V1StorageCell = shaka.offline.indexeddb.V1StorageCell; const initSegmentKey = old.initSegmentUri ? V1StorageCell.getKeyFromSegmentUri_(old.initSegmentUri) : null; + // timestampOffset in the new format is the inverse of + // presentationTimeOffset in the old format. Also, PTO did not include the + // period start, while TO does. + const timestampOffset = periodStart + old.presentationTimeOffset; + + const appendWindowStart = periodStart; + const appendWindowEnd = periodStart + periodDuration; + return { id: old.id, originalId: null, primary: old.primary, - presentationTimeOffset: old.presentationTimeOffset, contentType: old.contentType, mimeType: old.mimeType, codecs: old.codecs, @@ -136,18 +157,30 @@ shaka.offline.indexeddb.V1StorageCell = class height: old.height, initSegmentKey: initSegmentKey, encrypted: old.encrypted, - keyId: old.keyId, - segments: old.segments.map(V1StorageCell.convertSegment_), + keyIds: [old.keyId], + segments: old.segments.map((segment) => V1StorageCell.convertSegment_( + segment, initSegmentKey, appendWindowStart, appendWindowEnd, + timestampOffset)), variantIds: old.variantIds, + roles: [], + audioSamplingRate: null, + channelsCount: null, + closedCaptions: null, }; } /** * @param {shaka.extern.SegmentDBV1} old + * @param {?number} initSegmentKey + * @param {number} appendWindowStart + * @param {number} appendWindowEnd + * @param {number} timestampOffset * @return {shaka.extern.SegmentDB} * @private */ - static convertSegment_(old) { + static convertSegment_( + old, initSegmentKey, appendWindowStart, appendWindowEnd, + timestampOffset) { const V1StorageCell = shaka.offline.indexeddb.V1StorageCell; // Since we don't want to use the uri anymore, we need to parse the key @@ -155,9 +188,13 @@ shaka.offline.indexeddb.V1StorageCell = class const dataKey = V1StorageCell.getKeyFromSegmentUri_(old.uri); return { - startTime: old.startTime, - endTime: old.endTime, - dataKey: dataKey, + startTime: appendWindowStart + old.startTime, + endTime: appendWindowStart + old.endTime, + dataKey, + initSegmentKey, + appendWindowStart, + appendWindowEnd, + timestampOffset, }; } diff --git a/lib/offline/indexeddb/v2_storage_cell.js b/lib/offline/indexeddb/v2_storage_cell.js index 80888b8c86..bfceae4716 100644 --- a/lib/offline/indexeddb/v2_storage_cell.js +++ b/lib/offline/indexeddb/v2_storage_cell.js @@ -6,12 +6,13 @@ goog.provide('shaka.offline.indexeddb.V2StorageCell'); goog.require('shaka.offline.indexeddb.BaseStorageCell'); +goog.require('shaka.util.Periods'); /** * The V2StorageCell is for all stores that follow the shaka.externs V2 and V3 * offline types. V2 was introduced in Shaka Player v2.3.0 and quickly - * replaced with V3 in Shaka Player v2.3.2. + * replaced with V3 in Shaka Player v2.3.2. V3 was then deprecated in v2.6. * * Upgrading from V1 to V2 initially broke the database in a way that prevented * adding new records. The problem was with the upgrade process, not with the @@ -19,54 +20,112 @@ goog.require('shaka.offline.indexeddb.BaseStorageCell'); * database version to V3 and marked V2 as read-only. Therefore, V2 and V3 * databases can both be read by this cell. * + * The manifest and segment stores didn't change in database V4, but a separate + * table for session IDs was added. So this cell also covers DB V4. + * * @implements {shaka.extern.StorageCell} */ shaka.offline.indexeddb.V2StorageCell = class extends shaka.offline.indexeddb.BaseStorageCell { /** - * @param {IDBDatabase} connection - * @param {string} segmentStore - * @param {string} manifestStore - * @param {boolean} isFixedKey + * @override + * @param {shaka.extern.ManifestDBV2} old + * @return {shaka.extern.ManifestDB} */ - constructor(connection, segmentStore, manifestStore, isFixedKey) { - super(connection, segmentStore, manifestStore); + convertManifest(old) { + const streamsPerPeriod = []; - /** @private {boolean} */ - this.isFixedKey_ = isFixedKey; - } + for (let i = 0; i < old.periods.length; ++i) { + // The last period ends at the end of the presentation. + const periodEnd = i == old.periods.length - 1 ? + old.duration : old.periods[i + 1].startTime; + const duration = periodEnd - old.periods[i].startTime; + const streams = this.convertPeriod_(old.periods[i], duration); - /** @override */ - hasFixedKeySpace() { - return this.isFixedKey_; + streamsPerPeriod.push(streams); + } + + return { + appMetadata: old.appMetadata, + drmInfo: old.drmInfo, + duration: old.duration, + // JSON serialization turns Infinity into null, so turn it back now. + expiration: old.expiration == null ? Infinity : old.expiration, + originalManifestUri: old.originalManifestUri, + sessionIds: old.sessionIds, + size: old.size, + streams: shaka.util.Periods.stitchStreamDBs(streamsPerPeriod), + }; } - /** @override */ - addSegments(segments) { - if (this.isFixedKey_) { - return this.rejectAdd(this.segmentStore_); - } - return this.add(this.segmentStore_, segments); + /** + * @param {shaka.extern.PeriodDBV2} period + * @param {number} periodDuration + * @return {!Array.} + * @private + */ + convertPeriod_(period, periodDuration) { + return period.streams.map((stream) => this.convertStream_( + stream, period.startTime, period.startTime + periodDuration)); } - /** @override */ - addManifests(manifests) { - if (this.isFixedKey_) { - return this.rejectAdd(this.manifestStore_); - } - return this.add(this.manifestStore_, manifests); + /** + * @param {shaka.extern.StreamDBV2} old + * @param {number} periodStart + * @param {number} periodEnd + * @return {shaka.extern.StreamDB} + * @private + */ + convertStream_(old, periodStart, periodEnd) { + return { + id: old.id, + originalId: old.originalId, + primary: old.primary, + contentType: old.contentType, + mimeType: old.mimeType, + codecs: old.codecs, + frameRate: old.frameRate, + pixelAspectRatio: old.pixelAspectRatio, + kind: old.kind, + language: old.language, + label: old.label, + width: old.width, + height: old.height, + encrypted: old.encrypted, + keyIds: [old.keyId], + segments: old.segments.map((segment) => + this.convertSegment_( + segment, old.initSegmentKey, periodStart, periodEnd, + old.presentationTimeOffset)), + variantIds: old.variantIds, + roles: [], + audioSamplingRate: null, + channelsCount: null, + closedCaptions: null, + }; } /** - * @override - * @param {shaka.extern.ManifestDB} old - * @return {shaka.extern.ManifestDB} + * @param {shaka.extern.SegmentDBV2} old + * @param {?number} initSegmentKey + * @param {number} periodStart + * @param {number} periodEnd + * @param {number} presentationTimeOffset + * @return {shaka.extern.SegmentDB} + * @private */ - convertManifest(old) { - // JSON serialization turns Infinity into null, so turn it back now. - if (old.expiration == null) { - old.expiration = Infinity; - } - return old; + convertSegment_( + old, initSegmentKey, periodStart, periodEnd, presentationTimeOffset) { + const timestampOffset = periodStart - presentationTimeOffset; + + return { + startTime: periodStart + old.startTime, + endTime: periodStart + old.endTime, + initSegmentKey, + appendWindowStart: periodStart, + appendWindowEnd: periodEnd, + timestampOffset, + dataKey: old.dataKey, + }; } }; diff --git a/lib/offline/indexeddb/v5_storage_cell.js b/lib/offline/indexeddb/v5_storage_cell.js new file mode 100644 index 0000000000..5c3018c45e --- /dev/null +++ b/lib/offline/indexeddb/v5_storage_cell.js @@ -0,0 +1,43 @@ +/** @license + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.offline.indexeddb.V5StorageCell'); + +goog.require('shaka.offline.indexeddb.BaseStorageCell'); + + +/** + * The V5StorageCell is for all stores that follow the shaka.externs V5 offline + * types introduced in v2.6. + * + * @implements {shaka.extern.StorageCell} + */ +shaka.offline.indexeddb.V5StorageCell = class + extends shaka.offline.indexeddb.BaseStorageCell { + /** @override */ + hasFixedKeySpace() { + // This makes the cell read-write. + return false; + } + + /** @override */ + addSegments(segments) { + return this.add(this.segmentStore_, segments); + } + + /** @override */ + addManifests(manifests) { + return this.add(this.manifestStore_, manifests); + } + + /** @override */ + convertManifest(old) { + // JSON serialization turns Infinity into null, so turn it back now. + if (old.expiration == null) { + old.expiration = Infinity; + } + return /** @type {shaka.extern.ManifestDB} */(old); + } +}; diff --git a/lib/offline/manifest_converter.js b/lib/offline/manifest_converter.js index 77e23cd9b1..6aa5939e11 100644 --- a/lib/offline/manifest_converter.js +++ b/lib/offline/manifest_converter.js @@ -46,62 +46,33 @@ shaka.offline.ManifestConverter = class { const timeline = new shaka.media.PresentationTimeline(null, 0); timeline.setDuration(manifestDB.duration); - const periods = []; - const iterator = shaka.util.Iterables.enumerate(manifestDB.periods); - for (const {item: periodDB, next: nextPeriodDB} of iterator) { - const periodDuration = nextPeriodDB ? - nextPeriodDB.startTime - periodDB.startTime : - manifestDB.duration - periodDB.startTime; - periods.push(this.fromPeriodDB(periodDB, periodDuration, timeline)); - } - - const drmInfos = manifestDB.drmInfo ? [manifestDB.drmInfo] : []; - if (manifestDB.drmInfo) { - for (const period of periods) { - for (const variant of period.variants) { - variant.drmInfos = drmInfos; - } - } - } - - return { - presentationTimeline: timeline, - minBufferTime: 2, - offlineSessionIds: manifestDB.sessionIds, - periods: periods, - }; - } - - /** - * Create a period object from a database period. - * - * @param {shaka.extern.PeriodDB} period - * @param {number} periodDuration - * @param {shaka.media.PresentationTimeline} timeline - * @return {shaka.extern.Period} - */ - fromPeriodDB(period, periodDuration, timeline) { /** @type {!Array.} */ const audioStreams = - period.streams.filter((streamDB) => this.isAudio_(streamDB)); + manifestDB.streams.filter((streamDB) => this.isAudio_(streamDB)); + /** @type {!Array.} */ const videoStreams = - period.streams.filter((streamDB) => this.isVideo_(streamDB)); - - const periodStart = period.startTime; + manifestDB.streams.filter((streamDB) => this.isVideo_(streamDB)); /** @type {!Map.} */ - const variants = this.createVariants( - audioStreams, videoStreams, timeline, periodStart, periodDuration); + const variants = this.createVariants(audioStreams, videoStreams, timeline); /** @type {!Array.} */ - const textStreams = period.streams - .filter((streamDB) => this.isText_(streamDB)) - .map((streamDB) => this.fromStreamDB_( - streamDB, timeline, periodStart, periodDuration)); + const textStreams = + manifestDB.streams.filter((streamDB) => this.isText_(streamDB)) + .map((streamDB) => this.fromStreamDB_(streamDB, timeline)); + + const drmInfos = manifestDB.drmInfo ? [manifestDB.drmInfo] : []; + if (manifestDB.drmInfo) { + for (const variant of variants.values()) { + variant.drmInfos = drmInfos; + } + } return { - startTime: period.startTime, + presentationTimeline: timeline, + minBufferTime: 2, + offlineSessionIds: manifestDB.sessionIds, variants: Array.from(variants.values()), textStreams: textStreams, }; @@ -113,11 +84,9 @@ shaka.offline.ManifestConverter = class { * @param {!Array.} audios * @param {!Array.} videos * @param {shaka.media.PresentationTimeline} timeline - * @param {number} periodStart - * @param {number} periodDuration * @return {!Map.} */ - createVariants(audios, videos, timeline, periodStart, periodDuration) { + createVariants(audios, videos, timeline) { // Get all the variant ids from all audio and video streams. /** @type {!Set.} */ const variantIds = new Set(); @@ -141,8 +110,7 @@ shaka.offline.ManifestConverter = class { // Assign each audio stream to its variants. for (const audio of audios) { /** @type {shaka.extern.Stream} */ - const stream = this.fromStreamDB_( - audio, timeline, periodStart, periodDuration); + const stream = this.fromStreamDB_(audio, timeline); for (const variantId of audio.variantIds) { const variant = variantMap.get(variantId); @@ -159,8 +127,7 @@ shaka.offline.ManifestConverter = class { // Assign each video stream to its variants. for (const video of videos) { /** @type {shaka.extern.Stream} */ - const stream = this.fromStreamDB_( - video, timeline, periodStart, periodDuration); + const stream = this.fromStreamDB_(video, timeline); for (const variantId of video.variantIds) { const variant = variantMap.get(variantId); @@ -179,21 +146,13 @@ shaka.offline.ManifestConverter = class { /** * @param {shaka.extern.StreamDB} streamDB * @param {shaka.media.PresentationTimeline} timeline - * @param {number} periodStart - * @param {number} periodDuration * @return {shaka.extern.Stream} * @private */ - fromStreamDB_(streamDB, timeline, periodStart, periodDuration) { - const initSegmentReference = streamDB.initSegmentKey != null ? - this.fromInitSegmentDB_(streamDB.initSegmentKey) : null; - const presentationTimeOffset = streamDB.presentationTimeOffset; - + fromStreamDB_(streamDB, timeline) { /** @type {!Array.} */ const segments = streamDB.segments.map( - (segment, index) => this.fromSegmentDB_( - index, segment, initSegmentReference, presentationTimeOffset, - periodStart, periodDuration)); + (segment, index) => this.fromSegmentDB_(index, segment)); timeline.notifySegments(segments); @@ -210,22 +169,21 @@ shaka.offline.ManifestConverter = class { codecs: streamDB.codecs, width: streamDB.width || undefined, height: streamDB.height || undefined, - frameRate: streamDB.frameRate || undefined, - pixelAspectRatio: streamDB.pixelAspectRatio || undefined, + frameRate: streamDB.frameRate, + pixelAspectRatio: streamDB.pixelAspectRatio, kind: streamDB.kind, encrypted: streamDB.encrypted, - keyId: streamDB.keyId, + keyIds: streamDB.keyIds, language: streamDB.language, - label: streamDB.label || null, + label: streamDB.label, type: streamDB.contentType, primary: streamDB.primary, trickModeVideo: null, - // TODO(modmaker): Store offline? emsgSchemeIdUris: null, - roles: [], - channelsCount: null, - audioSamplingRate: null, - closedCaptions: null, + roles: streamDB.roles, + channelsCount: streamDB.channelsCount, + audioSamplingRate: streamDB.audioSamplingRate, + closedCaptions: streamDB.closedCaptions, }; return stream; @@ -234,36 +192,27 @@ shaka.offline.ManifestConverter = class { /** * @param {number} index * @param {shaka.extern.SegmentDB} segmentDB - * @param {shaka.media.InitSegmentReference} initSegmentReference - * @param {number} presentationTimeOffset - * @param {number} periodStart - * @param {number} periodDuration * @return {!shaka.media.SegmentReference} * @private */ - fromSegmentDB_( - index, segmentDB, initSegmentReference, presentationTimeOffset, - periodStart, periodDuration) { + fromSegmentDB_(index, segmentDB) { /** @type {!shaka.offline.OfflineUri} */ const uri = shaka.offline.OfflineUri.segment( this.mechanism_, this.cell_, segmentDB.dataKey); - // The new timestampOffset field is the inverse of the old - // presentationTimeOffset field, and accounts for the period start. - const timestampOffset = periodStart - presentationTimeOffset; + const initSegmentReference = segmentDB.initSegmentKey != null ? + this.fromInitSegmentDB_(segmentDB.initSegmentKey) : null; return new shaka.media.SegmentReference( - // The DB format still stores segment times relative to the period. The - // manifest format uses presentation-relative times. - periodStart + segmentDB.startTime, - periodStart + segmentDB.endTime, + segmentDB.startTime, + segmentDB.endTime, () => [uri.toString()], /* startByte= */ 0, /* endByte= */ null, initSegmentReference, - timestampOffset, - /* appendWindowStart= */ periodStart, - /* appendWindowEnd= */ periodStart + periodDuration); + segmentDB.timestampOffset, + segmentDB.appendWindowStart, + segmentDB.appendWindowEnd); } /** diff --git a/lib/offline/storage.js b/lib/offline/storage.js index ff49fad67d..f686130792 100644 --- a/lib/offline/storage.js +++ b/lib/offline/storage.js @@ -25,7 +25,6 @@ goog.require('shaka.util.IDestroyable'); goog.require('shaka.util.Iterables'); goog.require('shaka.util.ManifestFilter'); goog.require('shaka.util.Networking'); -goog.require('shaka.util.Periods'); goog.require('shaka.util.Platform'); goog.require('shaka.util.PlayerConfiguration'); goog.require('shaka.util.StreamUtils'); @@ -108,6 +107,26 @@ shaka.offline.Storage = class { */ this.openOperations_ = []; + /** + * A cache mapping init segment references to Promises to their DB key. + * + * @private {!Map.>} + */ + this.initSegmentDbKeyCache_ = new Map(); + + // A null init segment reference always maps to a null DB key. + this.initSegmentDbKeyCache_.set( + null, /** @type {!Promise.} */(Promise.resolve(null))); + + /** + * A cache mapping equivalent segment references to Promises to their DB + * key. The key in this map is a string of the form + * "--". + * + * @private {!Map.>} + */ + this.segmentDbKeyCache_ = new Map(); + /** * Storage should only destroy the networking engine if it was initialized * without a player instance. Store this as a flag here to avoid including @@ -414,6 +433,8 @@ shaka.offline.Storage = class { * @private */ async filterManifest_(manifest, drmEngine) { + const StreamUtils = shaka.util.StreamUtils; + // Filter the manifest based on the restrictions given in the player // configuration. const maxHwRes = {width: Infinity, height: Infinity}; @@ -428,56 +449,50 @@ shaka.offline.Storage = class { // playing later. shaka.util.ManifestFilter.filterByDrmSupport(manifest, drmEngine); - // Filter the manifest so that it will only use codecs that are available in - // all periods. - shaka.util.ManifestFilter.filterByCommonCodecs(manifest); + // Gather all tracks. + const allTracks = []; // Choose the codec that has the lowest average bandwidth. const preferredAudioChannelCount = this.config_.preferredAudioChannelCount; shaka.util.StreamUtils.chooseCodecsAndFilterManifest( manifest, preferredAudioChannelCount); - // Filter each variant based on what the app says they want to store. The - // app will only be given variants that are compatible with all previous - // post-filtered periods. - await shaka.util.ManifestFilter.rollingFilter(manifest, async (period) => { - const StreamUtils = shaka.util.StreamUtils; - const allTracks = []; - - for (const variant of period.variants) { - goog.asserts.assert( - StreamUtils.isPlayable(variant), - 'We should have already filtered by "is playable"'); + for (const variant of manifest.variants) { + goog.asserts.assert( + StreamUtils.isPlayable(variant), + 'We should have already filtered by "is playable"'); - allTracks.push(StreamUtils.variantToTrack(variant)); - } + allTracks.push(StreamUtils.variantToTrack(variant)); + } - for (const text of period.textStreams) { - allTracks.push(StreamUtils.textStreamToTrack(text)); - } + for (const text of manifest.textStreams) { + allTracks.push(StreamUtils.textStreamToTrack(text)); + } - const chosenTracks = - await this.config_.offline.trackSelectionCallback(allTracks); + // Let the application choose which tracks to store. + const chosenTracks = + await this.config_.offline.trackSelectionCallback(allTracks); - /** @type {!Set.} */ - const variantIds = new Set(); - /** @type {!Set.} */ - const textIds = new Set(); + /** @type {!Set.} */ + const variantIds = new Set(); + /** @type {!Set.} */ + const textIds = new Set(); - for (const track of chosenTracks) { - if (track.type == 'variant') { - variantIds.add(track.id); - } - if (track.type == 'text') { - textIds.add(track.id); - } + // Collect the IDs of the chosen tracks. + for (const track of chosenTracks) { + if (track.type == 'variant') { + variantIds.add(track.id); + } + if (track.type == 'text') { + textIds.add(track.id); } + } - period.variants = - period.variants.filter((variant) => variantIds.has(variant.id)); - period.textStreams = - period.textStreams.filter((stream) => textIds.has(stream.id)); - }); + // Filter the manifest to keep only what the app chose. + manifest.variants = + manifest.variants.filter((variant) => variantIds.has(variant.id)); + manifest.textStreams = + manifest.textStreams.filter((stream) => textIds.has(stream.id)); // Check the post-filtered manifest for characteristics that may indicate // issues with how the app selected tracks. @@ -503,16 +518,12 @@ shaka.offline.Storage = class { const pendingContent = shaka.offline.StoredContentUtils.fromManifest( uri, manifest, /* size= */ 0, metadata); - const isEncrypted = manifest.periods.some((period) => { - return period.variants.some((variant) => { - return variant.drmInfos && variant.drmInfos.length; - }); + const isEncrypted = manifest.variants.some((variant) => { + return variant.drmInfos && variant.drmInfos.length; }); - const includesInitData = manifest.periods.some((period) => { - return period.variants.some((variant) => { - return variant.drmInfos.some((drmInfos) => { - return drmInfos.initData && drmInfos.initData.length; - }); + const includesInitData = manifest.variants.some((variant) => { + return variant.drmInfos.some((drmInfos) => { + return drmInfos.initData && drmInfos.initData.length; }); }); const needsInitData = isEncrypted && !includesInitData; @@ -627,19 +638,17 @@ shaka.offline.Storage = class { const MimeUtils = shaka.util.MimeUtils; const ret = []; - for (const period of manifestDb.periods) { - for (const stream of period.streams) { - if (isVideo && stream.contentType == 'video') { - ret.push({ - contentType: MimeUtils.getFullType(stream.mimeType, stream.codecs), - robustness: manifestDb.drmInfo.videoRobustness, - }); - } else if (!isVideo && stream.contentType == 'audio') { - ret.push({ - contentType: MimeUtils.getFullType(stream.mimeType, stream.codecs), - robustness: manifestDb.drmInfo.audioRobustness, - }); - } + for (const stream of manifestDb.streams) { + if (isVideo && stream.contentType == 'video') { + ret.push({ + contentType: MimeUtils.getFullType(stream.mimeType, stream.codecs), + robustness: manifestDb.drmInfo.videoRobustness, + }); + } else if (!isVideo && stream.contentType == 'audio') { + ret.push({ + contentType: MimeUtils.getFullType(stream.mimeType, stream.codecs), + robustness: manifestDb.drmInfo.audioRobustness, + }); } } return ret; @@ -823,8 +832,7 @@ shaka.offline.Storage = class { // Don't bother filtering now. We will do that later when we have all the // information we need to filter. - filterAllPeriods: () => {}, - filterNewPeriod: () => {}, + filter: () => {}, onTimelineRegionAdded: () => {}, onEvent: () => {}, @@ -895,12 +903,10 @@ shaka.offline.Storage = class { onEvent: () => {}, }); - const variants = shaka.util.Periods.getAllVariantsFrom(manifest.periods); - const config = this.config_; drmEngine.configure(config.drm); await drmEngine.initForStorage( - variants, config.offline.usePersistentLicense); + manifest.variants, config.offline.usePersistentLicense); await drmEngine.setServerCertificate(); await drmEngine.createOrLoad(); @@ -910,7 +916,7 @@ shaka.offline.Storage = class { /** * Creates an offline 'manifest' for the real manifest. This does not store * the segments yet, only adds them to the download manager through - * createPeriod_. + * createStreams_. * * @param {!shaka.offline.DownloadManager} downloader * @param {shaka.extern.StorageCell} storage @@ -925,10 +931,8 @@ shaka.offline.Storage = class { downloader, storage, drmEngine, manifest, originalManifestUri, metadata) { const estimator = new shaka.offline.StreamBandwidthEstimator(); - const periods = manifest.periods.map((period) => { - return this.createPeriod_( - downloader, storage, estimator, drmEngine, manifest, period); - }); + const streams = this.createStreams_( + downloader, storage, estimator, drmEngine, manifest); const usePersistentLicense = this.config_.offline.usePersistentLicense; const drmInfo = drmEngine.getDrmInfo(); @@ -943,7 +947,7 @@ shaka.offline.Storage = class { duration: manifest.presentationTimeline.getDuration(), size: 0, expiration: drmEngine.getExpiration(), - periods: periods, + streams: streams, sessionIds: usePersistentLicense ? drmEngine.getSessionIds() : [], drmInfo: drmInfo, appMetadata: metadata, @@ -951,7 +955,7 @@ shaka.offline.Storage = class { } /** - * Converts a manifest Period to a database Period. This will use the current + * Converts manifest Streams to database Streams. This will use the current * configuration to get the tracks to use, then it will search each segment * index and add all the segments to the download manager through * createStream_. @@ -961,33 +965,33 @@ shaka.offline.Storage = class { * @param {shaka.offline.StreamBandwidthEstimator} estimator * @param {!shaka.media.DrmEngine} drmEngine * @param {shaka.extern.Manifest} manifest - * @param {shaka.extern.Period} period - * @return {shaka.extern.PeriodDB} + * @return {!Array.} * @private */ - createPeriod_(downloader, storage, estimator, drmEngine, manifest, period) { + createStreams_(downloader, storage, estimator, drmEngine, manifest) { // Pass all variants and text streams to the estimator so that we can // get the best estimate for each stream later. - for (const variant of period.variants) { + for (const variant of manifest.variants) { estimator.addVariant(variant); } - for (const text of period.textStreams) { + for (const text of manifest.textStreams) { estimator.addText(text); } // Find the streams we want to download and create a stream db instance // for each of them. - const streamSet = shaka.offline.Storage.getAllStreamsFromPeriod_(period); + const streamSet = + shaka.offline.Storage.getAllStreamsFromManifest_(manifest); const streamDBs = new Map(); for (const stream of streamSet) { const streamDB = this.createStream_( - downloader, storage, estimator, manifest, period, stream); + downloader, storage, estimator, manifest, stream); streamDBs.set(stream.id, streamDB); } // Connect streams and variants together. - for (const variant of period.variants) { + for (const variant of manifest.variants) { if (variant.audio) { streamDBs.get(variant.audio.id).variantIds.push(variant.id); } @@ -996,10 +1000,7 @@ shaka.offline.Storage = class { } } - return { - startTime: period.startTime, - streams: Array.from(streamDBs.values()), - }; + return Array.from(streamDBs.values()); } /** @@ -1010,18 +1011,16 @@ shaka.offline.Storage = class { * @param {shaka.extern.StorageCell} storage * @param {shaka.offline.StreamBandwidthEstimator} estimator * @param {shaka.extern.Manifest} manifest - * @param {shaka.extern.Period} period * @param {shaka.extern.Stream} stream * @return {shaka.extern.StreamDB} * @private */ - createStream_(downloader, storage, estimator, manifest, period, stream) { + createStream_(downloader, storage, estimator, manifest, stream) { /** @type {shaka.extern.StreamDB} */ const streamDb = { id: stream.id, originalId: stream.originalId, primary: stream.primary, - presentationTimeOffset: 0, contentType: stream.type, mimeType: stream.mimeType, codecs: stream.codecs, @@ -1032,80 +1031,143 @@ shaka.offline.Storage = class { label: stream.label, width: stream.width || null, height: stream.height || null, - initSegmentKey: null, encrypted: stream.encrypted, - keyId: stream.keyId, + keyIds: stream.keyIds, segments: [], variantIds: [], + roles: stream.roles, + channelsCount: stream.channelsCount, + audioSamplingRate: stream.audioSamplingRate, + closedCaptions: stream.closedCaptions, }; - /** @type {number} */ - const startTime = - manifest.presentationTimeline.getSegmentAvailabilityStart(); - // Download each stream in parallel. const downloadGroup = stream.id; - // Extract the init segment reference and PTO from the first segment. - // Temporary, until the DB types are updated to match the manifest types. - const firstPosition = stream.segmentIndex.find(startTime); - const firstMediaSegment = firstPosition != null ? - stream.segmentIndex.get(firstPosition) : null; - const initSegment = firstMediaSegment ? - firstMediaSegment.initSegmentReference : null; - // The old presentationTimeOffset field was the inverse of the new - // timestampOffset field, and didn't account for period start. - streamDb.presentationTimeOffset = firstMediaSegment ? - period.startTime - firstMediaSegment.timestampOffset : 0; - - if (initSegment) { - const request = shaka.util.Networking.createSegmentRequest( - initSegment.getUris(), - initSegment.startByte, - initSegment.endByte, - this.config_.streaming.retryParameters); - - downloader.queue( - downloadGroup, - request, - estimator.getInitSegmentEstimate(stream.id), - /* isInitSegment= */ true, - async (data) => { - const ids = await storage.addSegments([{data: data}]); - this.segmentsFromStore_.push(ids[0]); - streamDb.initSegmentKey = ids[0]; - }); - } + const startTime = + manifest.presentationTimeline.getSegmentAvailabilityStart(); shaka.offline.Storage.forEachSegment_(stream, startTime, (segment) => { - const request = shaka.util.Networking.createSegmentRequest( - segment.getUris(), - segment.startByte, - segment.endByte, - this.config_.streaming.retryParameters); - - downloader.queue( - downloadGroup, - request, - estimator.getSegmentEstimate(stream.id, segment), - /* isInitSegment= */ false, - async (data) => { - const ids = await storage.addSegments([{data: data}]); - this.segmentsFromStore_.push(ids[0]); - - streamDb.segments.push({ - // The DB format still stores segment times relative to the - // period. The manifest format uses presentation-relative times. - startTime: segment.startTime - period.startTime, - endTime: segment.endTime - period.startTime, - dataKey: ids[0], - }); - }); + const initSegmentKeyPromise = this.getInitSegmentDbKey_( + downloader, downloadGroup, stream.id, storage, estimator, + segment.initSegmentReference); + + const segmentKeyPromise = this.getSegmentDbKey_( + downloader, downloadGroup, stream.id, storage, estimator, segment); + + downloader.queueWork(downloadGroup, async () => { + const initSegmentKey = await initSegmentKeyPromise; + const dataKey = await segmentKeyPromise; + + streamDb.segments.push({ + initSegmentKey, + startTime: segment.startTime, + endTime: segment.endTime, + appendWindowStart: segment.appendWindowStart, + appendWindowEnd: segment.appendWindowEnd, + timestampOffset: segment.timestampOffset, + dataKey, + }); + }); }); return streamDb; } + /** + * Get a Promise to the DB key for a given init segment reference. + * + * The return values will be cached so that multiple calls with the same init + * segment reference will only trigger one request. + * + * @param {!shaka.offline.DownloadManager} downloader + * @param {number} downloadGroup + * @param {number} streamId + * @param {shaka.extern.StorageCell} storage + * @param {shaka.offline.StreamBandwidthEstimator} estimator + * @param {shaka.media.InitSegmentReference} initSegmentReference + * @return {!Promise.} + * @private + */ + getInitSegmentDbKey_( + downloader, downloadGroup, streamId, storage, estimator, + initSegmentReference) { + if (this.initSegmentDbKeyCache_.has(initSegmentReference)) { + return this.initSegmentDbKeyCache_.get(initSegmentReference); + } + + const request = shaka.util.Networking.createSegmentRequest( + initSegmentReference.getUris(), + initSegmentReference.startByte, + initSegmentReference.endByte, + this.config_.streaming.retryParameters); + + const promise = downloader.queue( + downloadGroup, + request, + estimator.getInitSegmentEstimate(streamId), + /* isInitSegment= */ true, + async (data) => { + /** @type {!Array.} */ + const ids = await storage.addSegments([{data: data}]); + this.segmentsFromStore_.push(ids[0]); + return ids[0]; + }); + + this.initSegmentDbKeyCache_.set(initSegmentReference, promise); + return promise; + } + + /** + * Get a Promise to the DB key for a given segment reference. + * + * The return values will be cached so that multiple calls with the same + * segment reference will only trigger one request. + * + * @param {!shaka.offline.DownloadManager} downloader + * @param {number} downloadGroup + * @param {number} streamId + * @param {shaka.extern.StorageCell} storage + * @param {shaka.offline.StreamBandwidthEstimator} estimator + * @param {shaka.media.SegmentReference} segmentReference + * @return {!Promise.} + * @private + */ + getSegmentDbKey_( + downloader, downloadGroup, streamId, storage, estimator, + segmentReference) { + const mapKey = [ + segmentReference.getUris()[0], + segmentReference.startByte, + segmentReference.endByte, + ].join('-'); + + if (this.segmentDbKeyCache_.has(mapKey)) { + return this.segmentDbKeyCache_.get(mapKey); + } + + const request = shaka.util.Networking.createSegmentRequest( + segmentReference.getUris(), + segmentReference.startByte, + segmentReference.endByte, + this.config_.streaming.retryParameters); + + const promise = downloader.queue( + downloadGroup, + request, + estimator.getSegmentEstimate(streamId, segmentReference), + /* isInitSegment= */ false, + async (data) => { + /** @type {!Array.} */ + const ids = await storage.addSegments([{data: data}]); + this.segmentsFromStore_.push(ids[0]); + return ids[0]; + }); + + this.segmentDbKeyCache_.set(mapKey, promise); + return promise; + } + /** * @param {shaka.extern.Stream} stream * @param {number} startTime @@ -1185,15 +1247,13 @@ shaka.offline.Storage = class { const ids = []; // Get every segment for every stream in the manifest. - for (const period of manifest.periods) { - for (const stream of period.streams) { - if (stream.initSegmentKey != null) { - ids.push(stream.initSegmentKey); + for (const stream of manifest.streams) { + for (const segment of stream.segments) { + if (segment.initSegmentKey != null) { + ids.push(segment.initSegmentKey); } - for (const segment of stream.segments) { - ids.push(segment.dataKey); - } + ids.push(segment.dataKey); } } @@ -1273,40 +1333,11 @@ shaka.offline.Storage = class { /** @type {!Set.} */ const set = new Set(); - for (const period of manifest.periods) { - for (const text of period.textStreams) { - set.add(text); - } - - for (const variant of period.variants) { - if (variant.audio) { - set.add(variant.audio); - } - if (variant.video) { - set.add(variant.video); - } - } - } - - return set; - } - - /** - * Get the set of all streams in |period|. - * - * @param {shaka.extern.Period} period - * @return {!Set.} - * @private - */ - static getAllStreamsFromPeriod_(period) { - /** @type {!Set.} */ - const set = new Set(); - - for (const text of period.textStreams) { + for (const text of manifest.textStreams) { set.add(text); } - for (const variant of period.variants) { + for (const variant of manifest.variants) { if (variant.audio) { set.add(variant.audio); } @@ -1325,29 +1356,9 @@ shaka.offline.Storage = class { * @private */ static validateManifest_(manifest) { - // Make sure that the period has not been reduced to nothing. - if (manifest.periods.length == 0) { - throw new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.NO_PERIODS); - } - - for (const period of manifest.periods) { - shaka.offline.Storage.validatePeriod_(period); - } - } - - /** - * Go over a period and issue warnings for any suspicious properties. - * - * @param {shaka.extern.Period} period - * @private - */ - static validatePeriod_(period) { - const videos = new Set(period.variants.map((v) => v.video)); - const audios = new Set(period.variants.map((v) => v.audio)); - const texts = period.textStreams; + const videos = new Set(manifest.variants.map((v) => v.video)); + const audios = new Set(manifest.variants.map((v) => v.audio)); + const texts = manifest.textStreams; if (videos.size > 1) { shaka.log.warning('Multiple video tracks selected to be stored'); diff --git a/lib/offline/stored_content_utils.js b/lib/offline/stored_content_utils.js index 456ac58d60..7b3d708e93 100644 --- a/lib/offline/stored_content_utils.js +++ b/lib/offline/stored_content_utils.js @@ -6,7 +6,6 @@ goog.provide('shaka.offline.StoredContentUtils'); goog.require('goog.asserts'); -goog.require('shaka.media.PresentationTimeline'); goog.require('shaka.offline.ManifestConverter'); goog.require('shaka.offline.OfflineUri'); goog.require('shaka.util.StreamUtils'); @@ -26,22 +25,14 @@ shaka.offline.StoredContentUtils = class { */ static fromManifest(originalUri, manifest, size, metadata) { goog.asserts.assert( - manifest.periods.length, - 'Cannot create stored content from manifest with no periods.'); - - /** @type {number} */ - const expiration = manifest.expiration == undefined ? - Infinity : - manifest.expiration; + manifest.variants.length, + 'Cannot create stored content from manifest with no variants.'); /** @type {number} */ const duration = manifest.presentationTimeline.getDuration(); - /** @type {shaka.extern.Period} */ - const firstPeriod = manifest.periods[0]; - /** @type {!Array.} */ - const tracks = shaka.offline.StoredContentUtils.getTracks_(firstPeriod); + const tracks = shaka.offline.StoredContentUtils.getTracks_(manifest); /** @type {shaka.extern.StoredContent} */ const content = { @@ -49,7 +40,10 @@ shaka.offline.StoredContentUtils = class { originalManifestUri: originalUri, duration: duration, size: size, - expiration: expiration, + // This expiration value is temporary and will be used in progress reports + // during the storage process. The real value would have to come from + // DrmEngine. + expiration: Infinity, tracks: tracks, appMetadata: metadata, }; @@ -57,7 +51,6 @@ shaka.offline.StoredContentUtils = class { return content; } - /** * @param {!shaka.offline.OfflineUri} offlineUri * @param {shaka.extern.ManifestDB} manifestDB @@ -65,31 +58,24 @@ shaka.offline.StoredContentUtils = class { */ static fromManifestDB(offlineUri, manifestDB) { goog.asserts.assert( - manifestDB.periods.length, - 'Cannot create stored content from manifestDB with no periods.'); + manifestDB.streams.length, + 'Cannot create stored content from manifestDB with no streams.'); const converter = new shaka.offline.ManifestConverter( offlineUri.mechanism(), offlineUri.cell()); - /** @type {shaka.extern.PeriodDB} */ - const firstPeriodDB = manifestDB.periods[0]; - /** @type {!shaka.media.PresentationTimeline} */ - const timeline = new shaka.media.PresentationTimeline(null, 0); - - // Getting the period duration would be a bit of a pain, and for the - // purposes of getting the metadata below, we don't need a real period - // duration. - const fakePeriodDuration = 1; - - /** @type {shaka.extern.Period} */ - const firstPeriod = converter.fromPeriodDB( - firstPeriodDB, fakePeriodDuration, timeline); + /** @type {shaka.extern.Manifest} */ + const manifest = converter.fromManifestDB(manifestDB); /** @type {!Object} */ const metadata = manifestDB.appMetadata || {}; /** @type {!Array.} */ - const tracks = shaka.offline.StoredContentUtils.getTracks_(firstPeriod); + const tracks = shaka.offline.StoredContentUtils.getTracks_(manifest); + + goog.asserts.assert( + manifestDB.expiration != null, + 'Manifest expiration must be set by now!'); /** @type {shaka.extern.StoredContent} */ const content = { @@ -105,25 +91,24 @@ shaka.offline.StoredContentUtils = class { return content; } - /** * Gets track representations of all playable variants and all text streams. * - * @param {shaka.extern.Period} period + * @param {shaka.extern.Manifest} manifest * @return {!Array.} * @private */ - static getTracks_(period) { + static getTracks_(manifest) { const StreamUtils = shaka.util.StreamUtils; const tracks = []; - const variants = StreamUtils.getPlayableVariants(period.variants); + const variants = StreamUtils.getPlayableVariants(manifest.variants); for (const variant of variants) { tracks.push(StreamUtils.variantToTrack(variant)); } - const textStreams = period.textStreams; + const textStreams = manifest.textStreams; for (const stream of textStreams) { tracks.push(StreamUtils.textStreamToTrack(stream)); } diff --git a/lib/player.js b/lib/player.js index a04377f485..f885c0f93e 100644 --- a/lib/player.js +++ b/lib/player.js @@ -8,7 +8,6 @@ goog.provide('shaka.Player'); goog.require('goog.asserts'); goog.require('shaka.Deprecate'); goog.require('shaka.log'); -goog.require('shaka.media.ActiveStreamMap'); goog.require('shaka.media.AdaptationSetCriteria'); goog.require('shaka.media.BufferingObserver'); goog.require('shaka.media.DrmEngine'); @@ -16,7 +15,6 @@ goog.require('shaka.media.ManifestParser'); goog.require('shaka.media.MediaSourceEngine'); goog.require('shaka.media.MuxJSClosedCaptionParser'); goog.require('shaka.media.NoopCaptionParser'); -goog.require('shaka.media.PeriodObserver'); goog.require('shaka.media.PlayRateController'); goog.require('shaka.media.Playhead'); goog.require('shaka.media.PlayheadObserverManager'); @@ -28,7 +26,6 @@ goog.require('shaka.media.StreamingEngine'); goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.routing.Walker'); goog.require('shaka.text.SimpleTextDisplayer'); -goog.require('shaka.util.ArrayUtils'); goog.require('shaka.util.Error'); goog.require('shaka.util.EventManager'); goog.require('shaka.util.FakeEvent'); @@ -38,7 +35,6 @@ goog.require('shaka.util.IDestroyable'); goog.require('shaka.util.LanguageUtils'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.MimeUtils'); -goog.require('shaka.util.Periods'); goog.require('shaka.util.Platform'); goog.require('shaka.util.PlayerConfiguration'); goog.require('shaka.util.Stats'); @@ -188,7 +184,7 @@ goog.require('shaka.util.Timer'); /** * @event shaka.Player.TracksChangedEvent * @description Fired when the list of tracks changes. For example, this will - * happen when changing periods or when track restrictions change. + * happen when new tracks are added/removed or when track restrictions change. * @property {string} type * 'trackschanged' * @exportDoc @@ -413,33 +409,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget { */ this.nextExternalStreamId_ = 1e9; - /** @private {!Set.} */ - this.loadingTextStreams_ = new Set(); - - /** @private {boolean} */ - this.switchingPeriods_ = true; - - /** @private {?shaka.extern.Variant} */ - this.deferredVariant_ = null; - - /** @private {boolean} */ - this.deferredVariantClearBuffer_ = false; - - /** @private {number} */ - this.deferredVariantClearBufferSafeMargin_ = 0; - - /** @private {?shaka.extern.Stream} */ - this.deferredTextStream_ = null; - - /** - * A mapping of which streams are/were active in each period. Used when the - * current period (the one containing playhead) differs from the active - * period (the one being streamed in by streaming engine). - * - * @private {!shaka.media.ActiveStreamMap} - */ - this.activeStreams_ = new shaka.media.ActiveStreamMap(); - /** @private {?shaka.extern.PlayerConfiguration} */ this.config_ = this.defaultConfig_(); @@ -1341,32 +1310,27 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.drmEngine_ = null; } - this.activeStreams_.clear(); this.assetUri_ = null; this.bufferObserver_ = null; - this.loadingTextStreams_.clear(); if (this.manifest_) { - for (const period of this.manifest_.periods) { - for (const variant of period.variants) { - for (const stream of [variant.audio, variant.video]) { - if (stream && stream.segmentIndex) { - stream.segmentIndex.release(); - } - } - } - for (const stream of period.textStreams) { - if (stream.segmentIndex) { + for (const variant of this.manifest_.variants) { + for (const stream of [variant.audio, variant.video]) { + if (stream && stream.segmentIndex) { stream.segmentIndex.release(); } } } + for (const stream of this.manifest_.textStreams) { + if (stream.segmentIndex) { + stream.segmentIndex.release(); + } + } } this.manifest_ = null; this.stats_ = new shaka.util.Stats(); // Replace with a clean stats object. this.lastTextFactory_ = null; - this.switchingPeriods_ = true; // Make sure that the app knows of the new buffering state. this.updateBufferState_(); @@ -1547,8 +1511,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { const playerInterface = { networkingEngine: networkingEngine, - filterNewPeriod: (period) => this.filterNewPeriod_(period), - filterAllPeriods: (periods) => this.filterAllPeriods_(periods), + filter: (manifest) => this.filterManifest_(manifest), // Called when the parser finds a timeline region. This can be called // before we start playback or during playback (live/in-progress @@ -1561,35 +1524,33 @@ shaka.Player = class extends shaka.util.FakeEventTarget { const startTime = Date.now() / 1000; - return new shaka.util.AbortableOperation( - /* promise= */ (async () => { - this.manifest_ = await this.parser_.start(assetUri, playerInterface); - - // This event is fired after the manifest is parsed, but before any - // filtering takes place. - const event = this.makeEvent_(shaka.Player.EventName.ManifestParsed); - this.dispatchEvent(event); - - // We require all manifests to have already one period. - if (this.manifest_.periods.length == 0) { - throw new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.NO_PERIODS); - } + return new shaka.util.AbortableOperation(/* promise= */ (async () => { + this.manifest_ = await this.parser_.start(assetUri, playerInterface); - // Make sure that all periods are either: audio-only, video-only, or - // audio-video. - shaka.Player.filterForAVVariants_(this.manifest_.periods); + // This event is fired after the manifest is parsed, but before any + // filtering takes place. + const event = this.makeEvent_(shaka.Player.EventName.ManifestParsed); + this.dispatchEvent(event); - const now = Date.now() / 1000; - const delta = now - startTime; - this.stats_.setManifestTime(delta); - })(), - /* onAbort= */ () => { - shaka.log.info('Aborting parser step...'); - return this.parser_.stop(); - }); + // We require all manifests to have at least one variant. + if (this.manifest_.variants.length == 0) { + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.MANIFEST, + shaka.util.Error.Code.NO_VARIANTS); + } + + // Make sure that all variants are either: audio-only, video-only, or + // audio-video. + shaka.Player.filterForAVVariants_(this.manifest_); + + const now = Date.now() / 1000; + const delta = now - startTime; + this.stats_.setManifestTime(delta); + })(), /* onAbort= */ () => { + shaka.log.info('Aborting parser step...'); + return this.parser_.stop(); + }); } /** @@ -1651,12 +1612,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.drmEngine_.configure(this.config_.drm); await this.drmEngine_.initForPlayback( - shaka.util.Periods.getAllVariantsFrom(this.manifest_.periods), + this.manifest_.variants, this.manifest_.offlineSessionIds); // Now that we have drm information, filter the manifest (again) so that we // can ensure we only use variants with the selected key system. - this.filterAllPeriods_(this.manifest_.periods); + this.filterManifest_(this.manifest_); } /** @@ -1732,10 +1693,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.abrManager_.configure(this.config_.abr); } - // TODO: When a manifest update adds a new period, that period's closed + // TODO: When a manifest update adds a new variant, that variant's closed // captions should also be turned into text streams. This should be called - // for each new period as well. - this.createTextStreamsForClosedCaptions_(this.manifest_.periods); + // for each new variant as well. + this.createTextStreamsForClosedCaptions_(this.manifest_.variants); // Copy preferred languages from the config again, in case the config was // changed between construction and playback. @@ -1767,14 +1728,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.manifest_.minBufferTime, this.config_.streaming.rebufferingGoal); this.startBufferManagement_(rebufferThreshold); - this.streamingEngine_ = this.createStreamingEngine(); - this.streamingEngine_.configure(this.config_.streaming); - // If the content is multi-codec and the browser can play more than one of // them, choose codecs now before we initialize streaming. shaka.util.StreamUtils.chooseCodecsAndFilterManifest( this.manifest_, this.config_.preferredAudioChannelCount); + this.streamingEngine_ = this.createStreamingEngine(); + this.streamingEngine_.configure(this.config_.streaming); + // Set the load mode to "loaded with media source" as late as possible so // that public methods won't try to access internal components until // they're all initialized. We MUST switch to loaded before calling @@ -1786,44 +1747,68 @@ shaka.Player = class extends shaka.util.FakeEventTarget { // event to make changes before streaming starts. this.dispatchEvent(this.makeEvent_(shaka.Player.EventName.Streaming)); - // Start streaming content. This will start the flow of content down to - // media source, including picking the initial streams to play. - await this.streamingEngine_.start(); + // Pick the initial streams to play. + const initialVariant = this.chooseVariant_(); + goog.asserts.assert(initialVariant, 'Must choose an initial variant!'); + this.addVariantToSwitchHistory_( + initialVariant, /* fromAdaptation= */ true); + this.streamingEngine_.switchVariant( + initialVariant, /* clearBuffer= */ false, /* safeMargin= */ 0); + + // Decide if text should be shown automatically. + const initialTextStream = this.chooseTextStream_(); + if (initialTextStream) { + this.addTextStreamToSwitchHistory_( + initialTextStream, /* fromAdaptation= */ true); + } - // We MUST wait until after we create streaming engine to adjust the start - // time because we rely on the active audio and video streams, which are - // selected in |StreamingEngine.init|. + this.setInitialTextState_(initialVariant, initialTextStream); + // Don't initialize with a text stream unless we should be streaming text. + if (initialTextStream && this.shouldStreamText_()) { + this.streamingEngine_.switchTextStream(initialTextStream); + } + + // Now that we have initial streams, we may adjust the start time to align + // to a segment boundary. if (this.config_.streaming.startAtSegmentBoundary) { const startTime = this.playhead_.getTime(); - const adjustedTime = this.adjustStartTime_(startTime); + const adjustedTime = + await this.adjustStartTime_(initialVariant, startTime); this.playhead_.setStartTime(adjustedTime); } - // Re-filter the manifest after streams have been chosen. - for (const period of this.manifest_.periods) { - this.filterNewPeriod_(period); + // Start streaming content. This will start the flow of content down to + // media source. + await this.streamingEngine_.start(); + + if (this.config_.abr.enabled) { + this.abrManager_.enable(); + this.onAbrStatusChanged_(); } + + // Re-filter the manifest after streams have been chosen. + this.filterManifest_(this.manifest_); // Dispatch a 'trackschanged' event now that all initial filtering is done. this.onTracksChanged_(); // Since the first streams just became active, send an adaptation event. this.onAdaptation_(); // Now that we've filtered out variants that aren't compatible with the - // active one, update abr manager with filtered variants for the current - // period. - /** @type {shaka.extern.Period} */ - const currentPeriod = - this.getPresentationPeriod_() || this.manifest_.periods[0]; - const hasPrimary = currentPeriod.variants.some((v) => v.primary); - + // active one, update abr manager with filtered variants. + // NOTE: This may be unnecessary. We've already chosen one codec in + // chooseCodecsAndFilterManifest_ before we started streaming. But it + // doesn't hurt, and this will all change when we start using + // MediaCapabilities and codec switching. + // TODO(#1391): Re-evaluate with MediaCapabilities and codec switching. + this.updateAbrManagerVariants_(); + + const hasPrimary = this.manifest_.variants.some((v) => v.primary); if (!this.config_.preferredAudioLanguage && !hasPrimary) { - shaka.log.warning('No preferred audio language set. We will choose an ' + + shaka.log.warning('No preferred audio language set. We have chosen an ' + 'arbitrary language initially'); } - this.chooseVariant_(currentPeriod.variants); - // Wait for the 'loadeddata' event to measure load() latency. this.eventManager_.listenOnce(mediaElement, 'loadeddata', () => { const now = Date.now() / 1000; @@ -1903,7 +1888,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { mimeType: 'video/mp4', codecs: '', encrypted: true, - keyId: null, + keyIds: [], language: 'und', label: null, type: ContentType.VIDEO, @@ -2060,7 +2045,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } /** - * Take a series of periods and ensure that they only contain one type of + * Take a series of variants and ensure that they only contain one type of * variant. The different options are: * 1. Audio-Video * 2. Audio-Only @@ -2068,28 +2053,23 @@ shaka.Player = class extends shaka.util.FakeEventTarget { * * A manifest can only contain a single type because once we initialize media * source to expect specific streams, it must always have content for those - * streams. If we were to start period 1 with audio+video but period 2 only - * had audio, media source would block waiting for video content. + * streams. If we were to start with audio+video and switch to an audio-only + * variant, media source would block waiting for video content. * - * @param {!Array.} periods + * @param {shaka.extern.Manifest} manifest * @private */ - static filterForAVVariants_(periods) { + static filterForAVVariants_(manifest) { const isAVVariant = (variant) => { // Audio-video variants may include both streams separately or may be // single multiplexed streams with multiple codecs. return (variant.video && variant.audio) || (variant.video && variant.video.codecs.includes(',')); }; - const hasAVVariant = periods.some((period) => { - return period.variants.some(isAVVariant); - }); - if (hasAVVariant) { + if (manifest.variants.some(isAVVariant)) { shaka.log.debug('Found variant with audio and video content, ' + - 'so filtering out audio-only content in all periods.'); - for (const period of periods) { - period.variants = period.variants.filter(isAVVariant); - } + 'so filtering out audio-only content.'); + manifest.variants = manifest.variants.filter(isAVVariant); } } @@ -2156,11 +2136,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget { goog.asserts.assert(this.regionTimeline_, 'Must have region timeline'); goog.asserts.assert(this.video_, 'Must have video element'); - // Create the period observer. This will allow us to notify the app when we - // transition between periods. - const periodObserver = new shaka.media.PeriodObserver(this.manifest_); - periodObserver.setListeners((period) => this.onChangePeriod_()); - // Create the region observer. This will allow us to notify the app when we // move in and out of timeline regions. const regionObserver = new shaka.media.RegionObserver(this.regionTimeline_); @@ -2182,7 +2157,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget { // Now that we have all our observers, create a manager for them. const manager = new shaka.media.PlayheadObserverManager(this.video_); - manager.manage(periodObserver); manager.manage(regionObserver); return manager; @@ -2315,8 +2289,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget { getBandwidthEstimate: () => this.abrManager_.getBandwidthEstimate(), mediaSourceEngine: this.mediaSourceEngine_, netEngine: this.networkingEngine_, - onChooseStreams: (period) => this.onChooseStreams_(period), - onCanSwitch: () => this.canSwitch_(), onError: (error) => this.onError_(error), onEvent: (event) => this.dispatchEvent(event), onManifestUpdate: () => this.onManifestUpdate_(), @@ -2403,33 +2375,30 @@ shaka.Player = class extends shaka.util.FakeEventTarget { if (this.streamingEngine_) { this.streamingEngine_.configure(this.config_.streaming); - // Need to apply the restrictions to every period. + // Need to apply the restrictions. try { - // this.filterNewPeriod_() may throw. - for (const period of this.manifest_.periods) { - this.filterNewPeriod_(period); - } + // this.filterManifest_() may throw. + this.filterManifest_(this.manifest_); } catch (error) { this.onError_(error); } - // If the stream we are playing is restricted, we need to switch. - const activeAudio = this.streamingEngine_.getBufferingAudio(); - const activeVideo = this.streamingEngine_.getBufferingVideo(); - /** @type {shaka.extern.Period} */ - const period = this.getPresentationPeriod_(); - const activeVariant = shaka.util.StreamUtils.getVariantByStreams( - activeAudio, activeVideo, period.variants); - if (this.abrManager_ && activeVariant && - activeVariant.allowedByApplication && - activeVariant.allowedByKeySystem) { + if (this.abrManager_) { // Update AbrManager variants to match these new settings. - this.chooseVariant_(period.variants); - } else { - shaka.log.debug('Choosing new streams after changing configuration'); - this.chooseStreamsAndSwitch_(period); + this.updateAbrManagerVariants_(); + } + + // If the streams we are playing are restricted, we need to switch. + const activeVariant = this.streamingEngine_.getCurrentVariant(); + if (activeVariant) { + if (!activeVariant.allowedByApplication || + !activeVariant.allowedByKeySystem) { + shaka.log.debug('Choosing new variant after changing configuration'); + this.chooseVariantAndSwitch_(); + } } } + if (this.mediaSourceEngine_) { const textDisplayerFactory = this.config_.textDisplayFactory; if (this.lastTextFactory_ != textDisplayerFactory) { @@ -2448,7 +2417,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.abrManager_.configure(this.config_.abr); // Simply enable/disable ABR with each call, since multiple calls to these // methods have no effect. - if (this.config_.abr.enabled && !this.switchingPeriods_) { + if (this.config_.abr.enabled) { this.abrManager_.enable(); } else { this.abrManager_.disable(); @@ -2617,12 +2586,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { */ isAudioOnly() { if (this.manifest_) { - const periods = this.manifest_.periods; - if (!periods.length) { - return false; - } - - const variants = this.manifest_.periods[0].variants; + const variants = this.manifest_.variants; if (!variants.length) { return false; } @@ -2816,9 +2780,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } /** - * Return a list of variant tracks that can be switched to in the current - * period. If there are multiple periods, you must seek to the period in order - * to get variants from that period. + * Return a list of variant tracks that can be switched to. * *

* If the player has not loaded content, this will return an empty list. @@ -2827,13 +2789,18 @@ shaka.Player = class extends shaka.util.FakeEventTarget { * @export */ getVariantTracks() { - if (this.manifest_ && this.playhead_) { - const currentVariant = this.getPresentationVariant_(); + if (this.manifest_) { + const currentVariant = this.streamingEngine_ ? + this.streamingEngine_.getCurrentVariant() : null; const tracks = []; // Convert each variant to a track. - for (const variant of this.getSelectableVariants_()) { + for (const variant of this.manifest_.variants) { + if (!shaka.util.StreamUtils.isPlayable(variant)) { + continue; + } + const track = shaka.util.StreamUtils.variantToTrack(variant); track.active = variant == currentVariant; @@ -2855,9 +2822,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } /** - * Return a list of text tracks that can be switched to in the current period. - * If there are multiple periods, you must seek to a period in order to get - * text tracks from that period. + * Return a list of text tracks that can be switched to. * *

* If the player has not loaded content, this will return an empty list. @@ -2866,14 +2831,15 @@ shaka.Player = class extends shaka.util.FakeEventTarget { * @export */ getTextTracks() { - if (this.manifest_ && this.playhead_) { - const currentText = this.getPresentationText_(); + if (this.manifest_) { + const currentTextStream = this.streamingEngine_ ? + this.streamingEngine_.getCurrentTextStream() : null; const tracks = []; // Convert all selectable text streams to tracks. - for (const text of this.getSelectableText_()) { + for (const text of this.manifest_.textStreams) { const track = shaka.util.StreamUtils.textStreamToTrack(text); - track.active = text == currentText; + track.active = text == currentTextStream; tracks.push(track); } @@ -2889,10 +2855,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } /** - * Select a specific text track from the current period. track - * should come from a call to getTextTracks. If the track is not - * found in the current period, this will be a no-op. If the player has not - * loaded content, this will be a no-op. + * Select a specific text track. track should come from a call to + * getTextTracks. If the track is not found, this will be a + * no-op. If the player has not loaded content, this will be a no-op. * *

* Note that AdaptationEvents are not fired for manual track @@ -2903,20 +2868,23 @@ shaka.Player = class extends shaka.util.FakeEventTarget { */ selectTextTrack(track) { if (this.manifest_ && this.streamingEngine_) { - /** @type {shaka.extern.Period} */ - const period = this.getPresentationPeriod_(); - const stream = period.textStreams.find((stream) => stream.id == track.id); + const stream = this.manifest_.textStreams.find( + (stream) => stream.id == track.id); if (!stream) { shaka.log.error('No stream with id', track.id); return; } - // Add entries to the history. - this.addTextStreamToSwitchHistory_( - period, stream, /* fromAdaptation= */ false); + if (stream == this.streamingEngine_.getCurrentTextStream()) { + shaka.log.debug('Text track already selected.'); + return; + } - this.switchTextStream_(stream); + // Add entries to the history. + this.addTextStreamToSwitchHistory_(stream, /* fromAdaptation= */ false); + this.streamingEngine_.switchTextStream(stream); + this.onTextChanged_(); // Workaround for https://github.com/google/shaka-player/issues/1299 // When track is selected, back-propagate the language to @@ -2939,11 +2907,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } /** - * Select a specific variant track to play from the current period. - * track should come from a call to - * getVariantTracks. If track cannot be found in the - * current variant, this will be a no-op. If the player has not loaded - * content, this will be a no-op. + * Select a specific variant track to play. track should come + * from a call to getVariantTracks. If track cannot + * be found, this will be a no-op. If the player has not loaded content, this + * will be a no-op. * *

* Changing variants will take effect once the currently buffered content has @@ -2967,11 +2934,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget { * safeMargin value. * @export */ - selectVariantTrack(track, clearBuffer, safeMargin = 0) { + selectVariantTrack(track, clearBuffer = false, safeMargin = 0) { if (this.manifest_ && this.streamingEngine_) { - /** @type {shaka.extern.Period} */ - const period = this.getPresentationPeriod_(); - if (this.config_.abr.enabled) { shaka.log.alwaysWarn('Changing tracks while abr manager is enabled ' + 'will likely result in the selected track ' + @@ -2979,7 +2943,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget { 'calling selectVariantTrack().'); } - const variant = period.variants.find((variant) => variant.id == track.id); + const variant = this.manifest_.variants.find( + (variant) => variant.id == track.id); if (!variant) { shaka.log.error('No variant with id', track.id); return; @@ -2994,10 +2959,16 @@ shaka.Player = class extends shaka.util.FakeEventTarget { return; } + if (variant == this.streamingEngine_.getCurrentVariant()) { + shaka.log.debug('Variant already selected.'); + return; + } + // Add entries to the history. - this.addVariantToSwitchHistory_(period, variant, - /* fromAdaptation= */ false); - this.switchVariant_(variant, clearBuffer, safeMargin); + this.addVariantToSwitchHistory_(variant, /* fromAdaptation= */ false); + this.streamingEngine_.switchVariant(variant, clearBuffer, safeMargin); + // Dispatch a 'variantchanged' event + this.onVariantChanged_(); // Workaround for https://github.com/google/shaka-player/issues/1299 // When track is selected, back-propagate the language to @@ -3006,7 +2977,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { variant); // Update AbrManager variants to match these new settings. - this.chooseVariant_(period.variants); + this.updateAbrManagerVariants_(); } else if (this.video_ && this.video_.audioTracks) { // Safari's native HLS won't let you choose an explicit variant, though // you can choose audio languages this way. @@ -3022,9 +2993,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } /** - * Return a list of audio language-role combinations available for the current - * period. If the player has not loaded any content, this will return an empty - * list. + * Return a list of audio language-role combinations available. If the + * player has not loaded any content, this will return an empty list. * * @return {!Array.} * @export @@ -3034,9 +3004,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } /** - * Return a list of text language-role combinations available for the current - * period. If the player has not loaded any content, this will be return an - * empty list. + * Return a list of text language-role combinations available. If the player + * has not loaded any content, this will be return an empty list. * * @return {!Array.} * @export @@ -3046,8 +3015,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } /** - * Return a list of audio languages available for the current period. If the - * player has not loaded any content, this will return an empty list. + * Return a list of audio languages available. If the player has not loaded + * any content, this will return an empty list. * * @return {!Array.} * @export @@ -3057,8 +3026,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } /** - * Return a list of text languages available for the current period. If the - * player has not loaded any content, this will return an empty list. + * Return a list of text languages available. If the player has not loaded + * any content, this will return an empty list. * * @return {!Array.} * @export @@ -3080,14 +3049,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget { const LanguageUtils = shaka.util.LanguageUtils; if (this.manifest_ && this.playhead_) { - /** @type {shaka.extern.Period} */ - const period = this.getPresentationPeriod_(); - this.currentAdaptationSetCriteria_ = new shaka.media.PreferenceBasedCriteria(language, role || '', /* channelCount= */ 0, /* label= */ '', /* type= */ 'audio'); - this.chooseVariantAndSwitch_(period); + this.chooseVariantAndSwitch_(); } else if (this.video_ && this.video_.audioTracks) { const audioTracks = Array.from(this.video_.audioTracks); const selectedLanguage = LanguageUtils.normalize(language); @@ -3115,18 +3081,21 @@ shaka.Player = class extends shaka.util.FakeEventTarget { const LanguageUtils = shaka.util.LanguageUtils; if (this.manifest_ && this.playhead_) { - /** @type {shaka.extern.Period} */ - const period = this.getPresentationPeriod_(); - this.currentTextLanguage_ = language; this.currentTextRole_ = role || ''; - const chosenText = this.chooseTextStream_(period.textStreams); + const chosenText = this.chooseTextStream_(); if (chosenText) { + if (chosenText == this.streamingEngine_.getCurrentTextStream()) { + shaka.log.debug('Text track already selected.'); + return; + } + this.addTextStreamToSwitchHistory_( - period, chosenText, /* fromAdaptation= */ false); + chosenText, /* fromAdaptation= */ false); if (this.shouldStreamText_()) { - this.switchTextStream_(chosenText); + this.streamingEngine_.switchTextStream(chosenText); + this.onTextChanged_(); } } } else { @@ -3152,11 +3121,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget { */ selectVariantsByLabel(label) { if (this.manifest_ && this.playhead_) { - /** @type {shaka.extern.Period} */ - const period = this.getPresentationPeriod_(); - let firstVariantWithLabel = null; - for (const variant of this.getSelectableVariants_()) { + for (const variant of this.manifest_.variants) { if (variant.audio.label == label) { firstVariantWithLabel = variant; break; @@ -3177,7 +3143,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { new shaka.media.PreferenceBasedCriteria( firstVariantWithLabel.language, '', 0, label); - this.chooseVariantAndSwitch_(period); + this.chooseVariantAndSwitch_(); } } @@ -3212,10 +3178,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget { * state, the request will be applied next time content is loaded. * * @param {boolean} isVisible - * @return {!Promise} * @export */ - async setTextTrackVisibility(isVisible) { + setTextTrackVisibility(isVisible) { const oldVisibilty = this.isTextVisible_; // Convert to boolean in case apps pass 0/1 instead false/true. const newVisibility = !!isVisible; @@ -3239,15 +3204,15 @@ shaka.Player = class extends shaka.util.FakeEventTarget { if (!this.config_.streaming.alwaysStreamText) { if (newVisibility) { // Find the text stream that best matches the user's preferences. - /** @type {shaka.extern.Period} */ - const period = this.getPresentationPeriod_(); const streams = shaka.util.StreamUtils.filterStreamsByLanguageAndRole( - period.textStreams, this.currentTextLanguage_, + this.manifest_.textStreams, + this.currentTextLanguage_, this.currentTextRole_); // It is possible that there are no streams to play. if (streams.length > 0) { - await this.streamingEngine_.loadNewTextStream(streams[0]); + this.streamingEngine_.switchTextStream(streams[0]); + this.onTextChanged_(); } } else { this.streamingEngine_.unloadTextStream(); @@ -3406,9 +3371,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget { if (this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE) { // Event through we are loaded, it is still possible that we don't have a - // presentation variant yet because we set the load mode before we select - // the first variant to stream. - const variant = this.getPresentationVariant_(); + // variant yet because we set the load mode before we select the first + // variant to stream. + const variant = this.streamingEngine_.getCurrentVariant(); if (variant) { const rate = this.playRateController_ ? @@ -3433,11 +3398,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } /** - * Adds the given text track to the current Period. load() must - * resolve before calling. The current Period or the presentation must have a - * duration. - * This returns a Promise that will resolve with the track that was created, - * when that track can be switched to. + * Adds the given text track to the loaded manifest. load() must + * resolve before calling. The presentation must have a duration. + * + * This returns the created track, which can immediately be selected by the + * application. The track will not be automatically selected. * * @param {string} uri * @param {string} language @@ -3445,10 +3410,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget { * @param {string} mime * @param {string=} codec * @param {string=} label - * @return {!Promise.} + * @return {shaka.extern.Track} * @export */ - async addTextTrack(uri, language, kind, mime, codec, label) { + addTextTrack(uri, language, kind, mime, codec, label) { // TODO: Add an actual error for this. if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) { shaka.log.error('Cannot add text when loaded with src='); @@ -3461,23 +3426,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget { throw new Error('State error!'); } - /** @type {shaka.extern.Period} */ - const period = this.getPresentationPeriod_(); - const ContentType = shaka.util.ManifestParserUtils.ContentType; - // Get the Period duration. - /** @type {number} */ - const periodIndex = this.manifest_.periods.indexOf(period); - /** @type {number} */ - const nextPeriodIndex = periodIndex + 1; - /** @type {number} */ - const nextPeriodStart = nextPeriodIndex >= this.manifest_.periods.length ? - this.manifest_.presentationTimeline.getDuration() : - this.manifest_.periods[nextPeriodIndex].startTime; - /** @type {number} */ - const periodDuration = nextPeriodStart - period.startTime; - if (periodDuration == Infinity) { + const duration = this.manifest_.presentationTimeline.getDuration(); + if (duration == Infinity) { throw new shaka.util.Error( shaka.util.Error.Severity.RECOVERABLE, shaka.util.Error.Category.MANIFEST, @@ -3490,14 +3442,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget { originalId: null, createSegmentIndex: () => Promise.resolve(), segmentIndex: shaka.media.SegmentIndex.forSingleSegment( - /* startTime= */ period.startTime, - /* duration= */ periodDuration, + /* startTime= */ 0, + /* duration= */ duration, /* uris= */ [uri]), mimeType: mime, codecs: codec || '', kind: kind, encrypted: false, - keyId: null, + keyIds: [], language: language, label: label || null, type: ContentType.TEXT, @@ -3510,28 +3462,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget { closedCaptions: null, }; - // Add the stream to the loading list to ensure it isn't switched to while - // it is initializing. - this.loadingTextStreams_.add(stream); - period.textStreams.push(stream); - - await this.streamingEngine_.loadNewTextStream(stream); - goog.asserts.assert(period, 'The period should still be non-null here.'); - - const activeText = this.streamingEngine_.getBufferingText(); - if (activeText) { - // If this was the first text stream, StreamingEngine will start streaming - // it in loadNewTextStream. To reflect this, update the active stream. - this.activeStreams_.useText(period, activeText); - } - - // Remove the stream from the loading list. - this.loadingTextStreams_.delete(stream); - - shaka.log.debug('Choosing new streams after adding a text stream'); - this.chooseStreamsAndSwitch_(period); + this.manifest_.textStreams.push(stream); this.onTracksChanged_(); - return shaka.util.StreamUtils.textStreamToTrack(stream); } @@ -3594,25 +3526,21 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } /** - * @param {shaka.extern.Period} period * @param {shaka.extern.Variant} variant * @param {boolean} fromAdaptation * @private */ - addVariantToSwitchHistory_(period, variant, fromAdaptation) { - this.activeStreams_.useVariant(period, variant); + addVariantToSwitchHistory_(variant, fromAdaptation) { const switchHistory = this.stats_.getSwitchHistory(); switchHistory.updateCurrentVariant(variant, fromAdaptation); } /** - * @param {shaka.extern.Period} period * @param {shaka.extern.Stream} textStream * @param {boolean} fromAdaptation * @private */ - addTextStreamToSwitchHistory_(period, textStream, fromAdaptation) { - this.activeStreams_.useText(period, textStream); + addTextStreamToSwitchHistory_(textStream, fromAdaptation) { const switchHistory = this.stats_.getSwitchHistory(); switchHistory.updateCurrentText(textStream, fromAdaptation); } @@ -3658,161 +3586,90 @@ shaka.Player = class extends shaka.util.FakeEventTarget { /** * For CEA closed captions embedded in the video streams, create dummy text * stream. - * @param {!Array.} periods + * @param {!Array.} variants * @private */ - createTextStreamsForClosedCaptions_(periods) { + createTextStreamsForClosedCaptions_(variants) { const ContentType = shaka.util.ManifestParserUtils.ContentType; const TextStreamKind = shaka.util.ManifestParserUtils.TextStreamKind; - for (const period of periods) { - // A map of the closed captions id and the new dummy text stream. - const closedCaptionsMap = new Map(); - for (const variant of period.variants) { - if (variant.video && variant.video.closedCaptions) { - const video = variant.video; - for (const id of video.closedCaptions.keys()) { - if (!closedCaptionsMap.has(id)) { - const textStream = { - id: this.nextExternalStreamId_++, // A globally unique ID. - originalId: id, // The CC ID string, like 'CC1', 'CC3', etc. - createSegmentIndex: () => Promise.resolve(), - segmentIndex: null, - mimeType: shaka.util.MimeUtils.CLOSED_CAPTION_MIMETYPE, - codecs: '', - kind: TextStreamKind.CLOSED_CAPTION, - encrypted: false, - keyId: null, - language: video.closedCaptions.get(id), - label: null, - type: ContentType.TEXT, - primary: false, - trickModeVideo: null, - emsgSchemeIdUris: null, - roles: video.roles, - channelsCount: null, - audioSamplingRate: null, - closedCaptions: null, - }; - closedCaptionsMap.set(id, textStream); - } + // A map of the closed captions id and the new dummy text stream. + const closedCaptionsMap = new Map(); + for (const variant of this.manifest_.variants) { + if (variant.video && variant.video.closedCaptions) { + const video = variant.video; + for (const id of video.closedCaptions.keys()) { + if (!closedCaptionsMap.has(id)) { + const textStream = { + id: this.nextExternalStreamId_++, // A globally unique ID. + originalId: id, // The CC ID string, like 'CC1', 'CC3', etc. + createSegmentIndex: () => Promise.resolve(), + segmentIndex: null, + mimeType: shaka.util.MimeUtils.CLOSED_CAPTION_MIMETYPE, + codecs: '', + kind: TextStreamKind.CLOSED_CAPTION, + encrypted: false, + keyIds: [], + language: video.closedCaptions.get(id), + label: null, + type: ContentType.TEXT, + primary: false, + trickModeVideo: null, + emsgSchemeIdUris: null, + roles: video.roles, + channelsCount: null, + audioSamplingRate: null, + closedCaptions: null, + }; + closedCaptionsMap.set(id, textStream); } } } - for (const textStream of closedCaptionsMap.values()) { - period.textStreams.push(textStream); - } + } + for (const textStream of closedCaptionsMap.values()) { + this.manifest_.textStreams.push(textStream); } } /** - * Filters a list of periods. - * @param {!Array.} periods + * Filters a manifest, removing unplayable streams/variants. + * + * @param {?shaka.extern.Manifest} manifest * @private */ - filterAllPeriods_(periods) { + filterManifest_(manifest) { + goog.asserts.assert(manifest, 'Manifest should exist!'); goog.asserts.assert(this.video_, 'Must not be destroyed'); - const ArrayUtils = shaka.util.ArrayUtils; const StreamUtils = shaka.util.StreamUtils; - /** @type {?shaka.extern.Stream} */ - const activeAudio = this.streamingEngine_ ? - this.streamingEngine_.getBufferingAudio() : - null; - /** @type {?shaka.extern.Stream} */ - const activeVideo = this.streamingEngine_ ? - this.streamingEngine_.getBufferingVideo() : - null; + /** @type {?shaka.extern.Variant} */ + const currentVariant = this.streamingEngine_ ? + this.streamingEngine_.getCurrentVariant() : null; - for (const period of periods) { - StreamUtils.filterNewPeriod( - this.drmEngine_, activeAudio, activeVideo, period); - } + StreamUtils.filterManifest( + this.drmEngine_, currentVariant, manifest); - const validPeriodsCount = ArrayUtils.count(periods, (period) => { - return period.variants.some(StreamUtils.isPlayable); - }); + const valid = manifest.variants.some(StreamUtils.isPlayable); - // If none of the periods are playable, throw + // If none of the variants are playable, throw // CONTENT_UNSUPPORTED_BY_BROWSER. - if (validPeriodsCount == 0) { + if (!valid) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.CONTENT_UNSUPPORTED_BY_BROWSER); } - // If only some of the periods are playable, throw UNPLAYABLE_PERIOD. - if (validPeriodsCount < periods.length) { - throw new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.UNPLAYABLE_PERIOD); - } - - for (const period of periods) { - const tracksChanged = shaka.util.StreamUtils.applyRestrictions( - period.variants, this.config_.restrictions, this.maxHwRes_); - if (tracksChanged && this.streamingEngine_ && - this.getPresentationPeriod_() == period) { - this.onTracksChanged_(); - } - - this.checkRestrictedVariants_(period.variants); - } - } - - /** - * Filters a new period. - * @param {shaka.extern.Period} period - * @private - */ - filterNewPeriod_(period) { - goog.asserts.assert(this.video_, 'Must not be destroyed'); - const StreamUtils = shaka.util.StreamUtils; - - /** @type {?shaka.extern.Stream} */ - const activeAudio = this.streamingEngine_ ? - this.streamingEngine_.getBufferingAudio() : - null; - /** @type {?shaka.extern.Stream} */ - const activeVideo = this.streamingEngine_ ? - this.streamingEngine_.getBufferingVideo() : - null; - - StreamUtils.filterNewPeriod( - this.drmEngine_, activeAudio, activeVideo, period); - - /** @type {!Array.} */ - const variants = period.variants; - - // Check for playable variants before restrictions, so that we can give a - // special error when there were tracks but they were all filtered. - const hasPlayableVariant = variants.some(StreamUtils.isPlayable); - if (!hasPlayableVariant) { - throw new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.UNPLAYABLE_PERIOD); - } - - this.checkRestrictedVariants_(period.variants); - const tracksChanged = shaka.util.StreamUtils.applyRestrictions( - variants, this.config_.restrictions, this.maxHwRes_); - - // Trigger the track change event if the restrictions now prevent use from - // using a variant that we previously thought we could use. - if (tracksChanged && this.streamingEngine_ && - this.getPresentationPeriod_() == period) { + manifest.variants, this.config_.restrictions, this.maxHwRes_); + if (tracksChanged && this.streamingEngine_) { this.onTracksChanged_(); } - // For new Periods, we may need to create new sessions for any new init - // data. + // We may need to create new sessions for any new init data. const curDrmInfo = this.drmEngine_ ? this.drmEngine_.getDrmInfo() : null; if (curDrmInfo) { - for (const variant of variants) { + for (const variant of manifest.variants) { for (const drmInfo of variant.drmInfos) { // Ignore any data for different key systems. if (drmInfo.keySystem == curDrmInfo.keySystem) { @@ -3824,138 +3681,46 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } } } - } - - /** - * Switches to the given variant, deferring if needed. - * @param {shaka.extern.Variant} variant - * @param {boolean=} clearBuffer - * @param {number=} safeMargin - * @return {boolean} - * @private - */ - switchVariant_(variant, clearBuffer = false, safeMargin = 0) { - if (this.switchingPeriods_) { - // Store this action for later. - this.deferredVariant_ = variant; - this.deferredVariantClearBuffer_ = clearBuffer; - this.deferredVariantClearBufferSafeMargin_ = safeMargin; - return true; - } else { - // Act now. - const changed = - this.streamingEngine_.switchVariant(variant, clearBuffer, safeMargin); - if (changed) { - // Dispatch a 'variantchanged' event - this.onVariantChanged_(); - } - return changed; - } - } - /** - * Switches to the given text stream, deferring if needed. - * @param {shaka.extern.Stream} textStream - * @return {boolean} - * @private - */ - switchTextStream_(textStream) { - if (this.switchingPeriods_) { - // Store this action for later. - this.deferredTextStream_ = textStream; - return true; - } else { - // Act now. - const changed = this.streamingEngine_.switchTextStream(textStream); - if (changed) { - this.onTextChanged_(); - } - return changed; - } - } - - /** - * Verifies that the active streams according to the player match those in - * StreamingEngine. - * @private - */ - assertCorrectActiveStreams_() { - if (!this.streamingEngine_ || !this.manifest_ || !goog.DEBUG) { - return; - } - - const activePeriod = this.streamingEngine_.getBufferingPeriod(); - /** @type {shaka.extern.Period} */ - const currentPeriod = this.getPresentationPeriod_(); - if (activePeriod == null || activePeriod != currentPeriod) { - return; - } - - const activeAudio = this.streamingEngine_.getBufferingAudio(); - const activeVideo = this.streamingEngine_.getBufferingVideo(); - const activeText = this.streamingEngine_.getBufferingText(); - - // If we have deferred variants/text we want to compare against those rather - // than what we are actually streaming. - const expectedAudio = this.deferredVariant_ ? - this.deferredVariant_.audio : - activeAudio; - - const expectedVideo = this.deferredVariant_ ? - this.deferredVariant_.video : - activeVideo; - - const expectedText = this.deferredTextStream_ || activeText; - - const actualVariant = this.activeStreams_.getVariant(currentPeriod); - const actualText = this.activeStreams_.getText(currentPeriod); - - goog.asserts.assert( - actualVariant.audio == expectedAudio, - 'Inconsistent active audio stream'); - goog.asserts.assert( - actualVariant.video == expectedVideo, - 'Inconsistent active video stream'); - - // Because we always set a text stream to be active in the active stream - // map, regardless of whether or not we are actually streaming text, it is - // possible for these to be out of line. - goog.asserts.assert( - expectedText == null || actualText == expectedText, - 'Inconsistent active text stream'); + this.checkRestrictedVariants_(manifest); } /** + * @param {shaka.extern.Variant} initialVariant * @param {number} time - * @return {number} + * @return {!Promise.} * @private */ - adjustStartTime_(time) { + async adjustStartTime_(initialVariant, time) { /** @type {?shaka.extern.Stream} */ - const activeAudio = this.streamingEngine_.getBufferingAudio(); + const activeAudio = initialVariant.audio; /** @type {?shaka.extern.Stream} */ - const activeVideo = this.streamingEngine_.getBufferingVideo(); - /** @type {shaka.extern.Period} */ - const period = this.getPresentationPeriod_(); + const activeVideo = initialVariant.video; - // This method is called after StreamingEngine.init resolves, which means - // that all the active streams have had createSegmentIndex called. - function getAdjustedTime(stream, time) { + /** + * @param {?shaka.extern.Stream} stream + * @param {number} time + * @return {!Promise.} + */ + const getAdjustedTime = async (stream, time) => { if (!stream) { return null; } - const ref = stream.segmentIndex[Symbol.iterator]().seek( - time - period.startTime); + + await stream.createSegmentIndex(); + const ref = stream.segmentIndex[Symbol.iterator]().seek(time); if (!ref) { return null; } - const refTime = ref.startTime + period.startTime; - goog.asserts.assert(refTime <= time, 'Segment should start before time'); + + const refTime = ref.startTime; + goog.asserts.assert(refTime <= time, + 'Segment should start before target time!'); return refTime; - } + }; - const audioStartTime = getAdjustedTime(activeAudio, time); - const videoStartTime = getAdjustedTime(activeVideo, time); + const audioStartTime = await getAdjustedTime(activeAudio, time); + const videoStartTime = await getAdjustedTime(activeVideo, time); // If we have both video and audio times, pick the larger one. If we picked // the smaller one, that one will download an entire segment to buffer the @@ -3997,14 +3762,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.dispatchEvent(this.makeEvent_(eventName, {'buffering': isBuffering})); } - /** - * Callback from PlayheadObserver. - * @private - */ - onChangePeriod_() { - this.onTracksChanged_(); - } - /** * A callback for when the playback rate changes. We need to watch the * playback rate so that if the playback rate on the media element changes @@ -4087,31 +3844,24 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } /** - * Chooses a variant from all possible variants while taking into account - * restrictions, preferences, and ABR. + * Update AbrManager with variants while taking into account restrictions, + * preferences, and ABR. * - * On error, this dispatches an error event and returns null. + * On error, this dispatches an error event and returns false. * - * @param {!Array.} allVariants - * @return {?shaka.extern.Variant} + * @return {boolean} True if successful. * @private */ - chooseVariant_(allVariants) { - goog.asserts.assert(this.config_, 'Must not be destroyed'); - + updateAbrManagerVariants_() { try { - // |variants| are the filtered variants, use |period.variants| so we know - // why they we restricted. - this.checkRestrictedVariants_(allVariants); + goog.asserts.assert(this.manifest_, 'Manifest should exist by now!'); + this.checkRestrictedVariants_(this.manifest_); } catch (e) { this.onError_(e); - return null; + return false; } - goog.asserts.assert( - allVariants.length, 'Should have thrown for no Variants.'); - - const playableVariants = allVariants.filter((variant) => { + const playableVariants = this.manifest_.variants.filter((variant) => { return shaka.util.StreamUtils.isPlayable(variant); }); @@ -4119,213 +3869,98 @@ shaka.Player = class extends shaka.util.FakeEventTarget { const adaptationSet = this.currentAdaptationSetCriteria_.create( playableVariants); this.abrManager_.setVariants(Array.from(adaptationSet.values())); - return this.abrManager_.chooseVariant(); + return true; + } + + /** + * Chooses a variant from all possible variants while taking into account + * restrictions, preferences, and ABR. + * + * On error, this dispatches an error event and returns null. + * + * @return {?shaka.extern.Variant} + * @private + */ + chooseVariant_() { + if (this.updateAbrManagerVariants_()) { + return this.abrManager_.chooseVariant(); + } else { + return null; + } } /** * Choose a text stream from all possible text streams while taking into * account user preference. * - * @param {!Array.} textStreams * @return {?shaka.extern.Stream} * @private */ - chooseTextStream_(textStreams) { + chooseTextStream_() { const subset = shaka.util.StreamUtils.filterStreamsByLanguageAndRole( - textStreams, + this.manifest_.textStreams, this.currentTextLanguage_, this.currentTextRole_); - return subset[0] || null; } /** - * Chooses streams from the given Period and switches to them. - * Called after a config change, a new text stream, a key status event, or an - * explicit language change. + * Chooses a new Variant. If the new variant differs from the old one, it + * adds the new one to the switch history and switches to it. * - * @param {!shaka.extern.Period} period - * @private - */ - chooseStreamsAndSwitch_(period) { - // Because we know we will change both variants and text, tell - // the switch methods not to through separate onAdaptation events, - // so UI doesn't update twice in a row. Switch everything, then throw - // a single event from this method with onAdaptation_(). - const variantChanged = - this.chooseVariantAndSwitch_(period, /* fireAdaptationEvent= */ false); - const textChanged = this.chooseTextAndSwitch_(period); - if (variantChanged || textChanged) { - this.onAdaptation_(); - } - } - - - /** - * Chooses a variant from the given Period and switches to it. + * Called after a config change, a key status event, or an explicit language + * change. * - * @param {!shaka.extern.Period} period - * @param {boolean=} fireAdaptationEvent - * @return {boolean} * @private */ - chooseVariantAndSwitch_(period, fireAdaptationEvent = true) { + chooseVariantAndSwitch_() { goog.asserts.assert(this.config_, 'Must not be destroyed'); // Because we're running this after a config change (manual language - // change), a new text stream, or a key status event, and because switching - // to an active stream is a no-op, it is always okay to clear the buffer + // change) or a key status event, it is always okay to clear the buffer // here. - const chosenVariant = this.chooseVariant_(period.variants); - let changed = false; + const chosenVariant = this.chooseVariant_(); if (chosenVariant) { - this.addVariantToSwitchHistory_( - period, chosenVariant, /* fromAdaptation= */ true); - changed = this.switchVariant_(chosenVariant, /* clearBuffers= */ true); - } - - if (fireAdaptationEvent && changed) { - // Send an adaptation event so that the UI can show the new - // language/tracks. - this.onAdaptation_(); - } - return changed; - } - - - /** - * If text should be streamed, chooses a text stream from - * the given Period and switches to it. - * - * @param {!shaka.extern.Period} period - * @return {boolean} - * @private - */ - chooseTextAndSwitch_(period) { - goog.asserts.assert(this.config_, 'Must not be destroyed'); + if (chosenVariant == this.streamingEngine_.getCurrentVariant()) { + shaka.log.debug('Variant already selected.'); + return; + } - // Only switch text if we should be streaming text right now. - const chosenText = this.chooseTextStream_(period.textStreams); - let changed = false; - if (chosenText && this.shouldStreamText_()) { - this.addTextStreamToSwitchHistory_( - period, chosenText, /* fromAdaptation= */ true); - changed = this.switchTextStream_(chosenText); + this.addVariantToSwitchHistory_( + chosenVariant, /* fromAdaptation= */ true); + this.streamingEngine_.switchVariant( + chosenVariant, /* clearBuffers= */ true, /* safeMargin= */ 0); + // Dispatch a 'variantchanged' event + this.onVariantChanged_(); } - return changed; - } - - /** - * Callback from StreamingEngine, invoked when a period starts. This method - * must always "succeed" so it may not throw an error. Any errors must be - * routed to |onError|. - * - * @param {!shaka.extern.Period} period - * @return {shaka.media.StreamingEngine.ChosenStreams} - * An object containing the chosen variant and text stream. - * @private - */ - onChooseStreams_(period) { - shaka.log.debug('onChooseStreams_', period); - - goog.asserts.assert(this.config_, 'Must not be destroyed'); - - try { - shaka.log.v2('onChooseStreams_, choosing variant from ', period.variants); - shaka.log.v2('onChooseStreams_, choosing text from ', period.textStreams); - - const chosen = this.chooseStreams_(period); - - shaka.log.v2('onChooseStreams_, chose variant ', chosen.variant); - shaka.log.v2('onChooseStreams_, chose text ', chosen.text); - - return chosen; - } catch (e) { - this.onError_(e); - return {variant: null, text: null}; - } + // Send an adaptation event so that the UI can show the new + // language/tracks. + this.onAdaptation_(); } /** - * This is the internal logic for |onChooseStreams_|. This separation is done - * to allow this implementation to throw errors without consequence. - * - * @param {shaka.extern.Period} period - * The period that we are selecting streams from. - * @return {shaka.media.StreamingEngine.ChosenStreams} - * An object containing the chosen variant and text stream. + * Decide during startup if text should be streamed/shown. * @private */ - chooseStreams_(period) { - // We are switching Periods, so the AbrManager will be disabled. But if we - // want to abr.enabled, we do not want to call AbrManager.enable before - // canSwitch_ is called. - this.switchingPeriods_ = true; - this.abrManager_.disable(); - this.onAbrStatusChanged_(); - - shaka.log.debug('Choosing new streams after period changed'); - - let chosenVariant = this.chooseVariant_(period.variants); - let chosenText = this.chooseTextStream_(period.textStreams); - - // Ignore deferred variant or text streams only if we are starting a new - // period. In this case, any deferred switches were from an older period, - // so they do not apply. We can still have deferred switches from the - // current period in the case of an early call to select*Track while we are - // setting up the first period. This can happen with the 'streaming' event. - if (this.deferredVariant_) { - if (period.variants.includes(this.deferredVariant_)) { - chosenVariant = this.deferredVariant_; - } - this.deferredVariant_ = null; - } - - if (this.deferredTextStream_) { - if (period.textStreams.includes(this.deferredTextStream_)) { - chosenText = this.deferredTextStream_; - } - this.deferredTextStream_ = null; - } - - if (chosenVariant) { - this.addVariantToSwitchHistory_( - period, chosenVariant, /* fromAdaptation= */ true); - } - - if (chosenText) { - this.addTextStreamToSwitchHistory_( - period, chosenText, /* fromAdaptation= */ true); - } - + setInitialTextState_(initialVariant, initialTextStream) { // Check if we should show text (based on difference between audio and text - // languages). Only check this during startup so we don't "pop-up" captions - // mid playback. - const startingUp = !this.streamingEngine_.getBufferingPeriod(); - const chosenAudio = chosenVariant ? chosenVariant.audio : null; - if (startingUp && chosenText) { - if (chosenAudio && this.shouldShowText_(chosenAudio, chosenText)) { + // languages). + if (initialTextStream) { + if (initialVariant.audio && this.shouldInitiallyShowText_( + initialVariant.audio, initialTextStream)) { this.isTextVisible_ = true; } if (this.isTextVisible_) { // If the cached value says to show text, then update the text displayer - // since it defaults to not shown. Note that returning the |chosenText| - // below will make StreamingEngine stream the text. + // since it defaults to not shown. this.mediaSourceEngine_.getTextDisplayer().setTextVisibility(true); goog.asserts.assert(this.shouldStreamText_(), 'Should be streaming text'); } this.onTextTrackVisibility_(); - } - - // Don't fire a tracks-changed event since we aren't inside the new Period - // yet. - // Don't initialize with a text stream unless we should be streaming text. - if (this.shouldStreamText_()) { - return {variant: chosenVariant, text: chosenText}; } else { - return {variant: chosenVariant, text: null}; + this.isTextVisible_ = false; } } @@ -4350,7 +3985,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { * @return {boolean} * @private */ - shouldShowText_(audioStream, textStream) { + shouldInitiallyShowText_(audioStream, textStream) { const LanguageUtils = shaka.util.LanguageUtils; /** @type {string} */ @@ -4366,37 +4001,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget { !LanguageUtils.areLanguageCompatible(audioLocale, textLocale)); } - /** - * Callback from StreamingEngine, invoked when the period is set up. - * - * @private - */ - canSwitch_() { - shaka.log.debug('canSwitch_'); - goog.asserts.assert(this.config_, 'Must not be destroyed'); - - this.switchingPeriods_ = false; - - if (this.config_.abr.enabled) { - this.abrManager_.enable(); - this.onAbrStatusChanged_(); - } - - // If we still have deferred switches, switch now. - if (this.deferredVariant_) { - this.streamingEngine_.switchVariant( - this.deferredVariant_, this.deferredVariantClearBuffer_, - this.deferredVariantClearBufferSafeMargin_); - this.onVariantChanged_(); - this.deferredVariant_ = null; - } - if (this.deferredTextStream_) { - this.streamingEngine_.switchTextStream(this.deferredTextStream_); - this.onTextChanged_(); - this.deferredTextStream_ = null; - } - } - /** * Callback from StreamingEngine. * @@ -4437,26 +4041,22 @@ shaka.Player = class extends shaka.util.FakeEventTarget { shaka.log.debug('switch_'); goog.asserts.assert(this.config_.abr.enabled, 'AbrManager should not call switch while disabled!'); - goog.asserts.assert(!this.switchingPeriods_, - 'AbrManager should not call switch while transitioning between ' + - 'Periods!'); goog.asserts.assert(this.manifest_, 'We need a manifest to switch ' + 'variants.'); - const period = this.findPeriodWithVariant_(variant); - goog.asserts.assert(period, 'A period should contain the variant.'); - - this.addVariantToSwitchHistory_( - period, variant, /* fromAdaptation= */ true); - if (!this.streamingEngine_) { // There's no way to change it. return; } - if (this.streamingEngine_.switchVariant(variant, clearBuffer, safeMargin)) { - this.onAdaptation_(); + if (variant == this.streamingEngine_.getCurrentVariant()) { + // This isn't a change. + return; } + + this.addVariantToSwitchHistory_(variant, /* fromAdaptation= */ true); + this.streamingEngine_.switchVariant(variant, clearBuffer, safeMargin); + this.onAdaptation_(); } /** @@ -4628,12 +4228,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget { return; } - const restrictedStatuses = shaka.Player.restrictedStatuses_; - - /** @type {shaka.extern.Period} */ - const currentPeriod = this.getPresentationPeriod_(); - let tracksChanged = false; - const keyIds = Object.keys(keyStatusMap); if (keyIds.length == 0) { shaka.log.warning( @@ -4646,7 +4240,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget { // byte). In this case, it is only used to report global success/failure. // See note about old platforms in: https://bit.ly/2tpez5Z const isGlobalStatus = keyIds.length == 1 && keyIds[0] == '00'; - if (isGlobalStatus) { shaka.log.warning( 'Got a synthetic key status event, so we don\'t know the real key ' + @@ -4654,48 +4247,48 @@ shaka.Player = class extends shaka.util.FakeEventTarget { 'restrictions so we don\'t select those tracks.'); } + const restrictedStatuses = shaka.Player.restrictedStatuses_; + let tracksChanged = false; + // Only filter tracks for keys if we have some key statuses to look at. if (keyIds.length) { - for (const period of this.manifest_.periods) { - for (const variant of period.variants) { - const streams = shaka.util.StreamUtils.getVariantStreams(variant); - - for (const stream of streams) { - const originalAllowed = variant.allowedByKeySystem; - - // Only update if we have a key ID for the stream. - // If the key isn't present, then we don't have that key and the - // track should be restricted. - if (stream.keyId) { - const keyStatus = - keyStatusMap[isGlobalStatus ? '00' : stream.keyId]; - variant.allowedByKeySystem = + for (const variant of this.manifest_.variants) { + const streams = shaka.util.StreamUtils.getVariantStreams(variant); + + for (const stream of streams) { + const originalAllowed = variant.allowedByKeySystem; + + // Only update if we have key IDs for the stream. If the keys aren't + // all present, then the track should be restricted. + if (stream.keyIds.length) { + variant.allowedByKeySystem = true; + + for (const keyId of stream.keyIds) { + const keyStatus = keyStatusMap[isGlobalStatus ? '00' : keyId]; + variant.allowedByKeySystem = variant.allowedByKeySystem && !!keyStatus && !restrictedStatuses.includes(keyStatus); } + } - if (originalAllowed != variant.allowedByKeySystem) { - tracksChanged = true; - } - } // for (const stream of streams) - } // for (const variant of period.variants) - } // for (const period of this.manifest_.periods) + if (originalAllowed != variant.allowedByKeySystem) { + tracksChanged = true; + } + } // for (const stream of streams) + } // for (const variant of this.manifest_.variants) } // if (keyIds.length) - // TODO: Get StreamingEngine to track variants and create - // getBufferingVariant() - const activeAudio = this.streamingEngine_.getBufferingAudio(); - const activeVideo = this.streamingEngine_.getBufferingVideo(); - const activeVariant = shaka.util.StreamUtils.getVariantByStreams( - activeAudio, activeVideo, currentPeriod.variants); + if (tracksChanged) { + this.updateAbrManagerVariants_(); + } - if (activeVariant && !activeVariant.allowedByKeySystem) { - shaka.log.debug('Choosing new variants after key status changed'); - this.chooseVariantAndSwitch_(currentPeriod); + const currentVariant = this.streamingEngine_.getCurrentVariant(); + if (currentVariant && !currentVariant.allowedByKeySystem) { + shaka.log.debug('Choosing new streams after key status changed'); + this.chooseVariantAndSwitch_(); } if (tracksChanged) { this.onTracksChanged_(); - this.chooseVariant_(currentPeriod.variants); } } @@ -4758,13 +4351,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } /** - * Checks the given variants and if they are all restricted, throw an - * appropriate exception. + * Checks if the variants are all restricted, and throw an appropriate + * exception if so. + * + * @param {shaka.extern.Manifest} manifest * - * @param {!Array.} variants * @private */ - checkRestrictedVariants_(variants) { + checkRestrictedVariants_(manifest) { const restrictedStatuses = shaka.Player.restrictedStatuses_; const keyStatusMap = this.drmEngine_ ? this.drmEngine_.getKeyStatuses() : {}; @@ -4772,11 +4366,15 @@ shaka.Player = class extends shaka.util.FakeEventTarget { const isGlobalStatus = keyIds.length && keyIds[0] == '00'; let hasPlayable = false; - let hasAppRestrict = false; - const missingKeys = []; - const badKeyStatuses = []; + let hasAppRestrictions = false; + + /** @type {!Set.} */ + const missingKeys = new Set(); + + /** @type {!Set.} */ + const badKeyStatuses = new Set(); - for (const variant of variants) { + for (const variant of manifest.variants) { // TODO: Combine with onKeyStatus_. const streams = []; if (variant.audio) { @@ -4787,22 +4385,20 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } for (const stream of streams) { - if (stream.keyId) { - const keyStatus = keyStatusMap[isGlobalStatus ? '00' : stream.keyId]; - if (!keyStatus) { - if (!missingKeys.includes(stream.keyId)) { - missingKeys.push(stream.keyId); - } - } else if (restrictedStatuses.includes(keyStatus)) { - if (!badKeyStatuses.includes(keyStatus)) { - badKeyStatuses.push(keyStatus); + if (stream.keyIds.length) { + for (const keyId of stream.keyIds) { + const keyStatus = keyStatusMap[isGlobalStatus ? '00' : keyId]; + if (!keyStatus) { + missingKeys.add(keyId); + } else if (restrictedStatuses.includes(keyStatus)) { + badKeyStatuses.add(keyStatus); } } - } + } // if (stream.keyIds.length) } if (!variant.allowedByApplication) { - hasAppRestrict = true; + hasAppRestrictions = true; } else if (variant.allowedByKeySystem) { hasPlayable = true; } @@ -4811,9 +4407,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget { if (!hasPlayable) { /** @type {shaka.extern.RestrictionInfo} */ const data = { - hasAppRestrictions: hasAppRestrict, - missingKeys: missingKeys, - restrictedKeyStatuses: badKeyStatuses, + hasAppRestrictions, + missingKeys: Array.from(missingKeys), + restrictedKeyStatuses: Array.from(badKeyStatuses), }; throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, @@ -4915,134 +4511,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget { return pairings; } - /** - * Get the variants that the user can select. The variants will be based on - * the period that the playhead is in and what variants are playable. - * - * @return {!Array.} - * @private - */ - getSelectableVariants_() { - // Use the period that is currently playing, allowing the change to affect - // the "now". - /** @type {shaka.extern.Period} */ - const currentPeriod = this.getPresentationPeriod_(); - - // If we have been called before we load content or after we have unloaded - // content, then we should return no variants. - if (currentPeriod == null) { - return []; - } - - this.assertCorrectActiveStreams_(); - - return currentPeriod.variants.filter((variant) => { - return shaka.util.StreamUtils.isPlayable(variant); - }); - } - - /** - * Get the text streams that the user can select. The streams will be based on - * the period that the playhead is in and what streams have finished loading. - * - * @return {!Array.} - * @private - */ - getSelectableText_() { - // Use the period that is currently playing, allowing the change to affect - // the "now". - /** @type {shaka.extern.Period} */ - const currentPeriod = this.getPresentationPeriod_(); - - // If we have been called before we load content or after we have unloaded - // content, then we should return no streams. - if (currentPeriod == null) { - return []; - } - - this.assertCorrectActiveStreams_(); - - // Don't show return streams that are still loading. - return currentPeriod.textStreams.filter((stream) => { - return !this.loadingTextStreams_.has(stream); - }); - } - - /** - * Get the period that is on the screen. This will return |null| if nothing - * is loaded. - * - * @return {shaka.extern.Period} - * @private - */ - getPresentationPeriod_() { - goog.asserts.assert(this.manifest_ && this.playhead_, - 'Only ask for the presentation period when loaded with media source.'); - - const presentationTime = this.playhead_.getTime(); - - let lastPeriod = null; - - // Periods are ordered by |startTime|. If we always keep the last period - // that started before our presentation time, it means we will have the - // best guess at which period we are presenting. - for (const period of this.manifest_.periods) { - if (period.startTime <= presentationTime) { - lastPeriod = period; - } - } - - goog.asserts.assert(lastPeriod, 'Should have found a period.'); - return lastPeriod; - } - - /** - * Get the variant that we are currently presenting to the user. If we are not - * showing anything, then we will return |null|. - * - * @return {?shaka.extern.Variant} - * @private - */ - getPresentationVariant_() { - /** @type {shaka.extern.Period} */ - const currentPeriod = this.getPresentationPeriod_(); - return this.activeStreams_.getVariant(currentPeriod); - } - - /** - * Get the text stream that we are either currently presenting to the user or - * will be presenting will captions are enabled. If we have no text to - * display, this will return |null|. - * - * @return {?shaka.extern.Stream} - * @private - */ - getPresentationText_() { - /** @type {shaka.extern.Period} */ - const currentPeriod = this.getPresentationPeriod_(); - - // Can't have a text stream when there is no period. - if (currentPeriod == null) { - return null; - } - - // This is a workaround for the demo page to be able to display the list of - // text tracks. If no text track is currently active, pick the one that's - // going to be streamed when captions are enabled and mark it as active. - if (!this.activeStreams_.getText(currentPeriod)) { - const textStreams = shaka.util.StreamUtils.filterStreamsByLanguageAndRole( - currentPeriod.textStreams, - this.currentTextLanguage_, - this.currentTextRole_); - - if (textStreams.length) { - this.activeStreams_.useText(currentPeriod, textStreams[0]); - } - } - - return this.activeStreams_.getText(currentPeriod); - } - /** * Assuming the player is playing content with media source, check if the * player has buffered enough content to make it to the end of the @@ -5122,24 +4590,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget { return bufferEnd >= this.video_.duration - fudge; } - /** - * Find the period in |this.manifest_| that contains |variant|. If no period - * contains |variant| this will return |null|. - * - * @param {shaka.extern.Variant} variant - * @return {?shaka.extern.Period} - * @private - */ - findPeriodWithVariant_(variant) { - for (const period of this.manifest_.periods) { - if (period.variants.includes(variant)) { - return period; - } - } - - return null; - } - /** * Create an error for when we purposely interrupt a load operation. * diff --git a/lib/util/error.js b/lib/util/error.js index 365c3e98ef..0fe4036787 100644 --- a/lib/util/error.js +++ b/lib/util/error.js @@ -491,12 +491,7 @@ shaka.util.Error.Code = { /** The DASH Manifest specifies conflicting key IDs. */ 'DASH_CONFLICTING_KEY_IDS': 4010, - /** - * The manifest contains a period with no playable streams. - * Either the period was originally empty, or the streams within cannot be - * played on this browser or platform. - */ - 'UNPLAYABLE_PERIOD': 4011, + // RETIRED: 'UNPLAYABLE_PERIOD': 4011, /** * There exist some streams that could be decoded, but restrictions imposed @@ -517,11 +512,7 @@ shaka.util.Error.Code = { // RETIRED: 'INTERNAL_ERROR_KEY_STATUS': 4013, - /** - * No valid periods were found in the manifest. Please check that your - * manifest is correct and free of typos. - */ - 'NO_PERIODS': 4014, + // RETIRED: 'NO_PERIODS': 4014, /** * HLS playlist doesn't start with a mandory #EXTM3U tag. @@ -644,22 +635,22 @@ shaka.util.Error.Code = { */ 'HLS_INTERNAL_SKIP_STREAM': 4035, + /** The Manifest contained no Variants. */ + 'NO_VARIANTS': 4036, + + // RETIRED: 'INCONSISTENT_BUFFER_STATE': 5000, // RETIRED: 'INVALID_SEGMENT_INDEX': 5001, // RETIRED: 'SEGMENT_DOES_NOT_EXIST': 5002, // RETIRED: 'CANNOT_SATISFY_BYTE_LIMIT': 5003, // RETIRED: 'BAD_SEGMENT': 5004, + // RETIRED: 'INVALID_STREAMS_CHOSEN': 5005, /** - * The StreamingEngine called onChooseStreams() but the callback receiver - * did not return the correct number or type of Streams. - * - * This can happen when there is multi-Period content where one Period is - * video+audio and another is video-only or audio-only. We don't support this - * case because it is incompatible with MSE. When the browser reaches the - * transition, it will pause, waiting for the audio stream. + * This would only happen if StreamingEngine were not started correctly, and + * should not be seen in production. */ - 'INVALID_STREAMS_CHOSEN': 5005, + 'STREAMING_ENGINE_STARTUP_INVALID_STATE': 5006, /** diff --git a/lib/util/manifest_filter.js b/lib/util/manifest_filter.js index e19b1f50ee..0a5211987d 100644 --- a/lib/util/manifest_filter.js +++ b/lib/util/manifest_filter.js @@ -5,8 +5,6 @@ goog.provide('shaka.util.ManifestFilter'); -goog.require('goog.asserts'); - /** * This utility class contains all the functions used to filter manifests @@ -22,12 +20,10 @@ shaka.util.ManifestFilter = class { * @param {{width: number, height:number}} maxHwResolution */ static filterByRestrictions(manifest, restrictions, maxHwResolution) { - for (const period of manifest.periods) { - period.variants = period.variants.filter((variant) => { - return shaka.util.StreamUtils.meetsRestrictions( - variant, restrictions, maxHwResolution); - }); - } + manifest.variants = manifest.variants.filter((variant) => { + return shaka.util.StreamUtils.meetsRestrictions( + variant, restrictions, maxHwResolution); + }); } @@ -40,20 +36,18 @@ shaka.util.ManifestFilter = class { static filterByMediaSourceSupport(manifest) { const MediaSourceEngine = shaka.media.MediaSourceEngine; - for (const period of manifest.periods) { - period.variants = period.variants.filter((variant) => { - let supported = true; - if (variant.audio) { - supported = - supported && MediaSourceEngine.isStreamSupported(variant.audio); - } - if (variant.video) { - supported = - supported && MediaSourceEngine.isStreamSupported(variant.video); - } - return supported; - }); - } + manifest.variants = manifest.variants.filter((variant) => { + let supported = true; + if (variant.audio) { + supported = + supported && MediaSourceEngine.isStreamSupported(variant.audio); + } + if (variant.video) { + supported = + supported && MediaSourceEngine.isStreamSupported(variant.video); + } + return supported; + }); } /** @@ -64,196 +58,8 @@ shaka.util.ManifestFilter = class { * @param {!shaka.media.DrmEngine} drmEngine */ static filterByDrmSupport(manifest, drmEngine) { - for (const period of manifest.periods) { - period.variants = period.variants.filter((variant) => { - return drmEngine.supportsVariant(variant); - }); - } - } - - /** - * Filter the variants in |manifest| to only include those that use codecs - * that will be supported in each variant. This ensures playback from the - * first period to the last period by "jumping between" compatible variants. - * - * @param {shaka.extern.Manifest} manifest - */ - static filterByCommonCodecs(manifest) { - goog.asserts.assert(manifest.periods.length > 0, - 'There should be at least be one period'); - - const ManifestFilter = shaka.util.ManifestFilter; - - // Create a set of summaries that occur in each period. - /** @type {!shaka.util.ManifestFilter.VariantCodecSummarySet} */ - const common = new shaka.util.ManifestFilter.VariantCodecSummarySet(); - - let first = true; - for (const period of manifest.periods) { - /** @type {!shaka.util.ManifestFilter.VariantCodecSummarySet} */ - const next = ManifestFilter.VariantCodecSummarySet.fromVariants( - period.variants); - - if (first) { - common.includeAll(next); - first = false; - } else { - common.onlyKeep(next); - } - } - - // Filter the variants in the period by whether they match a summary that - // occurs in every period. - for (const period of manifest.periods) { - period.variants = period.variants.filter((variant) => { - const summary = new ManifestFilter.VariantCodecSummary(variant); - return common.contains(summary); - }); - } - } - - /** - * Go through each period and apply the filter to the set of variants. - * |filter| will only be given the set of variants in the current period that - * are compatible with at least one variant in the previous period. - * - * @param {shaka.extern.Manifest} manifest - * @param {function(shaka.extern.Period):!Promise} filter - * @return {!Promise} - */ - static async rollingFilter(manifest, filter) { - // Store a reference to the variants so that the next period can easily - // reference them too. - /** @type {shaka.util.ManifestFilter.VariantCodecSummarySet} */ - let previous = null; - - for (const period of manifest.periods) { - // Remove all variants that don't have a compatible variant in the - // previous period. If we were to only use the first variant, we would - // risk a variant being removed from a later period that would break that - // path across all periods. - if (previous) { - period.variants = period.variants.filter((variant) => { - const summary = - new shaka.util.ManifestFilter.VariantCodecSummary(variant); - return previous.contains(summary); - }); - } - - // eslint-disable-next-line no-await-in-loop - await filter(period); - - // Use the results of filtering this period as the "previous" for the - // next period. - previous = shaka.util.ManifestFilter.VariantCodecSummarySet.fromVariants( - period.variants); - } - } -}; - - -/** - * The variant codec summary is a summary of the codec information for a given - * codec. This can be used to test the compatibility between variants by - * checking that their summaries contain the same information. - * - * @final - */ -shaka.util.ManifestFilter.VariantCodecSummary = class { - /** - * @param {shaka.extern.Variant} variant - */ - constructor(variant) { - // We summarize a variant based on the basic mime type and the basic - // codec because they must match for two variants to be compatible. For - // example, we can't adapt between WebM and MP4, nor can we adapt between - // mp4a.* to ec-3. - - const audio = variant.audio; - const video = variant.video; - - /** @private {?string} */ - this.audioMime_ = audio ? audio.mimeType : null; - /** @private {?string} */ - this.audioCodec_ = audio ? audio.codecs.split('.')[0] : null; - /** @private {?string} */ - this.videoMime_ = video ? video.mimeType : null; - /** @private {?string} */ - this.videoCodec_ = video ? video.codecs.split('.')[0] : null; - } - - /** - * Check if this summaries is equal to another. - * - * @param {!shaka.util.ManifestFilter.VariantCodecSummary} other - * @return {boolean} - */ - equals(other) { - return this.audioMime_ == other.audioMime_ && - this.audioCodec_ == other.audioCodec_ && - this.videoMime_ == other.videoMime_ && - this.videoCodec_ == other.videoCodec_; - } -}; - - -/** - * @final - */ -shaka.util.ManifestFilter.VariantCodecSummarySet = class { - constructor() { - /** @private {!Array.} */ - this.all_ = []; - } - - /** - * @param {!shaka.util.ManifestFilter.VariantCodecSummary} summary - */ - add(summary) { - if (!this.contains(summary)) { - this.all_.push(summary); - } - } - - /** - * Add all items from |other| to |this|. - * @param {!shaka.util.ManifestFilter.VariantCodecSummarySet} other - */ - includeAll(other) { - for (const item of other.all_) { - this.add(item); - } - } - - /** - * Remove all items from |this| that are not in |other|. - * @param {!shaka.util.ManifestFilter.VariantCodecSummarySet} other - */ - onlyKeep(other) { - this.all_ = this.all_.filter((x) => other.contains(x)); - } - - /** - * @param {!shaka.util.ManifestFilter.VariantCodecSummary} summary - * @return {boolean} - */ - contains(summary) { - return this.all_.some((x) => summary.equals(x)); - } - - /** - * Create a set of variant codec summaries for a list of variants. The set - * may have fewer elements than the list if there are variants with similar - * codecs. - * - * @param {!Array.} variants - * @return {!shaka.util.ManifestFilter.VariantCodecSummarySet} - */ - static fromVariants(variants) { - const set = new shaka.util.ManifestFilter.VariantCodecSummarySet(); - for (const variant of variants) { - set.add(new shaka.util.ManifestFilter.VariantCodecSummary(variant)); - } - return set; + manifest.variants = manifest.variants.filter((variant) => { + return drmEngine.supportsVariant(variant); + }); } }; diff --git a/lib/util/periods.js b/lib/util/periods.js index afa6e73add..9f0158b5df 100644 --- a/lib/util/periods.js +++ b/lib/util/periods.js @@ -5,7 +5,6 @@ goog.provide('shaka.util.Periods'); - /** * This is a collection of period-focused utility methods. * @@ -13,45 +12,44 @@ goog.provide('shaka.util.Periods'); */ shaka.util.Periods = class { /** - * Get all the variants across all periods. + * Stitch together variants across periods. * - * @param {!Iterable.} periods + * @param {!Array.>} variantsPerPeriod * @return {!Array.} */ - static getAllVariantsFrom(periods) { - const found = []; - - for (const period of periods) { - for (const variant of period.variants) { - found.push(variant); - } + static stitchVariants(variantsPerPeriod) { + if (variantsPerPeriod.length == 1) { + return variantsPerPeriod[0]; } - return found; + // FIXME(#1339): implement + return variantsPerPeriod[0]; } /** - * Find our best guess at which period contains the given time. If - * |timeInSeconds| starts before the first period, then |null| will be - * returned. + * Stitch together text streams across periods. * - * @param {!Iterable.} periods - * @param {number} timeInSeconds - * @return {?shaka.extern.Period} + * @param {!Array.>} textStreamsPerPeriod + * @return {!Array.} */ - static findPeriodForTime(periods, timeInSeconds) { - let bestGuess = null; - - // Go period-by-period and see if the period started before our current - // time. If so, we could be in that period. Since periods are supposed to be - // in order by start time, we can allow later periods to override our best - // guess. - for (const period of periods) { - if (timeInSeconds >= period.startTime) { - bestGuess = period; - } + static stitchTextStreams(textStreamsPerPeriod) { + if (textStreamsPerPeriod.length == 1) { + return textStreamsPerPeriod[0]; } - return bestGuess; + // FIXME(#1339): implement + return textStreamsPerPeriod[0]; + } + + /** + * Stitch together DB streams across periods, taking a mix of stream types. + * The offline database does not separate these by type. + * + * @param {!Array.>} streamDBsPerPeriod + * @return {!Array.} + */ + static stitchStreamDBs(streamDBsPerPeriod) { + // FIXME(#1339): implement + return streamDBsPerPeriod[0]; } }; diff --git a/lib/util/stream_utils.js b/lib/util/stream_utils.js index a82a63aa2c..c5e05653cf 100644 --- a/lib/util/stream_utils.js +++ b/lib/util/stream_utils.js @@ -31,18 +31,13 @@ shaka.util.StreamUtils = class { static chooseCodecsAndFilterManifest(manifest, preferredAudioChannelCount) { const MimeUtils = shaka.util.MimeUtils; - // Collect a list of variants for all periods. - /** @type {!Array.} */ - let variants = manifest.periods.reduce( - (variants, period) => variants.concat(period.variants), []); - // To start, consider a subset of variants based on audio channel // preferences. // For some content (#1013), surround-sound variants will use a different // codec than stereo variants, so it is important to choose codecs **after** // considering the audio channel config. - variants = shaka.util.StreamUtils.filterVariantsByAudioChannelCount( - variants, preferredAudioChannelCount); + const variants = shaka.util.StreamUtils.filterVariantsByAudioChannelCount( + manifest.variants, preferredAudioChannelCount); function variantCodecs(variant) { // Only consider the base of the codec string. For example, these should @@ -93,17 +88,15 @@ shaka.util.StreamUtils = class { // Filter out any variants that don't match, forcing AbrManager to choose // from the most efficient variants possible. - for (const period of manifest.periods) { - period.variants = period.variants.filter((variant) => { - const codecs = variantCodecs(variant); - if (codecs == bestCodecs) { - return true; - } + manifest.variants = manifest.variants.filter((variant) => { + const codecs = variantCodecs(variant); + if (codecs == bestCodecs) { + return true; + } - shaka.log.debug('Dropping Variant (better codec available)', variant); - return false; - }); - } + shaka.log.debug('Dropping Variant (better codec available)', variant); + return false; + }); } /** @@ -190,28 +183,17 @@ shaka.util.StreamUtils = class { /** - * Alters the given Period to filter out any unplayable streams. + * Alters the given Manifest to filter out any unplayable streams. * * @param {shaka.media.DrmEngine} drmEngine - * @param {?shaka.extern.Stream} activeAudio - * @param {?shaka.extern.Stream} activeVideo - * @param {shaka.extern.Period} period + * @param {?shaka.extern.Variant} currentVariant + * @param {shaka.extern.Manifest} manifest */ - static filterNewPeriod(drmEngine, activeAudio, activeVideo, period) { + static filterManifest(drmEngine, currentVariant, manifest) { const StreamUtils = shaka.util.StreamUtils; - if (activeAudio) { - goog.asserts.assert(StreamUtils.isAudio(activeAudio), - 'Audio streams must have the audio type.'); - } - - if (activeVideo) { - goog.asserts.assert(StreamUtils.isVideo(activeVideo), - 'Video streams must have the video type.'); - } - // Filter variants. - period.variants = period.variants.filter((variant) => { + manifest.variants = manifest.variants.filter((variant) => { if (drmEngine && drmEngine.initialized()) { if (!drmEngine.supportsVariant(variant)) { shaka.log.debug('Dropping variant - not compatible with key system', @@ -235,22 +217,22 @@ shaka.util.StreamUtils = class { return false; } - if (audio && activeAudio) { - if (!StreamUtils.areStreamsCompatible_(audio, activeAudio)) { + if (audio && currentVariant && currentVariant.audio) { + if (!StreamUtils.areStreamsCompatible_(audio, currentVariant.audio)) { shaka.log.debug('Droping variant - not compatible with active audio', 'active audio', - StreamUtils.getStreamSummaryString_(activeAudio), + StreamUtils.getStreamSummaryString_(currentVariant.audio), 'variant.audio', StreamUtils.getStreamSummaryString_(audio)); return false; } } - if (video && activeVideo) { - if (!StreamUtils.areStreamsCompatible_(video, activeVideo)) { + if (video && currentVariant && currentVariant.video) { + if (!StreamUtils.areStreamsCompatible_(video, currentVariant.video)) { shaka.log.debug('Droping variant - not compatible with active video', 'active video', - StreamUtils.getStreamSummaryString_(activeVideo), + StreamUtils.getStreamSummaryString_(currentVariant.video), 'variant.video', StreamUtils.getStreamSummaryString_(video)); return false; @@ -261,7 +243,7 @@ shaka.util.StreamUtils = class { }); // Filter text streams. - period.textStreams = period.textStreams.filter((stream) => { + manifest.textStreams = manifest.textStreams.filter((stream) => { const fullMimeType = shaka.util.MimeUtils.getFullType( stream.mimeType, stream.codecs); const keep = shaka.text.TextEngine.isTypeSupported(fullMimeType); @@ -728,38 +710,6 @@ shaka.util.StreamUtils = class { } - /** - * Finds a Variant with given audio and video streams. - * Returns null if no such Variant was found. - * - * @param {?shaka.extern.Stream} audio - * @param {?shaka.extern.Stream} video - * @param {!Array.} variants - * @return {?shaka.extern.Variant} - */ - static getVariantByStreams(audio, video, variants) { - if (audio) { - goog.asserts.assert( - shaka.util.StreamUtils.isAudio(audio), - 'Audio streams must have the audio type.'); - } - - if (video) { - goog.asserts.assert( - shaka.util.StreamUtils.isVideo(video), - 'Video streams must have the video type.'); - } - - for (const variant of variants) { - if (variant.audio == audio && variant.video == video) { - return variant; - } - } - - return null; - } - - /** * Checks if the given stream is an audio stream. * diff --git a/test/abr/simple_abr_manager_unit.js b/test/abr/simple_abr_manager_unit.js index ba7d5b02e5..38503327e9 100644 --- a/test/abr/simple_abr_manager_unit.js +++ b/test/abr/simple_abr_manager_unit.js @@ -25,46 +25,44 @@ describe('SimpleAbrManager', () => { // Keep unsorted. manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(100, (variant) => { - variant.bandwidth = 4e5; // 400 kbps - variant.addAudio(0); - variant.addVideo(1); - }); - period.addVariant(101, (variant) => { - variant.bandwidth = 1e6; // 1000 kbps - variant.addAudio(2); - variant.addVideo(3); - }); - period.addVariant(102, (variant) => { - variant.bandwidth = 5e5; // 500 kbps - variant.addAudio(4); - variant.addVideo(5); - }); - period.addVariant(103, (variant) => { - variant.bandwidth = 2e6; - variant.addAudio(6); - variant.addVideo(7); - }); - period.addVariant(104, (variant) => { - variant.bandwidth = 2e6; // Identical on purpose. - variant.addAudio(8); - variant.addVideo(9); - }); - period.addVariant(105, (variant) => { - variant.bandwidth = 6e5; - variant.addAudio(10); - variant.addVideo(11); - }); - period.addTextStream(20); - period.addTextStream(21); + manifest.addVariant(100, (variant) => { + variant.bandwidth = 4e5; // 400 kbps + variant.addAudio(0); + variant.addVideo(1); + }); + manifest.addVariant(101, (variant) => { + variant.bandwidth = 1e6; // 1000 kbps + variant.addAudio(2); + variant.addVideo(3); + }); + manifest.addVariant(102, (variant) => { + variant.bandwidth = 5e5; // 500 kbps + variant.addAudio(4); + variant.addVideo(5); + }); + manifest.addVariant(103, (variant) => { + variant.bandwidth = 2e6; + variant.addAudio(6); + variant.addVideo(7); + }); + manifest.addVariant(104, (variant) => { + variant.bandwidth = 2e6; // Identical on purpose. + variant.addAudio(8); + variant.addVideo(9); }); + manifest.addVariant(105, (variant) => { + variant.bandwidth = 6e5; + variant.addAudio(10); + variant.addVideo(11); + }); + manifest.addTextStream(20); + manifest.addTextStream(21); }); config = shaka.util.PlayerConfiguration.createDefault().abr; config.defaultBandwidthEstimate = defaultBandwidthEstimate; - variants = manifest.periods[0].variants; + variants = manifest.variants; abrManager = new shaka.abr.SimpleAbrManager(); abrManager.init(shaka.test.Util.spyFunc(switchCallback)); @@ -97,19 +95,17 @@ describe('SimpleAbrManager', () => { it('can choose from audio only variants', () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.bandwidth = 4e5; - variant.addAudio(0); - }); - period.addVariant(1, (variant) => { - variant.bandwidth = 1e6; - variant.addAudio(2); - }); + manifest.addVariant(0, (variant) => { + variant.bandwidth = 4e5; + variant.addAudio(0); + }); + manifest.addVariant(1, (variant) => { + variant.bandwidth = 1e6; + variant.addAudio(2); }); }); - abrManager.setVariants(manifest.periods[0].variants); + abrManager.setVariants(manifest.variants); const chosen = abrManager.chooseVariant(); expect(chosen).not.toBe(null); expect(chosen.audio).not.toBe(null); @@ -118,19 +114,17 @@ describe('SimpleAbrManager', () => { it('can choose from video only variants', () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.bandwidth = 4e5; - variant.addVideo(0); - }); - period.addVariant(1, (variant) => { - variant.bandwidth = 1e6; - variant.addVideo(2); - }); + manifest.addVariant(0, (variant) => { + variant.bandwidth = 4e5; + variant.addVideo(0); + }); + manifest.addVariant(1, (variant) => { + variant.bandwidth = 1e6; + variant.addVideo(2); }); }); - abrManager.setVariants(manifest.periods[0].variants); + abrManager.setVariants(manifest.variants); const chosen = abrManager.chooseVariant(); expect(chosen).not.toBe(null); expect(chosen.audio).toBe(null); @@ -319,23 +313,21 @@ describe('SimpleAbrManager', () => { it('will respect restrictions', () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(10, (variant) => { - variant.bandwidth = 1e5; - variant.addVideo(0, (stream) => { - stream.size(50, 50); - }); + manifest.addVariant(10, (variant) => { + variant.bandwidth = 1e5; + variant.addVideo(0, (stream) => { + stream.size(50, 50); }); - period.addVariant(11, (variant) => { - variant.bandwidth = 2e5; - variant.addVideo(1, (stream) => { - stream.size(200, 200); - }); + }); + manifest.addVariant(11, (variant) => { + variant.bandwidth = 2e5; + variant.addVideo(1, (stream) => { + stream.size(200, 200); }); }); }); - abrManager.setVariants(manifest.periods[0].variants); + abrManager.setVariants(manifest.variants); let chosen = abrManager.chooseVariant(); expect(chosen.id).toBe(11); @@ -348,23 +340,21 @@ describe('SimpleAbrManager', () => { it('uses lowest-bandwidth variant when restrictions cannot be met', () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(10, (variant) => { - variant.bandwidth = 1e5; - variant.addVideo(0, (stream) => { - stream.size(50, 50); - }); + manifest.addVariant(10, (variant) => { + variant.bandwidth = 1e5; + variant.addVideo(0, (stream) => { + stream.size(50, 50); }); - period.addVariant(11, (variant) => { - variant.bandwidth = 2e5; - variant.addVideo(1, (stream) => { - stream.size(200, 200); - }); + }); + manifest.addVariant(11, (variant) => { + variant.bandwidth = 2e5; + variant.addVideo(1, (stream) => { + stream.size(200, 200); }); }); }); - abrManager.setVariants(manifest.periods[0].variants); + abrManager.setVariants(manifest.variants); let chosen = abrManager.chooseVariant(); expect(chosen.id).toBe(11); diff --git a/test/dash/dash_parser_content_protection_unit.js b/test/dash/dash_parser_content_protection_unit.js index 0eb6ee3bf7..5022086706 100644 --- a/test/dash/dash_parser_content_protection_unit.js +++ b/test/dash/dash_parser_content_protection_unit.js @@ -30,16 +30,16 @@ describe('DashParser ContentProtection', () => { config.dash.ignoreDrmInfo = ignoreDrmInfo || false; dashParser.configure(config); - const playerEvents = { + const playerInterface = { networkingEngine: netEngine, - filterNewPeriod: () => {}, - filterAllPeriods: () => {}, + filter: (manifest) => {}, onTimelineRegionAdded: fail, // Should not have any EventStream elements. onEvent: fail, onError: fail, }; - const actual = await dashParser.start('http://example.com', playerEvents); + const actual = await dashParser.start( + 'http://example.com', playerInterface); expect(actual).toEqual(expected); } @@ -97,19 +97,15 @@ describe('DashParser ContentProtection', () => { const variant = jasmine.objectContaining({ drmInfos: drmInfos, video: jasmine.objectContaining({ - keyId: keyIds[i] || null, + keyIds: keyIds[i] ? [keyIds[i]] : [], }), }); variants.push(variant); } return jasmine.objectContaining({ - periods: [ - jasmine.objectContaining({ - variants: variants, - textStreams: [], - }), - ], // periods + variants: variants, + textStreams: [], }); } diff --git a/test/dash/dash_parser_live_unit.js b/test/dash/dash_parser_live_unit.js index a780bd14c7..7139b8dc64 100644 --- a/test/dash/dash_parser_live_unit.js +++ b/test/dash/dash_parser_live_unit.js @@ -25,8 +25,7 @@ describe('DashParser Live', () => { parser.configure(shaka.util.PlayerConfiguration.createDefault().manifest); playerInterface = { networkingEngine: fakeNetEngine, - filterNewPeriod: () => {}, - filterAllPeriods: () => {}, + filter: (manifest) => {}, onTimelineRegionAdded: fail, // Should not have any EventStream elements. onEvent: fail, onError: fail, @@ -148,18 +147,13 @@ describe('DashParser Live', () => { fakeNetEngine.setResponseText('dummy://foo', firstManifest); const manifest = await parser.start('dummy://foo', playerInterface); - const stream = manifest.periods[0].variants[0].video; + const stream = manifest.variants[0].video; await stream.createSegmentIndex(); ManifestParser.verifySegmentIndex(stream, firstReferences); - expect(manifest.periods.length).toBe(1); fakeNetEngine.setResponseText('dummy://foo', secondManifest); await updateManifest(); ManifestParser.verifySegmentIndex(stream, secondReferences); - // In https://github.com/google/shaka-player/issues/963, we - // duplicated periods during the first update. This check covers - // this case. - expect(manifest.periods.length).toBe(1); } it('basic support', async () => { @@ -195,7 +189,7 @@ describe('DashParser Live', () => { const manifest = await parser.start('dummy://foo', playerInterface); expect(manifest).toBeTruthy(); - const stream = manifest.periods[0].variants[0].video; + const stream = manifest.variants[0].video; expect(stream).toBeTruthy(); await stream.createSegmentIndex(); @@ -215,7 +209,8 @@ describe('DashParser Live', () => { ManifestParser.verifySegmentIndex(stream, basicRefs.slice(1)); }); - it('evicts old references for multi-period live stream', async () => { + // FIXME(#1339): re-enable this test! + xit('evicts old references for multi-period live stream', async () => { const template = [ ' { /** @const {!Array.} */ const period2Refs = cloneRefs(basicRefs); for (const ref of period2Refs) { + ref.timestampOffset = pStart; ref.startTime += pStart; ref.endTime += pStart; } + /** @const {!Array.} */ + const allRefs = period1Refs.concat(period2Refs); - const stream1 = manifest.periods[0].variants[0].video; - const stream2 = manifest.periods[1].variants[0].video; + const stream1 = manifest.variants[0].video; await stream1.createSegmentIndex(); - await stream2.createSegmentIndex(); - ManifestParser.verifySegmentIndex(stream1, period1Refs); - ManifestParser.verifySegmentIndex(stream2, period2Refs); + ManifestParser.verifySegmentIndex(stream1, allRefs); // The 60 second availability window is initially full in all cases // (SegmentTemplate+Timeline, etc.) The first segment is always 10 @@ -280,14 +275,12 @@ describe('DashParser Live', () => { Date.now = () => 11 * 1000; await updateManifest(); // The first reference should have been evicted. - ManifestParser.verifySegmentIndex(stream1, period1Refs.slice(1)); - ManifestParser.verifySegmentIndex(stream2, period2Refs); + ManifestParser.verifySegmentIndex(stream1, allRefs.slice(1)); // Same as above, but 1 period length later Date.now = () => (11 + pStart) * 1000; await updateManifest(); - ManifestParser.verifySegmentIndex(stream1, []); - ManifestParser.verifySegmentIndex(stream2, period2Refs.slice(1)); + ManifestParser.verifySegmentIndex(stream1, period2Refs.slice(1)); }); it('sets infinite duration for single-period live streams', async () => { @@ -313,7 +306,6 @@ describe('DashParser Live', () => { Date.now = () => 0; const manifest = await parser.start('dummy://foo', playerInterface); - expect(manifest.periods.length).toBe(1); const timeline = manifest.presentationTimeline; expect(timeline.getDuration()).toBe(Infinity); }); @@ -349,59 +341,76 @@ describe('DashParser Live', () => { Date.now = () => 0; const manifest = await parser.start('dummy://foo', playerInterface); - expect(manifest.periods.length).toBe(2); - expect(manifest.periods[1].startTime).toBe(60); const timeline = manifest.presentationTimeline; expect(timeline.getDuration()).toBe(Infinity); }); } - it('can add Periods', async () => { - const lines = [ - '', - ]; - const template = [ + // FIXME(#1339): re-enable this test! + xit('can add Periods', async () => { + const template1 = [ '', - ' ', + ' ', ' ', - ' ', + ' ', ' http://example.com', - '%(contents)s', + ' ', ' ', ' ', ' ', '', ].join('\n'); - const secondManifest = - sprintf(template, {updateTime: updateTime, contents: lines.join('\n')}); - const firstManifest = makeSimpleLiveManifestText(lines, updateTime); - - /** @type {!jasmine.Spy} */ - const filterNewPeriod = jasmine.createSpy('filterNewPeriod'); - playerInterface.filterNewPeriod = Util.spyFunc(filterNewPeriod); - - /** @type {!jasmine.Spy} */ - const filterAllPeriods = jasmine.createSpy('filterAllPeriods'); - playerInterface.filterAllPeriods = Util.spyFunc(filterAllPeriods); + const template2 = [ + '', + ' ', + ' ', + ' ', + ' http://example.com', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' http://example.com', + ' ', + ' ', + ' ', + ' ', + '', + ].join('\n'); + const firstManifest = sprintf(template1, {updateTime: updateTime}); + const secondManifest = sprintf(template2, {updateTime: updateTime}); fakeNetEngine.setResponseText('dummy://foo', firstManifest); + // First two segments should exist + Date.now = () => 5; + const manifest = await parser.start('dummy://foo', playerInterface); + const variant = manifest.variants[0]; + const stream = variant.video; + await stream.createSegmentIndex(); - expect(manifest.periods.length).toBe(1); - // Should call filterAllPeriods for parsing the first manifest - expect(filterNewPeriod).not.toHaveBeenCalled(); - expect(filterAllPeriods).toHaveBeenCalledTimes(1); + // First two segments exist, but not the third. + expect(stream.segmentIndex.find(3)).not.toBe(null); + expect(stream.segmentIndex.find(5)).toBe(null); fakeNetEngine.setResponseText('dummy://foo', secondManifest); + // First period (10s) is complete, plus first two segments of next period + Date.now = () => 15; + await updateManifest(); - // Should update the same manifest object. - expect(manifest.periods.length).toBe(2); - // Should call filterNewPeriod for parsing the new manifest - expect(filterAllPeriods).toHaveBeenCalledTimes(1); - expect(filterNewPeriod).toHaveBeenCalledTimes(1); + // The update should have affected the same variant object we captured + // before. Now the entire first period should exist (10s), plus the next + // two segments. + expect(stream.segmentIndex.find(9)).not.toBe(null); + expect(stream.segmentIndex.find(13)).not.toBe(null); }); it('uses redirect URL for manifest BaseURL and updates', async () => { @@ -445,7 +454,7 @@ describe('DashParser Live', () => { // Since the manifest request was redirected, the segment refers to // the redirected base. - const stream = manifest.periods[0].variants[0].video; + const stream = manifest.variants[0].video; await stream.createSegmentIndex(); const pos = stream.segmentIndex.find(0); const segmentUri = stream.segmentIndex.get(pos).getUris()[0]; @@ -1043,8 +1052,7 @@ describe('DashParser Live', () => { fakeNetEngine.setResponseText('dummy://foo', manifestText); const manifest = await parser.start('dummy://foo', playerInterface); - expect(manifest.periods.length).toBe(1); - const stream = manifest.periods[0].variants[0].video; + const stream = manifest.variants[0].video; await stream.createSegmentIndex(); const liveEdge = @@ -1271,7 +1279,7 @@ describe('DashParser Live', () => { // This should be seen as in-progress. expect(manifest.presentationTimeline.isInProgress()).toBe(true); - const stream = manifest.periods[0].variants[0].video; + const stream = manifest.variants[0].video; expect(stream).toBeTruthy(); await stream.createSegmentIndex(); diff --git a/test/dash/dash_parser_manifest_unit.js b/test/dash/dash_parser_manifest_unit.js index 061bea5cab..6d4a671a87 100644 --- a/test/dash/dash_parser_manifest_unit.js +++ b/test/dash/dash_parser_manifest_unit.js @@ -30,8 +30,7 @@ describe('DashParser Manifest', () => { onEventSpy = jasmine.createSpy('onEvent'); playerInterface = { networkingEngine: fakeNetEngine, - filterNewPeriod: () => {}, - filterAllPeriods: () => {}, + filter: (manifest) => {}, onTimelineRegionAdded: fail, // Should not have any EventStream elements. onEvent: shaka.test.Util.spyFunc(onEventSpy), onError: fail, @@ -131,55 +130,54 @@ describe('DashParser Manifest', () => { shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); manifest.minBufferTime = 75; - manifest.addPeriod(null, (period) => { - period.addPartialVariant((variant) => { - variant.language = 'en'; - variant.bandwidth = 200; - variant.primary = true; - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.bandwidth = 100; - stream.frameRate = 1000000 / 42000; - stream.size(768, 576); - stream.mime('video/mp4', 'avc1.4d401f'); - }); - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.bandwidth = 100; - stream.primary = true; - stream.roles = ['main']; - stream.mime('audio/mp4', 'mp4a.40.29'); - }); - }); - period.addPartialVariant((variant) => { - variant.language = 'en'; - variant.bandwidth = 150; - variant.primary = true; - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.bandwidth = 50; - stream.frameRate = 1000000 / 42000; - stream.size(576, 432); - stream.mime('video/mp4', 'avc1.4d401f'); - }); - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.bandwidth = 100; - stream.primary = true; - stream.roles = ['main']; - stream.mime('audio/mp4', 'mp4a.40.29'); - }); + manifest.addPartialVariant((variant) => { + variant.language = 'en'; + variant.bandwidth = 200; + variant.primary = true; + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.bandwidth = 100; + stream.frameRate = 1000000 / 42000; + stream.size(768, 576); + stream.mime('video/mp4', 'avc1.4d401f'); }); - period.addPartialTextStream((stream) => { - stream.language = 'es'; - stream.label = 'spanish'; + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.bandwidth = 100; stream.primary = true; - stream.mimeType = 'text/vtt'; + stream.roles = ['main']; + stream.mime('audio/mp4', 'mp4a.40.29'); + }); + }); + manifest.addPartialVariant((variant) => { + variant.language = 'en'; + variant.bandwidth = 150; + variant.primary = true; + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.bandwidth = 50; + stream.frameRate = 1000000 / 42000; + stream.size(576, 432); + stream.mime('video/mp4', 'avc1.4d401f'); + }); + variant.addPartialStream(ContentType.AUDIO, (stream) => { stream.bandwidth = 100; - stream.kind = 'caption'; - stream.roles = ['caption', 'main']; + stream.primary = true; + stream.roles = ['main']; + stream.mime('audio/mp4', 'mp4a.40.29'); }); }); + manifest.addPartialTextStream((stream) => { + stream.language = 'es'; + stream.label = 'spanish'; + stream.primary = true; + stream.mimeType = 'text/vtt'; + stream.bandwidth = 100; + stream.kind = 'caption'; + stream.roles = ['caption', 'main']; + }); })); }); - it('skips any periods after one without duration', async () => { + // TODO(#1339): Update this test not to rely on manifest.periods + xit('skips any periods after one without duration', async () => { const periodContents = [ ' ', ' ', @@ -218,7 +216,7 @@ describe('DashParser Manifest', () => { ' ', ].join('\n'); const template = [ - '', + '', ' ', '%(periodContents)s', ' ', @@ -235,10 +233,8 @@ describe('DashParser Manifest', () => { fakeNetEngine.setResponseText('dummy://foo', source); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - expect(manifest.periods.length).toBe(3); - expect(manifest.periods[0].startTime).toBe(10); - expect(manifest.periods[1].startTime).toBe(20); - expect(manifest.periods[2].startTime).toBe(30); + const timeline = manifest.presentationTimeline; + expect(timeline.getDuration()).toBe(40); }); it('defaults to SegmentBase with multiple Segment*', async () => { @@ -256,7 +252,7 @@ describe('DashParser Manifest', () => { fakeNetEngine.setResponseValue('http://example.com', mp4Index); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - const stream = manifest.periods[0].variants[0].video; + const stream = manifest.variants[0].video; await stream.createSegmentIndex(); const pos = stream.segmentIndex.find(0); const ref = stream.segmentIndex.get(pos); @@ -281,7 +277,7 @@ describe('DashParser Manifest', () => { fakeNetEngine.setResponseText('dummy://foo', source); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - const stream = manifest.periods[0].variants[0].video; + const stream = manifest.variants[0].video; await stream.createSegmentIndex(); const position = stream.segmentIndex.find(0); const ref = stream.segmentIndex.get(position); @@ -310,7 +306,7 @@ describe('DashParser Manifest', () => { /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - const stream = manifest.periods[0].textStreams[0]; + const stream = manifest.textStreams[0]; await stream.createSegmentIndex(); const pos = stream.segmentIndex.find(0); const ref = stream.segmentIndex.get(pos); @@ -361,10 +357,9 @@ describe('DashParser Manifest', () => { /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); // First Representation should be dropped. - const period = manifest.periods[0]; - const stream1 = period.variants[0].video; - const stream2 = period.variants[1].video; - const stream3 = period.variants[2].video; + const stream1 = manifest.variants[0].video; + const stream2 = manifest.variants[1].video; + const stream3 = manifest.variants[2].video; const expectedClosedCaptions = new Map( [['CC1', shaka.util.LanguageUtils.normalize('eng')], @@ -395,7 +390,7 @@ describe('DashParser Manifest', () => { /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - const stream = manifest.periods[0].variants[0].audio; + const stream = manifest.variants[0].audio; expect(stream.mimeType).toBe('audio/eac3-joc'); }); @@ -418,7 +413,7 @@ describe('DashParser Manifest', () => { /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - const stream = manifest.periods[0].variants[0].video; + const stream = manifest.variants[0].video; const expectedClosedCaptions = new Map( [['CC1', shaka.util.LanguageUtils.normalize('eng')], ['CC3', shaka.util.LanguageUtils.normalize('swe')]] @@ -445,7 +440,7 @@ describe('DashParser Manifest', () => { /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - const stream = manifest.periods[0].variants[0].video; + const stream = manifest.variants[0].video; const expectedClosedCaptions = new Map([['CC1', 'und']]); expect(stream.closedCaptions).toEqual(expectedClosedCaptions); }); @@ -469,8 +464,8 @@ describe('DashParser Manifest', () => { /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - const variant = manifest.periods[0].variants[0]; - const stream = manifest.periods[0].variants[0].audio; + const variant = manifest.variants[0]; + const stream = variant.audio; await stream.createSegmentIndex(); const position = stream.segmentIndex.find(0); const segment = stream.segmentIndex.get(position); @@ -675,9 +670,8 @@ describe('DashParser Manifest', () => { /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); // First Representation should be dropped. - const period = manifest.periods[0]; - expect(period.variants.length).toBe(1); - expect(period.variants[0].bandwidth).toBe(200); + expect(manifest.variants.length).toBe(1); + expect(manifest.variants[0].bandwidth).toBe(200); }); describe('allows missing Segment* elements for text', () => { @@ -701,7 +695,7 @@ describe('DashParser Manifest', () => { /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - expect(manifest.periods[0].textStreams.length).toBe(1); + expect(manifest.textStreams.length).toBe(1); }); it('specified via AdaptationSet@mimeType', async () => { @@ -724,7 +718,7 @@ describe('DashParser Manifest', () => { /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - expect(manifest.periods[0].textStreams.length).toBe(1); + expect(manifest.textStreams.length).toBe(1); }); it('specified via Representation@mimeType', async () => { @@ -747,7 +741,7 @@ describe('DashParser Manifest', () => { /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - expect(manifest.periods[0].textStreams.length).toBe(1); + expect(manifest.textStreams.length).toBe(1); }); }); @@ -901,11 +895,10 @@ describe('DashParser Manifest', () => { fakeNetEngine.setResponseText('dummy://foo', manifestText); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - expect(manifest.periods.length).toBe(1); - expect(manifest.periods[0].variants.length).toBe(1); - expect(manifest.periods[0].textStreams.length).toBe(0); + expect(manifest.variants.length).toBe(1); + expect(manifest.textStreams.length).toBe(0); - const variant = manifest.periods[0].variants[0]; + const variant = manifest.variants[0]; const trickModeVideo = variant && variant.video && variant.video.trickModeVideo; expect(trickModeVideo).toEqual(jasmine.objectContaining({ @@ -936,14 +929,13 @@ describe('DashParser Manifest', () => { fakeNetEngine.setResponseText('dummy://foo', manifestText); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - expect(manifest.periods.length).toBe(1); // The bogus EssentialProperty did not result in a variant. - expect(manifest.periods[0].variants.length).toBe(1); - expect(manifest.periods[0].textStreams.length).toBe(0); + expect(manifest.variants.length).toBe(1); + expect(manifest.textStreams.length).toBe(0); // The bogus EssentialProperty did not result in a trick mode track. - const variant = manifest.periods[0].variants[0]; + const variant = manifest.variants[0]; const trickModeVideo = variant && variant.video && variant.video.trickModeVideo; expect(trickModeVideo).toBe(null); @@ -977,12 +969,11 @@ describe('DashParser Manifest', () => { fakeNetEngine.setResponseText('dummy://foo', manifestText); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - expect(manifest.periods.length).toBe(1); - expect(manifest.periods[0].textStreams.length).toBe(2); + expect(manifest.textStreams.length).toBe(2); // At one time, these came out as 'application' rather than 'text'. const ContentType = shaka.util.ManifestParserUtils.ContentType; - expect(manifest.periods[0].textStreams[0].type).toBe(ContentType.TEXT); - expect(manifest.periods[0].textStreams[1].type).toBe(ContentType.TEXT); + expect(manifest.textStreams[0].type).toBe(ContentType.TEXT); + expect(manifest.textStreams[1].type).toBe(ContentType.TEXT); }); it('handles text with mime and codecs on different levels', async () => { @@ -1007,13 +998,12 @@ describe('DashParser Manifest', () => { fakeNetEngine.setResponseText('dummy://foo', manifestText); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - expect(manifest.periods.length).toBe(1); // In #875, this was an empty list. - expect(manifest.periods[0].textStreams.length).toBe(1); - if (manifest.periods[0].textStreams.length) { + expect(manifest.textStreams.length).toBe(1); + if (manifest.textStreams.length) { const ContentType = shaka.util.ManifestParserUtils.ContentType; - expect(manifest.periods[0].textStreams[0].type).toBe(ContentType.TEXT); + expect(manifest.textStreams[0].type).toBe(ContentType.TEXT); } }); @@ -1051,11 +1041,10 @@ describe('DashParser Manifest', () => { fakeNetEngine.setResponseText('dummy://foo', source); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - expect(manifest.periods.length).toBe(1); - expect(manifest.periods[0].variants.length).toBe(2); + expect(manifest.variants.length).toBe(2); - const variant1 = manifest.periods[0].variants[0]; - const variant2 = manifest.periods[0].variants[1]; + const variant1 = manifest.variants[0]; + const variant2 = manifest.variants[1]; await variant1.video.createSegmentIndex(); await variant2.video.createSegmentIndex(); @@ -1092,14 +1081,13 @@ describe('DashParser Manifest', () => { fakeNetEngine.setResponseText('dummy://foo', source); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - expect(manifest.periods.length).toBe(1); - expect(manifest.periods[0].variants.length).toBe(2); + expect(manifest.variants.length).toBe(2); - const variant1 = manifest.periods[0].variants[0]; + const variant1 = manifest.variants[0]; expect(isNaN(variant1.bandwidth)).toBe(false); expect(variant1.bandwidth).toBeGreaterThan(0); - const variant2 = manifest.periods[0].variants[1]; + const variant2 = manifest.variants[1]; expect(isNaN(variant2.bandwidth)).toBe(false); expect(variant2.bandwidth).toBeGreaterThan(0); }); @@ -1145,10 +1133,9 @@ describe('DashParser Manifest', () => { fakeNetEngine.setResponseText('dummy://foo', source); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - expect(manifest.periods.length).toBe(1); - expect(manifest.periods[0].variants.length).toBe(1); + expect(manifest.variants.length).toBe(1); - const variant = manifest.periods[0].variants[0]; + const variant = manifest.variants[0]; expect(variant.audio.channelsCount).toBe(expectedNumChannels); } @@ -1253,8 +1240,8 @@ describe('DashParser Manifest', () => { fakeNetEngine.setResponseText('dummy://foo', manifestText); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - const variant = manifest.periods[0].variants[0]; - const textStream = manifest.periods[0].textStreams[0]; + const variant = manifest.variants[0]; + const textStream = manifest.textStreams[0]; expect(variant.audio.originalId).toBe('audio-en'); expect(variant.video.originalId).toBe('video-sd'); expect(textStream.originalId).toBe('text-en'); @@ -1287,7 +1274,7 @@ describe('DashParser Manifest', () => { /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - const variant = manifest.periods[0].variants[0]; + const variant = manifest.variants[0]; expect(variant.audio).toBe(null); expect(variant.video).toBeTruthy(); }); @@ -1319,7 +1306,7 @@ describe('DashParser Manifest', () => { /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - const variant = manifest.periods[0].variants[0]; + const variant = manifest.variants[0]; expect(variant.audio).toBeTruthy(); expect(variant.video).toBe(null); }); @@ -1356,7 +1343,7 @@ describe('DashParser Manifest', () => { /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - const stream = manifest.periods[0].textStreams[0]; + const stream = manifest.textStreams[0]; expect(stream).toBeUndefined(); }); @@ -1549,7 +1536,7 @@ describe('DashParser Manifest', () => { fakeNetEngine.setResponseText('dummy://foo', manifestText); /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - const textStream = manifest.periods[0].textStreams[0]; + const textStream = manifest.textStreams[0]; expect(textStream.roles).toEqual(['captions', 'foo']); expect(textStream.kind).toBe('caption'); }); @@ -1580,7 +1567,7 @@ describe('DashParser Manifest', () => { /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - const variant = manifest.periods[0].variants[0]; + const variant = manifest.variants[0]; expect(variant.audio).toBeTruthy(); expect(variant.video).toBeTruthy(); }); diff --git a/test/dash/dash_parser_segment_base_unit.js b/test/dash/dash_parser_segment_base_unit.js index dd135a5fe1..34d27cd9bd 100644 --- a/test/dash/dash_parser_segment_base_unit.js +++ b/test/dash/dash_parser_segment_base_unit.js @@ -27,8 +27,7 @@ describe('DashParser SegmentBase', () => { playerInterface = { networkingEngine: fakeNetEngine, - filterNewPeriod: () => {}, - filterAllPeriods: () => {}, + filter: (manifest) => {}, onTimelineRegionAdded: fail, // Should not have any EventStream elements. onEvent: fail, onError: fail, @@ -70,9 +69,9 @@ describe('DashParser SegmentBase', () => { // Call createSegmentIndex() on each stream to make the requests, but expect // failure from the actual parsing, since the data is bogus. - const stream1 = manifest.periods[0].variants[0].video; + const stream1 = manifest.variants[0].video; await expectAsync(stream1.createSegmentIndex()).toBeRejected(); - const stream2 = manifest.periods[0].variants[1].video; + const stream2 = manifest.variants[1].video; await expectAsync(stream2.createSegmentIndex()).toBeRejected(); expect(fakeNetEngine.request).toHaveBeenCalledTimes(5); @@ -286,7 +285,7 @@ describe('DashParser SegmentBase', () => { /** @type {shaka.extern.Manifest} */ const manifest = await parser.start('dummy://foo', playerInterface); - const video = manifest.periods[0].variants[0].video; + const video = manifest.variants[0].video; await video.createSegmentIndex(); // real data, should succeed const pos = video.segmentIndex.find(0); diff --git a/test/dash/dash_parser_segment_template_unit.js b/test/dash/dash_parser_segment_template_unit.js index 01855e51ed..3a34eee17e 100644 --- a/test/dash/dash_parser_segment_template_unit.js +++ b/test/dash/dash_parser_segment_template_unit.js @@ -36,8 +36,7 @@ describe('DashParser SegmentTemplate', () => { playerInterface = { networkingEngine: fakeNetEngine, - filterNewPeriod: () => {}, - filterAllPeriods: () => {}, + filter: (manifest) => {}, onTimelineRegionAdded: fail, // Should not have any EventStream elements. onEvent: fail, onError: fail, @@ -86,10 +85,9 @@ describe('DashParser SegmentTemplate', () => { fakeNetEngine.setResponseText('dummy://foo', source); const manifest = await parser.start('dummy://foo', playerInterface); - expect(manifest.periods.length).toBe(1); - expect(manifest.periods[0].variants.length).toBe(1); + expect(manifest.variants.length).toBe(1); - const stream = manifest.periods[0].variants[0].video; + const stream = manifest.variants[0].video; expect(stream).toBeTruthy(); await stream.createSegmentIndex(); @@ -376,7 +374,7 @@ describe('DashParser SegmentTemplate', () => { const actual = await parser.start('dummy://foo', playerInterface); expect(actual).toBeTruthy(); - const variants = actual.periods[0].variants; + const variants = actual.variants; expect(variants.length).toBe(3); await variants[0].video.createSegmentIndex(); @@ -427,7 +425,7 @@ describe('DashParser SegmentTemplate', () => { const actual = await parser.start('dummy://foo', playerInterface); expect(actual).toBeTruthy(); - const variants = actual.periods[0].variants; + const variants = actual.variants; expect(variants.length).toBe(3); await variants[0].video.createSegmentIndex(); await variants[1].video.createSegmentIndex(); diff --git a/test/hls/hls_live_unit.js b/test/hls/hls_live_unit.js index 9ac708ccfb..7f68a8bd50 100644 --- a/test/hls/hls_live_unit.js +++ b/test/hls/hls_live_unit.js @@ -103,8 +103,7 @@ describe('HlsParser live', () => { config = shaka.util.PlayerConfiguration.createDefault().manifest; playerInterface = { - filterNewPeriod: () => {}, - filterAllPeriods: () => {}, + filter: () => {}, networkingEngine: fakeNetEngine, onError: fail, onEvent: fail, @@ -151,9 +150,7 @@ describe('HlsParser live', () => { const manifest = await parser.start('test:/master', playerInterface); - /** @type {!Array.} */ - const variants = manifest.periods[0].variants; - await Promise.all(variants.map(async (variant) => { + await Promise.all(manifest.variants.map(async (variant) => { await variant.video.createSegmentIndex(); ManifestParser.verifySegmentIndex(variant.video, initialReferences); if (variant.audio) { @@ -170,7 +167,7 @@ describe('HlsParser live', () => { .setResponseText('test:/audio', updatedMedia); await delayForUpdatePeriod(); - for (const variant of variants) { + for (const variant of manifest.variants) { ManifestParser.verifySegmentIndex(variant.video, updatedReferences); if (variant.audio) { ManifestParser.verifySegmentIndex(variant.audio, updatedReferences); @@ -282,7 +279,7 @@ describe('HlsParser live', () => { const manifest = await parser.start('test:/master', playerInterface); - const video = manifest.periods[0].variants[0].video; + const video = manifest.variants[0].video; await video.createSegmentIndex(); ManifestParser.verifySegmentIndex(video, [ref1]); @@ -532,13 +529,13 @@ describe('HlsParser live', () => { .setResponseText('test:/main.vtt', vtt); const manifest = await parser.start('test:/master', playerInterface); - const textStream = manifest.periods[0].textStreams[0]; + const textStream = manifest.textStreams[0]; await textStream.createSegmentIndex(); let ref = Array.from(textStream.segmentIndex)[0]; expect(ref).not.toBe(null); expect(ref.startTime).not.toBeLessThan(rolloverOffset); - const videoStream = manifest.periods[0].variants[0].video; + const videoStream = manifest.variants[0].video; await videoStream.createSegmentIndex(); ref = Array.from(videoStream.segmentIndex)[0]; expect(ref).not.toBe(null); @@ -602,7 +599,7 @@ describe('HlsParser live', () => { expectedRef.timestampOffset = 0; const manifest = await parser.start('test:/master', playerInterface); - const video = manifest.periods[0].variants[0].video; + const video = manifest.variants[0].video; await video.createSegmentIndex(); ManifestParser.verifySegmentIndex(video, [expectedRef]); }); @@ -622,7 +619,7 @@ describe('HlsParser live', () => { segmentDataStartTime + 4); const manifest = await parser.start('test:/master', playerInterface); - const video = manifest.periods[0].variants[0].video; + const video = manifest.variants[0].video; await video.createSegmentIndex(); ManifestParser.verifySegmentIndex(video, [ref1, ref2]); @@ -661,7 +658,7 @@ describe('HlsParser live', () => { expectedRef.timestampOffset = 0; const manifest = await parser.start('test:/master', playerInterface); - const video = manifest.periods[0].variants[0].video; + const video = manifest.variants[0].video; await video.createSegmentIndex(); ManifestParser.verifySegmentIndex(video, [expectedRef]); }); @@ -685,7 +682,7 @@ describe('HlsParser live', () => { expectedEndByte); // Complete segment reference const manifest = await parser.start('test:/master', playerInterface); - const video = manifest.periods[0].variants[0].video; + const video = manifest.variants[0].video; await video.createSegmentIndex(); ManifestParser.verifySegmentIndex(video, [expectedRef]); diff --git a/test/hls/hls_parser_unit.js b/test/hls/hls_parser_unit.js index 495a6476dc..9249f3ddf8 100644 --- a/test/hls/hls_parser_unit.js +++ b/test/hls/hls_parser_unit.js @@ -77,8 +77,7 @@ describe('HlsParser', () => { config = shaka.util.PlayerConfiguration.createDefault().manifest; playerInterface = { - filterNewPeriod: () => {}, - filterAllPeriods: () => {}, + filter: () => {}, networkingEngine: fakeNetEngine, onError: fail, onEvent: fail, @@ -148,32 +147,30 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.language = 'en'; - variant.bandwidth = 200; - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.frameRate = 60; - stream.mime('video/mp4', 'avc1'); - stream.size(960, 540); - }); - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.language = 'en'; - stream.channelsCount = 2; - stream.mime('audio/mp4', 'mp4a'); - }); + manifest.addPartialVariant((variant) => { + variant.language = 'en'; + variant.bandwidth = 200; + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.frameRate = 60; + stream.mime('video/mp4', 'avc1'); + stream.size(960, 540); }); - period.addPartialTextStream((stream) => { + variant.addPartialStream(ContentType.AUDIO, (stream) => { stream.language = 'en'; - stream.kind = TextStreamKind.SUBTITLE; - stream.mime('text/vtt', ''); - }); - period.addPartialTextStream((stream) => { - stream.language = 'es'; - stream.kind = TextStreamKind.SUBTITLE; - stream.mime('text/vtt', ''); + stream.channelsCount = 2; + stream.mime('audio/mp4', 'mp4a'); }); }); + manifest.addPartialTextStream((stream) => { + stream.language = 'en'; + stream.kind = TextStreamKind.SUBTITLE; + stream.mime('text/vtt', ''); + }); + manifest.addPartialTextStream((stream) => { + stream.language = 'es'; + stream.kind = TextStreamKind.SUBTITLE; + stream.mime('text/vtt', ''); + }); }); fakeNetEngine @@ -209,11 +206,9 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.mime('video/mp4', 'avc1.4d001e'); - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.mime('video/mp4', 'avc1.4d001e'); }); }); }); @@ -240,11 +235,9 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.mime('video/mp4', 'avc1'); - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.mime('video/mp4', 'avc1'); }); }); }); @@ -270,11 +263,9 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.mime('video/mp4', 'avc1'); - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.mime('video/mp4', 'avc1'); }); }); }); @@ -300,11 +291,9 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.mime('audio/mp4', 'mp4a'); - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.mime('audio/mp4', 'mp4a'); }); }); }); @@ -333,14 +322,12 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.mime('video/mp4', 'avc1'); - }); - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.mime('audio/mp4', 'mp4a'); - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.mime('video/mp4', 'avc1'); + }); + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.mime('audio/mp4', 'mp4a'); }); }); }); @@ -369,14 +356,12 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.mime('video/mp4', 'avc1'); - }); - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.mime('audio/mp4', ''); - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.mime('video/mp4', 'avc1'); + }); + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.mime('audio/mp4', ''); }); }); }); @@ -408,15 +393,13 @@ describe('HlsParser', () => { const closedCaptions = new Map([['CC1', 'en']]); const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.closedCaptions = closedCaptions; - stream.mime('video/mp4', 'avc1'); - }); - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.mime('audio/mp4', 'mp4a'); - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.closedCaptions = closedCaptions; + stream.mime('video/mp4', 'avc1'); + }); + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.mime('audio/mp4', 'mp4a'); }); }); }); @@ -447,14 +430,12 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.mime('video/mp4', 'avc1'); - }); - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.mime('audio/mp4', 'mp4a'); - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.mime('video/mp4', 'avc1'); + }); + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.mime('audio/mp4', 'mp4a'); }); }); }); @@ -482,11 +463,9 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.mime('audio/mp4', 'mp4a'); - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.mime('audio/mp4', 'mp4a'); }); }); }); @@ -534,7 +513,7 @@ describe('HlsParser', () => { const manifest = await parser.start('test:/master', playerInterface); const presentationTimeline = manifest.presentationTimeline; - const stream = manifest.periods[0].variants[0].video; + const stream = manifest.variants[0].video; await stream.createSegmentIndex(); const pos = stream.segmentIndex.find(0); @@ -567,11 +546,9 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.mime('video/mp4', 'avc1,mp4a'); - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.mime('video/mp4', 'avc1,mp4a'); }); }); }); @@ -598,11 +575,9 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.mime('video/mp4', /** @type {?} */ (jasmine.any(String))); - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.mime('video/mp4', /** @type {?} */ (jasmine.any(String))); }); }); }); @@ -631,14 +606,12 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.mime('video/mp4', /** @type {?} */ (jasmine.any(String))); - }); - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.mime('audio/mp4', /** @type {?} */ (jasmine.any(String))); - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.mime('video/mp4', /** @type {?} */ (jasmine.any(String))); + }); + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.mime('audio/mp4', /** @type {?} */ (jasmine.any(String))); }); }); }); @@ -666,11 +639,9 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.mime('video/mp4', /** @type {?} */ (jasmine.any(String))); - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.mime('video/mp4', /** @type {?} */ (jasmine.any(String))); }); }); }); @@ -698,11 +669,9 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.mime('audio/mp4', /** @type {?} */ (jasmine.any(String))); - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.mime('audio/mp4', /** @type {?} */ (jasmine.any(String))); }); }); }); @@ -736,24 +705,22 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.bandwidth = 200; - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.size(960, 540); - }); - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.language = 'en'; - }); + manifest.addPartialVariant((variant) => { + variant.bandwidth = 200; + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.size(960, 540); }); - period.addPartialVariant((variant) => { - variant.bandwidth = 300; - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.size(960, 540); - }); - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.language = 'fr'; - }); + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.language = 'en'; + }); + }); + manifest.addPartialVariant((variant) => { + variant.bandwidth = 300; + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.size(960, 540); + }); + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.language = 'fr'; }); }); }); @@ -784,20 +751,18 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.language = 'en'; - variant.addPartialStream(ContentType.VIDEO); - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.language = 'en'; - }); + manifest.addPartialVariant((variant) => { + variant.language = 'en'; + variant.addPartialStream(ContentType.VIDEO); + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.language = 'en'; }); - period.addPartialVariant((variant) => { - variant.language = 'fr'; - variant.addPartialStream(ContentType.VIDEO); - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.language = 'fr'; - }); + }); + manifest.addPartialVariant((variant) => { + variant.language = 'fr'; + variant.addPartialStream(ContentType.VIDEO); + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.language = 'fr'; }); }); }); @@ -805,7 +770,7 @@ describe('HlsParser', () => { await testHlsParser(master, media, manifest); }); - it('should call filterAllPeriods for parsing', async () => { + it('should call filter during parsing', async () => { const master = [ '#EXTM3U\n', '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",', @@ -830,11 +795,11 @@ describe('HlsParser', () => { .setResponseValue('test:/main.mp4', segmentData); /** @type {!jasmine.Spy} */ - const filterAllPeriods = jasmine.createSpy('filterAllPeriods'); - playerInterface.filterAllPeriods = Util.spyFunc(filterAllPeriods); + const filter = jasmine.createSpy('filter'); + playerInterface.filter = Util.spyFunc(filter); await parser.start('test:/master', playerInterface); - expect(filterAllPeriods).toHaveBeenCalledTimes(1); + expect(filter).toHaveBeenCalledTimes(1); }); it('fetch the start time for one audio/video stream and reuse for the others', @@ -908,11 +873,9 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.mime('video/mp4', 'avc1'); - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.mime('video/mp4', 'avc1'); }); }); }); @@ -962,26 +925,24 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.mime('video/mp4', 'avc1'); - }); - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.mime('audio/mp4', 'mp4a'); - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.mime('video/mp4', 'avc1'); }); - period.addPartialTextStream((stream) => { - stream.language = 'en'; - stream.kind = TextStreamKind.SUBTITLE; - stream.mime('text/vtt', ''); - }); - period.addPartialTextStream((stream) => { - stream.language = 'es'; - stream.kind = TextStreamKind.SUBTITLE; - stream.mime('text/vtt', ''); + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.mime('audio/mp4', 'mp4a'); }); }); + manifest.addPartialTextStream((stream) => { + stream.language = 'en'; + stream.kind = TextStreamKind.SUBTITLE; + stream.mime('text/vtt', ''); + }); + manifest.addPartialTextStream((stream) => { + stream.language = 'es'; + stream.kind = TextStreamKind.SUBTITLE; + stream.mime('text/vtt', ''); + }); }); fakeNetEngine @@ -1032,19 +993,17 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO); - variant.addPartialStream(ContentType.AUDIO); - }); - period.addPartialTextStream((stream) => { - stream.kind = TextStreamKind.SUBTITLE; - stream.mime('text/vtt', ''); - }); - period.addPartialTextStream((stream) => { - stream.kind = TextStreamKind.SUBTITLE; - stream.mime('text/vtt', ''); - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO); + variant.addPartialStream(ContentType.AUDIO); + }); + manifest.addPartialTextStream((stream) => { + stream.kind = TextStreamKind.SUBTITLE; + stream.mime('text/vtt', ''); + }); + manifest.addPartialTextStream((stream) => { + stream.kind = TextStreamKind.SUBTITLE; + stream.mime('text/vtt', ''); }); }); @@ -1119,11 +1078,10 @@ describe('HlsParser', () => { const timeline = actual.presentationTimeline; expect(timeline.getDuration()).toBe(10); - const period = actual.periods[0]; - expect(period.textStreams.length).toBe(1); - expect(period.variants.length).toBe(1); - expect(period.variants[0].audio).toBeTruthy(); - expect(period.variants[0].video).toBeTruthy(); + expect(actual.textStreams.length).toBe(1); + expect(actual.variants.length).toBe(1); + expect(actual.variants[0].audio).toBeTruthy(); + expect(actual.variants[0].video).toBeTruthy(); }); it('Disable audio does not create audio streams', async () => { @@ -1182,7 +1140,7 @@ describe('HlsParser', () => { parser.configure(config); const actual = await parser.start('test:/master', playerInterface); - const variant = actual.periods[0].variants[0]; + const variant = actual.variants[0]; expect(variant.audio).toBe(null); expect(variant.video).toBeTruthy(); }); @@ -1243,7 +1201,7 @@ describe('HlsParser', () => { parser.configure(config); const actual = await parser.start('test:/master', playerInterface); - const variant = actual.periods[0].variants[0]; + const variant = actual.variants[0]; expect(variant.audio).toBeTruthy(); expect(variant.video).toBe(null); }); @@ -1304,7 +1262,7 @@ describe('HlsParser', () => { parser.configure(config); const actual = await parser.start('test:/master', playerInterface); - const stream = actual.periods[0].textStreams[0]; + const stream = actual.textStreams[0]; expect(stream).toBeUndefined(); }); @@ -1329,14 +1287,12 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO); - }); - period.addPartialTextStream((stream) => { - stream.language = 'en'; - stream.mime('application/mp4', 'stpp.ttml.im1t'); - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO); + }); + manifest.addPartialTextStream((stream) => { + stream.language = 'en'; + stream.mime('application/mp4', 'stpp.ttml.im1t'); }); }); @@ -1381,13 +1337,11 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO); - }); - period.addPartialTextStream((stream) => { - stream.mime('text/vtt', 'vtt'); - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO); + }); + manifest.addPartialTextStream((stream) => { + stream.mime('text/vtt', 'vtt'); }); }); @@ -1425,13 +1379,11 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(/** @type {?} */ (jasmine.any(Number)), (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO); - }); - period.addPartialTextStream((stream) => { - stream.kind = TextStreamKind.SUBTITLE; - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO); + }); + manifest.addPartialTextStream((stream) => { + stream.kind = TextStreamKind.SUBTITLE; }); }); @@ -1467,10 +1419,8 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO); - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO); }); }); @@ -1506,15 +1456,13 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.frameRate = 60; - stream.mime('video/mp4', 'avc1'); - stream.size(960, 540); - }); - variant.addPartialStream(ContentType.AUDIO); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.frameRate = 60; + stream.mime('video/mp4', 'avc1'); + stream.size(960, 540); }); + variant.addPartialStream(ContentType.AUDIO); }); }); @@ -1549,8 +1497,8 @@ describe('HlsParser', () => { const actual = await parser.start('test:/host/master.m3u8', playerInterface); - const video = actual.periods[0].variants[0].video; - const audio = actual.periods[0].variants[0].audio; + const video = actual.variants[0].video; + const audio = actual.variants[0].audio; await video.createSegmentIndex(); await audio.createSegmentIndex(); @@ -1596,14 +1544,12 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.mime('video/mp4', 'avc1'); - }); - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.mime('audio/mp4', 'mp4a'); - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.mime('video/mp4', 'avc1'); + }); + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.mime('audio/mp4', 'mp4a'); }); }); }); @@ -1642,7 +1588,7 @@ describe('HlsParser', () => { .setResponseValue('test:/main2.mp4', segmentData); const actualManifest = await parser.start('test:/master', playerInterface); - const actualVideo = actualManifest.periods[0].variants[0].video; + const actualVideo = actualManifest.variants[0].video; await actualVideo.createSegmentIndex(); // Verify that the stream contains two segment references, each of the @@ -1691,15 +1637,13 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.bandwidth = 200; - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.size(960, 540); - }); - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.language = 'en'; - }); + manifest.addPartialVariant((variant) => { + variant.bandwidth = 200; + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.size(960, 540); + }); + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.language = 'en'; }); }); }); @@ -1748,14 +1692,12 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addDrmInfo('com.widevine.alpha', (drmInfo) => { - drmInfo.addCencInitData(initDataBase64); - }); - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.encrypted = true; - }); + manifest.addPartialVariant((variant) => { + variant.addDrmInfo('com.widevine.alpha', (drmInfo) => { + drmInfo.addCencInitData(initDataBase64); + }); + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.encrypted = true; }); }); }); @@ -2064,7 +2006,7 @@ describe('HlsParser', () => { expectedRef.timestampOffset = -segmentDataStartTime; const manifest = await parser.start('test:/master', playerInterface); - const video = manifest.periods[0].variants[0].video; + const video = manifest.variants[0].video; await video.createSegmentIndex(); ManifestParser.verifySegmentIndex(video, [expectedRef]); @@ -2096,7 +2038,7 @@ describe('HlsParser', () => { expectedRef.timestampOffset = -segmentDataStartTime; const manifest = await parser.start('test:/master', playerInterface); - const video = manifest.periods[0].variants[0].video; + const video = manifest.variants[0].video; await video.createSegmentIndex(); ManifestParser.verifySegmentIndex(video, [expectedRef]); @@ -2158,7 +2100,7 @@ describe('HlsParser', () => { const manifest = await parser.start('test:/master', playerInterface); const presentationTimeline = manifest.presentationTimeline; - const video = manifest.periods[0].variants[0].video; + const video = manifest.variants[0].video; await video.createSegmentIndex(); const refs = Array.from(video.segmentIndex); expect(refs.length).toBe(1); @@ -2249,7 +2191,7 @@ describe('HlsParser', () => { .setResponseValue('test:/main.mp4', segmentData); const manifest = await parser.start('test:/master', playerInterface); - const video = manifest.periods[0].variants[0].video; + const video = manifest.variants[0].video; expect(video.mimeType).toBe('video/mp4'); }); @@ -2277,7 +2219,7 @@ describe('HlsParser', () => { .setResponseValue('test:/main.mp4?foo=bar', segmentData); const manifest = await parser.start('test:/master', playerInterface); - const video = manifest.periods[0].variants[0].video; + const video = manifest.variants[0].video; expect(video.mimeType).toBe('video/mp4'); }); @@ -2312,9 +2254,9 @@ describe('HlsParser', () => { .setResponseValue('test:/main.mp4', segmentData); const manifest = await parser.start('test:/master', playerInterface); - expect(manifest.periods[0].variants.length).toBe(2); - const audio0 = manifest.periods[0].variants[0].audio; - const audio1 = manifest.periods[0].variants[1].audio; + expect(manifest.variants.length).toBe(2); + const audio0 = manifest.variants[0].audio; + const audio1 = manifest.variants[1].audio; // These should be the exact same memory address, not merely equal. // Otherwise, the parser will only be replacing one of the SegmentIndexes // on update, which will lead to live streaming issues. @@ -2361,7 +2303,7 @@ describe('HlsParser', () => { // would still be wrong. const manifest = await parser.start('media/master', playerInterface); - expect(manifest.periods[0].variants.length).toBe(1); + expect(manifest.variants.length).toBe(1); }); // https://github.com/google/shaka-player/issues/1908 @@ -2391,42 +2333,40 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.language = 'en'; - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.size(1280, 720); - }); - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.language = 'en'; - }); + manifest.addPartialVariant((variant) => { + variant.language = 'en'; + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.size(1280, 720); }); - period.addPartialVariant((variant) => { - variant.language = 'fr'; - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.size(1280, 720); - }); - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.language = 'fr'; - }); + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.language = 'en'; }); - period.addPartialVariant((variant) => { - variant.language = 'en'; - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.size(1920, 1080); - }); - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.language = 'en'; - }); + }); + manifest.addPartialVariant((variant) => { + variant.language = 'fr'; + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.size(1280, 720); }); - period.addPartialVariant((variant) => { - variant.language = 'fr'; - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.size(1920, 1080); - }); - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.language = 'fr'; - }); + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.language = 'fr'; + }); + }); + manifest.addPartialVariant((variant) => { + variant.language = 'en'; + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.size(1920, 1080); + }); + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.language = 'en'; + }); + }); + manifest.addPartialVariant((variant) => { + variant.language = 'fr'; + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.size(1920, 1080); + }); + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.language = 'fr'; }); }); }); @@ -2496,8 +2436,8 @@ describe('HlsParser', () => { shaka.log.alwaysWarn = shaka.test.Util.spyFunc(alwaysWarnSpy); const manifest = await parser.start('test:/master', playerInterface); - expect(manifest.periods[0].variants.length).toBe(1); - expect(manifest.periods[0].variants[0].audio).toBe(null); + expect(manifest.variants.length).toBe(1); + expect(manifest.variants[0].audio).toBe(null); // We should log a warning when this happens. expect(alwaysWarnSpy).toHaveBeenCalled(); @@ -2531,14 +2471,11 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); - manifest.addPeriod(0, (period) => { - const anyVariantId = /** @type {?} */(jasmine.any(Number)); - period.addVariant(anyVariantId, (variant) => { - variant.bandwidth = 200; - variant.language = 'und'; - variant.addPartialStream(ContentType.AUDIO, (stream) => { - stream.mime('audio/mp4', 'mp4a'); - }); + manifest.addPartialVariant((variant) => { + variant.bandwidth = 200; + variant.language = 'und'; + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.mime('audio/mp4', 'mp4a'); }); }); }); @@ -2552,7 +2489,7 @@ describe('HlsParser', () => { .setResponseValue('test:/main.mp4', segmentData); const actual = await parser.start('test:/master', playerInterface); - expect(actual.periods[0].variants.length).toBe(1); + expect(actual.variants.length).toBe(1); expect(actual).toEqual(manifest); }); }); diff --git a/test/media/adaptation_set_criteria_unit.js b/test/media/adaptation_set_criteria_unit.js index 286c27ccea..bb87c84db6 100644 --- a/test/media/adaptation_set_criteria_unit.js +++ b/test/media/adaptation_set_criteria_unit.js @@ -5,520 +5,490 @@ describe('AdaptationSetCriteria', () => { describe('preference based selection', () => { - function variants(manifest) { - return manifest.periods[0].variants; - } - it('chooses variants in user\'s preferred language', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(1, (variant) => { - variant.language = 'es'; - }); - period.addVariant(2, (variant) => { - variant.language = 'en'; - }); - period.addVariant(3, (variant) => { - variant.language = 'en'; - }); + manifest.addVariant(1, (variant) => { + variant.language = 'es'; + }); + manifest.addVariant(2, (variant) => { + variant.language = 'en'; + }); + manifest.addVariant(3, (variant) => { + variant.language = 'en'; }); }); const builder = new shaka.media.PreferenceBasedCriteria('en', '', 0); - const set = builder.create(variants(manifest)); + const set = builder.create(manifest.variants); checkSet(set, [ - manifest.periods[0].variants[1], - manifest.periods[0].variants[2], + manifest.variants[1], + manifest.variants[2], ]); }); it('prefers primary variants', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(1, (variant) => { - variant.primary = true; - }); - period.addVariant(2); - period.addVariant(3); - period.addVariant(4, (variant) => { - variant.primary = true; - }); + manifest.addVariant(1, (variant) => { + variant.primary = true; + }); + manifest.addVariant(2); + manifest.addVariant(3); + manifest.addVariant(4, (variant) => { + variant.primary = true; }); }); const builder = new shaka.media.PreferenceBasedCriteria('en', '', 0); - const set = builder.create(variants(manifest)); + const set = builder.create(manifest.variants); checkSet(set, [ - manifest.periods[0].variants[0], - manifest.periods[0].variants[3], + manifest.variants[0], + manifest.variants[3], ]); }); it('chooses variants in preferred language and role', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(1, (variant) => { - variant.language = 'en'; - variant.addAudio(10, (stream) => { - stream.roles = ['main', 'commentary']; - }); + manifest.addVariant(1, (variant) => { + variant.language = 'en'; + variant.addAudio(10, (stream) => { + stream.roles = ['main', 'commentary']; }); - period.addVariant(2, (variant) => { - variant.language = 'en'; - variant.addAudio(20, (stream) => { - stream.roles = ['secondary']; - }); + }); + manifest.addVariant(2, (variant) => { + variant.language = 'en'; + variant.addAudio(20, (stream) => { + stream.roles = ['secondary']; }); - period.addVariant(3, (variant) => { - variant.language = 'es'; - variant.addAudio(30, (stream) => { - stream.roles = ['main']; - }); + }); + manifest.addVariant(3, (variant) => { + variant.language = 'es'; + variant.addAudio(30, (stream) => { + stream.roles = ['main']; }); }); }); const builder = new shaka.media.PreferenceBasedCriteria('en', 'main', 0); - const set = builder.create(variants(manifest)); + const set = builder.create(manifest.variants); checkSet(set, [ - manifest.periods[0].variants[0], + manifest.variants[0], ]); }); it('chooses only one role, even if none is preferred', () => { // Regression test for https://github.com/google/shaka-player/issues/949 const manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(1, (variant) => { - variant.language = 'en'; - variant.addAudio(10, (stream) => { - stream.roles = ['commentary']; - }); + manifest.addVariant(1, (variant) => { + variant.language = 'en'; + variant.addAudio(10, (stream) => { + stream.roles = ['commentary']; }); - period.addVariant(2, (variant) => { - variant.language = 'en'; - variant.addAudio(20, (stream) => { - stream.roles = ['commentary']; - }); + }); + manifest.addVariant(2, (variant) => { + variant.language = 'en'; + variant.addAudio(20, (stream) => { + stream.roles = ['commentary']; }); - period.addVariant(3, (variant) => { - variant.language = 'en'; - variant.addAudio(30, (stream) => { - stream.roles = ['secondary']; - }); + }); + manifest.addVariant(3, (variant) => { + variant.language = 'en'; + variant.addAudio(30, (stream) => { + stream.roles = ['secondary']; }); - period.addVariant(4, (variant) => { - variant.language = 'en'; - variant.addAudio(40, (stream) => { - stream.roles = ['secondary']; - }); + }); + manifest.addVariant(4, (variant) => { + variant.language = 'en'; + variant.addAudio(40, (stream) => { + stream.roles = ['secondary']; }); - period.addVariant(5, (variant) => { - variant.language = 'en'; - variant.addAudio(50, (stream) => { - stream.roles = ['main']; - }); + }); + manifest.addVariant(5, (variant) => { + variant.language = 'en'; + variant.addAudio(50, (stream) => { + stream.roles = ['main']; }); - period.addVariant(6, (variant) => { - variant.language = 'en'; - variant.addAudio(60, (stream) => { - stream.roles = ['main']; - }); + }); + manifest.addVariant(6, (variant) => { + variant.language = 'en'; + variant.addAudio(60, (stream) => { + stream.roles = ['main']; }); }); }); const builder = new shaka.media.PreferenceBasedCriteria('en', '', 0); - const set = builder.create(variants(manifest)); + const set = builder.create(manifest.variants); // Which role is chosen is an implementation detail. // Each role is found on two variants, so we should have two. checkSet(set, [ - manifest.periods[0].variants[0], - manifest.periods[0].variants[1], + manifest.variants[0], + manifest.variants[1], ]); }); it('chooses only one role, even if all are primary', () => { // Regression test for https://github.com/google/shaka-player/issues/949 const manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(1, (variant) => { - variant.language = 'en'; - variant.primary = true; - variant.addAudio(10, (stream) => { - stream.roles = ['commentary']; - }); + manifest.addVariant(1, (variant) => { + variant.language = 'en'; + variant.primary = true; + variant.addAudio(10, (stream) => { + stream.roles = ['commentary']; }); - period.addVariant(2, (variant) => { - variant.language = 'en'; - variant.primary = true; - variant.addAudio(20, (stream) => { - stream.roles = ['commentary']; - }); + }); + manifest.addVariant(2, (variant) => { + variant.language = 'en'; + variant.primary = true; + variant.addAudio(20, (stream) => { + stream.roles = ['commentary']; }); - period.addVariant(3, (variant) => { - variant.language = 'en'; - variant.primary = true; - variant.addAudio(30, (stream) => { - stream.roles = ['secondary']; - }); + }); + manifest.addVariant(3, (variant) => { + variant.language = 'en'; + variant.primary = true; + variant.addAudio(30, (stream) => { + stream.roles = ['secondary']; }); - period.addVariant(4, (variant) => { - variant.language = 'en'; - variant.primary = true; - variant.addAudio(40, (stream) => { - stream.roles = ['secondary']; - }); + }); + manifest.addVariant(4, (variant) => { + variant.language = 'en'; + variant.primary = true; + variant.addAudio(40, (stream) => { + stream.roles = ['secondary']; }); - period.addVariant(5, (variant) => { - variant.language = 'en'; - variant.primary = true; - variant.addAudio(50, (stream) => { - stream.roles = ['main']; - }); + }); + manifest.addVariant(5, (variant) => { + variant.language = 'en'; + variant.primary = true; + variant.addAudio(50, (stream) => { + stream.roles = ['main']; }); - period.addVariant(6, (variant) => { - variant.language = 'en'; - variant.primary = true; - variant.addAudio(60, (stream) => { - stream.roles = ['main']; - }); + }); + manifest.addVariant(6, (variant) => { + variant.language = 'en'; + variant.primary = true; + variant.addAudio(60, (stream) => { + stream.roles = ['main']; }); }); }); const builder = new shaka.media.PreferenceBasedCriteria('zh', '', 0); - const set = builder.create(variants(manifest)); + const set = builder.create(manifest.variants); // Which role is chosen is an implementation detail. // Each role is found on two variants, so we should have two. checkSet(set, [ - manifest.periods[0].variants[0], - manifest.periods[0].variants[1], + manifest.variants[0], + manifest.variants[1], ]); }); it('chooses only one language, even if all are primary', () => { // Regression test for https://github.com/google/shaka-player/issues/918 const manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(1, (variant) => { - variant.language = 'en'; - variant.primary = true; - variant.addAudio(10); - }); - period.addVariant(2, (variant) => { - variant.language = 'en'; - variant.primary = true; - variant.addAudio(20); - }); - period.addVariant(3, (variant) => { - variant.language = 'es'; - variant.primary = true; - variant.addAudio(30); - }); - period.addVariant(4, (variant) => { - variant.language = 'es'; - variant.primary = true; - variant.addAudio(40); - }); + manifest.addVariant(1, (variant) => { + variant.language = 'en'; + variant.primary = true; + variant.addAudio(10); + }); + manifest.addVariant(2, (variant) => { + variant.language = 'en'; + variant.primary = true; + variant.addAudio(20); + }); + manifest.addVariant(3, (variant) => { + variant.language = 'es'; + variant.primary = true; + variant.addAudio(30); + }); + manifest.addVariant(4, (variant) => { + variant.language = 'es'; + variant.primary = true; + variant.addAudio(40); }); }); const builder = new shaka.media.PreferenceBasedCriteria('zh', '', 0); - const set = builder.create(variants(manifest)); + const set = builder.create(manifest.variants); // Which language is chosen is an implementation detail. // Each role is found on two variants, so we should have two. checkSet(set, [ - manifest.periods[0].variants[0], - manifest.periods[0].variants[1], + manifest.variants[0], + manifest.variants[1], ]); }); it('chooses a role from among primary variants without language match', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(1, (variant) => { - variant.language = 'en'; - variant.primary = true; - variant.addAudio(10, (stream) => { - stream.roles = ['commentary']; - }); + manifest.addVariant(1, (variant) => { + variant.language = 'en'; + variant.primary = true; + variant.addAudio(10, (stream) => { + stream.roles = ['commentary']; }); - period.addVariant(2, (variant) => { - variant.language = 'en'; - variant.primary = true; - variant.addAudio(20, (stream) => { - stream.roles = ['commentary']; - }); + }); + manifest.addVariant(2, (variant) => { + variant.language = 'en'; + variant.primary = true; + variant.addAudio(20, (stream) => { + stream.roles = ['commentary']; }); - period.addVariant(3, (variant) => { - variant.language = 'en'; - variant.addAudio(30, (stream) => { - stream.roles = ['secondary']; - }); + }); + manifest.addVariant(3, (variant) => { + variant.language = 'en'; + variant.addAudio(30, (stream) => { + stream.roles = ['secondary']; }); - period.addVariant(4, (variant) => { - variant.language = 'en'; - variant.addAudio(40, (stream) => { - stream.roles = ['secondary']; - }); + }); + manifest.addVariant(4, (variant) => { + variant.language = 'en'; + variant.addAudio(40, (stream) => { + stream.roles = ['secondary']; }); - period.addVariant(5, (variant) => { - variant.language = 'en'; - variant.primary = true; - variant.addAudio(50, (stream) => { - stream.roles = ['main']; - }); + }); + manifest.addVariant(5, (variant) => { + variant.language = 'en'; + variant.primary = true; + variant.addAudio(50, (stream) => { + stream.roles = ['main']; }); - period.addVariant(6, (variant) => { - variant.language = 'en'; - variant.primary = true; - variant.addAudio(60, (stream) => { - stream.roles = ['main']; - }); + }); + manifest.addVariant(6, (variant) => { + variant.language = 'en'; + variant.primary = true; + variant.addAudio(60, (stream) => { + stream.roles = ['main']; }); }); }); const builder = new shaka.media.PreferenceBasedCriteria('zh', '', 0); - const set = builder.create(variants(manifest)); + const set = builder.create(manifest.variants); // Which role is chosen is an implementation detail. Each role is // found on two variants, so we should have two. Since nothing matches // our language preference, we chose primary variants. checkSet(set, [ - manifest.periods[0].variants[0], - manifest.periods[0].variants[1], + manifest.variants[0], + manifest.variants[1], ]); }); it('chooses a role from best language match, in spite of primary', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(1, (variant) => { - variant.language = 'en'; - variant.primary = true; - variant.addAudio(10, (stream) => { - stream.roles = ['commentary']; - }); + manifest.addVariant(1, (variant) => { + variant.language = 'en'; + variant.primary = true; + variant.addAudio(10, (stream) => { + stream.roles = ['commentary']; }); - period.addVariant(2, (variant) => { - variant.language = 'en'; - variant.primary = true; - variant.addAudio(20, (stream) => { - stream.roles = ['commentary']; - }); + }); + manifest.addVariant(2, (variant) => { + variant.language = 'en'; + variant.primary = true; + variant.addAudio(20, (stream) => { + stream.roles = ['commentary']; }); - period.addVariant(3, (variant) => { - variant.language = 'zh'; - variant.addAudio(30, (stream) => { - stream.roles = ['secondary']; - }); + }); + manifest.addVariant(3, (variant) => { + variant.language = 'zh'; + variant.addAudio(30, (stream) => { + stream.roles = ['secondary']; }); - period.addVariant(4, (variant) => { - variant.language = 'zh'; - variant.addAudio(40, (stream) => { - stream.roles = ['secondary']; - }); + }); + manifest.addVariant(4, (variant) => { + variant.language = 'zh'; + variant.addAudio(40, (stream) => { + stream.roles = ['secondary']; }); - period.addVariant(5, (variant) => { - variant.language = 'en'; - variant.primary = true; - variant.addAudio(50, (stream) => { - stream.roles = ['main']; - }); + }); + manifest.addVariant(5, (variant) => { + variant.language = 'en'; + variant.primary = true; + variant.addAudio(50, (stream) => { + stream.roles = ['main']; }); - period.addVariant(6, (variant) => { - variant.language = 'en'; - variant.primary = true; - variant.addAudio(60, (stream) => { - stream.roles = ['main']; - }); + }); + manifest.addVariant(6, (variant) => { + variant.language = 'en'; + variant.primary = true; + variant.addAudio(60, (stream) => { + stream.roles = ['main']; }); }); }); const builder = new shaka.media.PreferenceBasedCriteria('zh', '', 0); - const set = builder.create(variants(manifest)); + const set = builder.create(manifest.variants); checkSet(set, [ - manifest.periods[0].variants[2], - manifest.periods[0].variants[3], + manifest.variants[2], + manifest.variants[3], ]); }); it('chooses variants with preferred audio channels count', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(1, (variant) => { - variant.addAudio(10, (stream) => { - stream.channelsCount = 2; - }); + manifest.addVariant(1, (variant) => { + variant.addAudio(10, (stream) => { + stream.channelsCount = 2; }); - period.addVariant(2, (variant) => { - variant.addAudio(20, (stream) => { - stream.channelsCount = 6; - }); + }); + manifest.addVariant(2, (variant) => { + variant.addAudio(20, (stream) => { + stream.channelsCount = 6; }); - period.addVariant(3, (variant) => { - variant.addAudio(30, (stream) => { - stream.channelsCount = 2; - }); + }); + manifest.addVariant(3, (variant) => { + variant.addAudio(30, (stream) => { + stream.channelsCount = 2; }); }); }); const builder = new shaka.media.PreferenceBasedCriteria('', '', 2); - const set = builder.create(variants(manifest)); + const set = builder.create(manifest.variants); checkSet(set, [ - manifest.periods[0].variants[0], - manifest.periods[0].variants[2], + manifest.variants[0], + manifest.variants[2], ]); }); it('chooses variants with largest audio channel count less than config' + ' when no exact audio channel count match is possible', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(1, (variant) => { - variant.addAudio(10, (stream) => { - stream.channelsCount = 2; - }); + manifest.addVariant(1, (variant) => { + variant.addAudio(10, (stream) => { + stream.channelsCount = 2; }); - period.addVariant(2, (variant) => { - variant.addAudio(20, (stream) => { - stream.channelsCount = 8; - }); + }); + manifest.addVariant(2, (variant) => { + variant.addAudio(20, (stream) => { + stream.channelsCount = 8; }); - period.addVariant(3, (variant) => { - variant.addAudio(30, (stream) => { - stream.channelsCount = 2; - }); + }); + manifest.addVariant(3, (variant) => { + variant.addAudio(30, (stream) => { + stream.channelsCount = 2; }); }); }); const builder = new shaka.media.PreferenceBasedCriteria('', '', 6); - const set = builder.create(variants(manifest)); + const set = builder.create(manifest.variants); checkSet(set, [ - manifest.periods[0].variants[0], - manifest.periods[0].variants[2], + manifest.variants[0], + manifest.variants[2], ]); }); it('chooses variants with fewest audio channels when none fit in the ' + 'config', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(1, (variant) => { - variant.addAudio(10, (stream) => { - stream.channelsCount = 6; - }); + manifest.addVariant(1, (variant) => { + variant.addAudio(10, (stream) => { + stream.channelsCount = 6; }); - period.addVariant(2, (variant) => { - variant.addAudio(20, (stream) => { - stream.channelsCount = 8; - }); + }); + manifest.addVariant(2, (variant) => { + variant.addAudio(20, (stream) => { + stream.channelsCount = 8; }); - period.addVariant(3, (variant) => { - variant.addAudio(30, (stream) => { - stream.channelsCount = 6; - }); + }); + manifest.addVariant(3, (variant) => { + variant.addAudio(30, (stream) => { + stream.channelsCount = 6; }); }); }); const builder = new shaka.media.PreferenceBasedCriteria('', '', 2); - const set = builder.create(variants(manifest)); + const set = builder.create(manifest.variants); checkSet(set, [ - manifest.periods[0].variants[0], - manifest.periods[0].variants[2], + manifest.variants[0], + manifest.variants[2], ]); }); it('chooses variants with preferred label', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(1, (variant) => { - variant.addAudio(10, (stream) => { - stream.label = 'preferredLabel'; - }); + manifest.addVariant(1, (variant) => { + variant.addAudio(10, (stream) => { + stream.label = 'preferredLabel'; }); - period.addVariant(2, (variant) => { - variant.addAudio(20, (stream) => { - stream.label = 'otherLabel'; - }); + }); + manifest.addVariant(2, (variant) => { + variant.addAudio(20, (stream) => { + stream.label = 'otherLabel'; }); - period.addVariant(3, (variant) => { - variant.addAudio(30, (stream) => { - stream.label = 'preferredLabel'; - }); + }); + manifest.addVariant(3, (variant) => { + variant.addAudio(30, (stream) => { + stream.label = 'preferredLabel'; }); }); }); const builder = new shaka.media.PreferenceBasedCriteria('', '', 0, 'preferredLabel'); - const set = builder.create(variants(manifest)); + const set = builder.create(manifest.variants); checkSet(set, [ - manifest.periods[0].variants[0], - manifest.periods[0].variants[2], + manifest.variants[0], + manifest.variants[2], ]); }); it('chooses variants with preferred label and language', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - // Preferred language and label - period.addVariant(1, (variant) => { - variant.language = 'zh'; - variant.addAudio(10, (stream) => { - stream.label = 'preferredLabel'; - }); + // Preferred language and label + manifest.addVariant(1, (variant) => { + variant.language = 'zh'; + variant.addAudio(10, (stream) => { + stream.label = 'preferredLabel'; }); - // Same language, a different label - period.addVariant(2, (variant) => { - variant.language = 'zh'; - variant.addAudio(20, (stream) => { - stream.label = 'otherLabel'; - }); + }); + // Same language, a different label + manifest.addVariant(2, (variant) => { + variant.language = 'zh'; + variant.addAudio(20, (stream) => { + stream.label = 'otherLabel'; }); - // Same language and label - period.addVariant(3, (variant) => { - variant.language = 'zh'; - variant.addAudio(30, (stream) => { - stream.label = 'preferredLabel'; - }); + }); + // Same language and label + manifest.addVariant(3, (variant) => { + variant.language = 'zh'; + variant.addAudio(30, (stream) => { + stream.label = 'preferredLabel'; }); - // Same label different language - period.addVariant(4, (variant) => { - variant.language = 'pt'; - variant.addAudio(40, (stream) => { - stream.label = 'preferredLabel'; - }); + }); + // Same label different language + manifest.addVariant(4, (variant) => { + variant.language = 'pt'; + variant.addAudio(40, (stream) => { + stream.label = 'preferredLabel'; }); }); }); const builder = new shaka.media.PreferenceBasedCriteria( 'zh', '', 0, 'preferredLabel'); - const set = builder.create(variants(manifest)); + const set = builder.create(manifest.variants); checkSet(set, [ - manifest.periods[0].variants[0], - manifest.periods[0].variants[2], + manifest.variants[0], + manifest.variants[2], ]); }); }); diff --git a/test/media/adaptation_set_unit.js b/test/media/adaptation_set_unit.js index c0aa0e1fc4..de196c58df 100644 --- a/test/media/adaptation_set_unit.js +++ b/test/media/adaptation_set_unit.js @@ -168,7 +168,7 @@ describe('AdaptationSet', () => { encrypted: false, segmentIndex: null, id: id, - keyId: null, + keyIds: [], label: null, language: '', mimeType: mimeType, diff --git a/test/media/drm_engine_integration.js b/test/media/drm_engine_integration.js index 6415590648..222d0ceeb7 100644 --- a/test/media/drm_engine_integration.js +++ b/test/media/drm_engine_integration.js @@ -104,22 +104,20 @@ describe('DrmEngine', () => { drmEngine.configure(config); manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addDrmInfo('com.widevine.alpha'); - variant.addDrmInfo('com.microsoft.playready'); - variant.addVideo(1, (stream) => { - stream.encrypted = true; - }); - variant.addAudio(2, (stream) => { - stream.encrypted = true; - }); + manifest.addVariant(0, (variant) => { + variant.addDrmInfo('com.widevine.alpha'); + variant.addDrmInfo('com.microsoft.playready'); + variant.addVideo(1, (stream) => { + stream.encrypted = true; + }); + variant.addAudio(2, (stream) => { + stream.encrypted = true; }); }); }); - const videoStream = manifest.periods[0].variants[0].video; - const audioStream = manifest.periods[0].variants[0].audio; + const videoStream = manifest.variants[0].video; + const audioStream = manifest.variants[0].audio; eventManager = new shaka.util.EventManager(); @@ -191,8 +189,7 @@ describe('DrmEngine', () => { keyStatusEventSeen.resolve(); }); - const periods = manifest.periods; - const variants = shaka.util.Periods.getAllVariantsFrom(periods); + const variants = manifest.variants; await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); await drmEngine.attach(video); diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index 603a6ef05c..079e11b162 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -4,7 +4,6 @@ */ describe('DrmEngine', () => { - const Periods = shaka.util.Periods; const Util = shaka.test.Util; const originalRequestMediaKeySystemAccess = @@ -66,18 +65,16 @@ describe('DrmEngine', () => { onEventSpy = jasmine.createSpy('onEvent'); manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addDrmInfo('drm.abc'); - variant.addDrmInfo('drm.def'); - variant.addVideo(1, (stream) => { - stream.encrypted = true; - stream.mime('video/foo', 'vbar'); - }); - variant.addAudio(2, (stream) => { - stream.encrypted = true; - stream.mime('audio/foo', 'abar'); - }); + manifest.addVariant(0, (variant) => { + variant.addDrmInfo('drm.abc'); + variant.addDrmInfo('drm.def'); + variant.addVideo(1, (stream) => { + stream.encrypted = true; + stream.mime('video/foo', 'vbar'); + }); + variant.addAudio(2, (stream) => { + stream.encrypted = true; + stream.mime('audio/foo', 'abar'); }); }); }); @@ -137,19 +134,17 @@ describe('DrmEngine', () => { describe('supportsVariants', () => { it('supports all clear variants', async () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addDrmInfo('drm.abc'); - variant.addDrmInfo('drm.def'); - variant.addVideo(1, (stream) => { - stream.encrypted = false; - stream.mime('video/foo', 'vbar'); - }); + manifest.addVariant(0, (variant) => { + variant.addDrmInfo('drm.abc'); + variant.addDrmInfo('drm.def'); + variant.addVideo(1, (stream) => { + stream.encrypted = false; + stream.mime('video/foo', 'vbar'); }); }); }); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); expect(drmEngine.supportsVariant(variants[0])).toBeTruthy(); @@ -161,7 +156,7 @@ describe('DrmEngine', () => { // Accept both drm.abc and drm.def. Only one can be chosen. setRequestMediaKeySystemAccessSpy(['drm.abc', 'drm.def']); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); expect(drmEngine.initialized()).toBe(true); expect(shaka.media.DrmEngine.keySystem(drmEngine.getDrmInfo())) @@ -177,7 +172,7 @@ describe('DrmEngine', () => { // Fail both key systems. setRequestMediaKeySystemAccessSpy([]); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await expectAsync( drmEngine.initForPlayback(variants, manifest.offlineSessionIds)) .toBeRejected(); @@ -200,7 +195,7 @@ describe('DrmEngine', () => { // Ignore error logs, which we expect to occur due to the missing server. logErrorSpy.and.stub(); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await expectAsync( drmEngine.initForPlayback(variants, manifest.offlineSessionIds)) .toBeRejected(); @@ -218,7 +213,7 @@ describe('DrmEngine', () => { setRequestMediaKeySystemAccessSpy(['drm.abc', 'drm.def']); // Add manifest-supplied license servers for both. - for (const drmInfo of manifest.periods[0].variants[0].drmInfos) { + for (const drmInfo of manifest.variants[0].drmInfos) { if (drmInfo.keySystem == 'drm.abc') { drmInfo.licenseServerUri = 'http://foo.bar/abc'; } else if (drmInfo.keySystem == 'drm.def') { @@ -239,7 +234,7 @@ describe('DrmEngine', () => { // Ignore error logs, which we expect to occur due to the missing server. logErrorSpy.and.stub(); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); // Although drm.def appears second in the manifest, it is queried first @@ -255,7 +250,7 @@ describe('DrmEngine', () => { it('detects content type capabilities of key system', async () => { setRequestMediaKeySystemAccessSpy(['drm.abc']); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); expect(drmEngine.initialized()).toBe(true); expect(drmEngine.willSupport('audio/webm')).toBeTruthy(); @@ -273,7 +268,7 @@ describe('DrmEngine', () => { // Accept drm.def, but not drm.abc. setRequestMediaKeySystemAccessSpy(['drm.def']); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); expect(drmEngine.initialized()).toBe(true); expect(shaka.media.DrmEngine.keySystem(drmEngine.getDrmInfo())) @@ -295,7 +290,7 @@ describe('DrmEngine', () => { shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.DRM, shaka.util.Error.Code.REQUESTED_KEY_SYSTEM_CONFIG_UNAVAILABLE)); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await expectAsync( drmEngine.initForPlayback(variants, manifest.offlineSessionIds)) .toBeRejectedWith(expected); @@ -312,14 +307,12 @@ describe('DrmEngine', () => { it('silences errors for unencrypted assets', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(1, (stream) => { - stream.mime('video/foo', 'vbar'); - }); - variant.addAudio(2, (stream) => { - stream.mime('audio/foo', 'abar'); - }); + manifest.addVariant(0, (variant) => { + variant.addVideo(1, (stream) => { + stream.mime('video/foo', 'vbar'); + }); + variant.addAudio(2, (stream) => { + stream.mime('audio/foo', 'abar'); }); }); }); @@ -327,7 +320,7 @@ describe('DrmEngine', () => { // Accept no key systems. setRequestMediaKeySystemAccessSpy([]); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); // Both key systems were tried, since the first one failed. @@ -341,14 +334,14 @@ describe('DrmEngine', () => { it('fails to initialize if no key systems are recognized', async () => { // Simulate the DASH parser inserting a blank placeholder when only // unrecognized custom schemes are found. - manifest.periods[0].variants[0].drmInfos[0].keySystem = ''; - manifest.periods[0].variants[0].drmInfos[1].keySystem = ''; + manifest.variants[0].drmInfos[0].keySystem = ''; + manifest.variants[0].drmInfos[1].keySystem = ''; const expected = Util.jasmineError(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.DRM, shaka.util.Error.Code.NO_RECOGNIZED_KEY_SYSTEMS)); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await expectAsync( drmEngine.initForPlayback(variants, manifest.offlineSessionIds)) .toBeRejectedWith(expected); @@ -368,7 +361,7 @@ describe('DrmEngine', () => { shaka.util.Error.Category.DRM, shaka.util.Error.Code.FAILED_TO_CREATE_CDM, 'whoops!')); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await expectAsync( drmEngine.initForPlayback(variants, manifest.offlineSessionIds)) .toBeRejectedWith(expected); @@ -383,7 +376,7 @@ describe('DrmEngine', () => { it('queries audio/video capabilities', async () => { setRequestMediaKeySystemAccessSpy([]); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await expectAsync( drmEngine.initForPlayback(variants, manifest.offlineSessionIds)) .toBeRejected(); @@ -415,7 +408,7 @@ describe('DrmEngine', () => { it('asks for persistent state and license for offline', async () => { setRequestMediaKeySystemAccessSpy([]); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await expectAsync( drmEngine.initForStorage(variants, /* usePersistentLicense= */ true)) .toBeRejected(); @@ -440,12 +433,10 @@ describe('DrmEngine', () => { it('honors distinctive identifier and persistent state', async () => { setRequestMediaKeySystemAccessSpy([]); - manifest.periods[0].variants[0].drmInfos[0] - .distinctiveIdentifierRequired = true; - manifest.periods[0].variants[0].drmInfos[1] - .persistentStateRequired = true; + manifest.variants[0].drmInfos[0].distinctiveIdentifierRequired = true; + manifest.variants[0].drmInfos[1].persistentStateRequired = true; - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await expectAsync( drmEngine.initForPlayback(variants, manifest.offlineSessionIds)) .toBeRejected(); @@ -468,12 +459,12 @@ describe('DrmEngine', () => { it('makes no queries for clear content if no key config', async () => { setRequestMediaKeySystemAccessSpy([]); - manifest.periods[0].variants[0].drmInfos = []; + manifest.variants[0].drmInfos = []; config.servers = {}; config.advanced = {}; drmEngine.configure(config); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); expect(drmEngine.initialized()).toBe(true); expect(shaka.media.DrmEngine.keySystem(drmEngine.getDrmInfo())).toBe(''); @@ -482,13 +473,13 @@ describe('DrmEngine', () => { it('makes queries for clear content if key is configured', async () => { setRequestMediaKeySystemAccessSpy(['drm.abc']); - manifest.periods[0].variants[0].drmInfos = []; + manifest.variants[0].drmInfos = []; config.servers = { 'drm.abc': 'http://abc.drm/license', }; drmEngine.configure(config); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); expect(drmEngine.initialized()).toBe(true); expect(shaka.media.DrmEngine.keySystem(drmEngine.getDrmInfo())) @@ -499,14 +490,12 @@ describe('DrmEngine', () => { it('uses advanced config to fill in DrmInfo', async () => { // Leave only one drmInfo manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addDrmInfo('drm.abc'); - variant.addVideo(1, (stream) => { - stream.encrypted = true; - }); - variant.addAudio(2); + manifest.addVariant(0, (variant) => { + variant.addDrmInfo('drm.abc'); + variant.addVideo(1, (stream) => { + stream.encrypted = true; }); + variant.addAudio(2); }); }); @@ -522,7 +511,7 @@ describe('DrmEngine', () => { }; drmEngine.configure(config); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await expectAsync( drmEngine.initForPlayback(variants, manifest.offlineSessionIds)) .toBeRejected(); @@ -545,27 +534,22 @@ describe('DrmEngine', () => { it('prefers advanced config from manifest if present', async () => { // Leave only one drmInfo manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addDrmInfo('drm.abc'); - variant.addVideo(1, (stream) => { - stream.encrypted = true; - }); - variant.addAudio(2); + manifest.addVariant(0, (variant) => { + variant.addDrmInfo('drm.abc'); + variant.addVideo(1, (stream) => { + stream.encrypted = true; }); + variant.addAudio(2); }); }); setRequestMediaKeySystemAccessSpy([]); // DrmInfo directly sets advanced settings. - manifest.periods[0].variants[0].drmInfos[0] - .distinctiveIdentifierRequired = true; - manifest.periods[0].variants[0].drmInfos[0] - .persistentStateRequired = true; - manifest.periods[0].variants[0].drmInfos[0] - .audioRobustness = 'good'; - manifest.periods[0].variants[0].drmInfos[0] + manifest.variants[0].drmInfos[0].distinctiveIdentifierRequired = true; + manifest.variants[0].drmInfos[0].persistentStateRequired = true; + manifest.variants[0].drmInfos[0].audioRobustness = 'good'; + manifest.variants[0].drmInfos[0] .videoRobustness = 'really_really_ridiculously_good'; config.advanced['drm.abc'] = { @@ -578,7 +562,7 @@ describe('DrmEngine', () => { }; drmEngine.configure(config); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await expectAsync( drmEngine.initForPlayback(variants, manifest.offlineSessionIds)) .toBeRejected(); @@ -609,7 +593,7 @@ describe('DrmEngine', () => { shaka.util.Error.Category.DRM, shaka.util.Error.Code.NO_LICENSE_SERVER_GIVEN, 'drm.abc')); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await expectAsync( drmEngine.initForPlayback(variants, manifest.offlineSessionIds)) .toBeRejectedWith(expected); @@ -620,15 +604,13 @@ describe('DrmEngine', () => { beforeEach(() => { // Both audio and video with the same key system: manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addDrmInfo('drm.abc'); - variant.addVideo(1, (stream) => { - stream.encrypted = true; - }); - variant.addAudio(2, (stream) => { - stream.encrypted = true; - }); + manifest.addVariant(0, (variant) => { + variant.addDrmInfo('drm.abc'); + variant.addVideo(1, (stream) => { + stream.encrypted = true; + }); + variant.addAudio(2, (stream) => { + stream.encrypted = true; }); }); }); @@ -636,7 +618,7 @@ describe('DrmEngine', () => { it('does nothing for unencrypted content', async () => { setRequestMediaKeySystemAccessSpy([]); - manifest.periods[0].variants[0].drmInfos = []; + manifest.variants[0].drmInfos = []; config.servers = {}; config.advanced = {}; @@ -661,7 +643,7 @@ describe('DrmEngine', () => { it('prefers server certificate from DrmInfo', async () => { const cert1 = new Uint8Array(5); const cert2 = new Uint8Array(1); - manifest.periods[0].variants[0].drmInfos[0].serverCertificate = cert1; + manifest.variants[0].drmInfos[0].serverCertificate = cert1; config.advanced['drm.abc'] = createAdvancedConfig(cert2); drmEngine.configure(config); @@ -683,7 +665,7 @@ describe('DrmEngine', () => { const initData2 = new Uint8Array(0); /** @type {!Uint8Array} */ const initData3 = new Uint8Array(10); - manifest.periods[0].variants[0].drmInfos[0].initData = [ + manifest.variants[0].drmInfos[0].initData = [ {initData: initData1, initDataType: 'cenc', keyId: null}, {initData: initData2, initDataType: 'webm', keyId: null}, {initData: initData3, initDataType: 'cenc', keyId: null}, @@ -710,7 +692,7 @@ describe('DrmEngine', () => { const initData1 = new Uint8Array(1); const initData2 = new Uint8Array(1); const initData3 = new Uint8Array(10); - manifest.periods[0].variants[0].drmInfos[0].initData = [ + manifest.variants[0].drmInfos[0].initData = [ {initData: initData1, initDataType: 'cenc', keyId: 'abc'}, {initData: initData2, initDataType: 'cenc', keyId: 'def'}, {initData: initData3, initDataType: 'cenc', keyId: 'abc'}, @@ -723,7 +705,7 @@ describe('DrmEngine', () => { }); it('uses clearKeys config to override DrmInfo', async () => { - manifest.periods[0].variants[0].drmInfos[0].keySystem = + manifest.variants[0].drmInfos[0].keySystem = 'com.fake.NOT.clearkey'; setRequestMediaKeySystemAccessSpy(['org.w3.clearkey']); @@ -744,8 +726,8 @@ describe('DrmEngine', () => { await initAndAttach(); const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils; - expect(manifest.periods[0].variants[0].drmInfos.length).toBe(1); - expect(manifest.periods[0].variants[0].drmInfos[0].keySystem) + expect(manifest.variants[0].drmInfos.length).toBe(1); + expect(manifest.variants[0].drmInfos[0].keySystem) .toBe('org.w3.clearkey'); expect(session.generateRequest) @@ -764,7 +746,7 @@ describe('DrmEngine', () => { // Regression test for #2139, in which we suppressed errors if drmInfos was // empty and clearKeys config was given it('fails if clearKeys config fails', async () => { - manifest.periods[0].variants[0].drmInfos = []; + manifest.variants[0].drmInfos = []; // Make it so that clear key setup fails by pretending we don't have it. // In reality, it was failing because of missing codec info, but any @@ -778,7 +760,7 @@ describe('DrmEngine', () => { }; drmEngine.configure(config); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; const expected = Util.jasmineError(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, @@ -824,7 +806,7 @@ describe('DrmEngine', () => { // Set up an init data override in the manifest to get an immediate call // to generateRequest: const initData1 = new Uint8Array(5); - manifest.periods[0].variants[0].drmInfos[0].initData = [ + manifest.variants[0].drmInfos[0].initData = [ {initData: initData1, initDataType: 'cenc', keyId: null}, ]; @@ -888,7 +870,7 @@ describe('DrmEngine', () => { it('is ignored when init data is in DrmInfo', async () => { // Set up an init data override in the manifest: - manifest.periods[0].variants[0].drmInfos[0].initData = [ + manifest.variants[0].drmInfos[0].initData = [ {initData: new Uint8Array(0), initDataType: 'cenc', keyId: null}, ]; @@ -918,7 +900,7 @@ describe('DrmEngine', () => { }); it('dispatches an error if manifest says unencrypted', async () => { - manifest.periods[0].variants[0].drmInfos = []; + manifest.variants[0].drmInfos = []; config.servers = {}; config.advanced = {}; @@ -954,7 +936,7 @@ describe('DrmEngine', () => { }); it('prefers a license server URI from configuration', async () => { - manifest.periods[0].variants[0].drmInfos[0].licenseServerUri = + manifest.variants[0].drmInfos[0].licenseServerUri = 'http://foo.bar/drm'; await sendMessageTest('http://abc.drm/license'); }); @@ -1123,7 +1105,7 @@ describe('DrmEngine', () => { // sessions. const initData1 = new Uint8Array(10); const initData2 = new Uint8Array(11); - manifest.periods[0].variants[0].drmInfos[0].initData = [ + manifest.variants[0].drmInfos[0].initData = [ {initData: initData1, initDataType: 'cenc', keyId: null}, {initData: initData2, initDataType: 'cenc', keyId: null}, ]; @@ -1268,8 +1250,7 @@ describe('DrmEngine', () => { }); it('uses clearKeys config to override DrmInfo', async () => { - manifest.periods[0].variants[0].drmInfos[0].keySystem = - 'com.fake.NOT.clearkey'; + manifest.variants[0].drmInfos[0].keySystem = 'com.fake.NOT.clearkey'; setRequestMediaKeySystemAccessSpy(['org.w3.clearkey']); // Configure clear keys (map of hex key IDs to keys) @@ -1423,7 +1404,7 @@ describe('DrmEngine', () => { const p = new shaka.util.PublicPromise(); requestMediaKeySystemAccessSpy.and.returnValue(p); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; const init = drmEngine.initForPlayback( variants, manifest.offlineSessionIds); @@ -1447,7 +1428,7 @@ describe('DrmEngine', () => { const p = new shaka.util.PublicPromise(); requestMediaKeySystemAccessSpy.and.returnValue(p); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; const init = drmEngine.initForPlayback( variants, manifest.offlineSessionIds); @@ -1470,7 +1451,7 @@ describe('DrmEngine', () => { const p = new shaka.util.PublicPromise(); mockMediaKeySystemAccess.createMediaKeys.and.returnValue(p); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; const init = drmEngine.initForPlayback( variants, manifest.offlineSessionIds); @@ -1737,22 +1718,20 @@ describe('DrmEngine', () => { it('includes correct info', async () => { // Leave only one drmInfo manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addDrmInfo('drm.abc'); - variant.addVideo(1, (stream) => { - stream.encrypted = true; - }); - variant.addAudio(2, (stream) => { - stream.encrypted = true; - }); + manifest.addVariant(0, (variant) => { + variant.addDrmInfo('drm.abc'); + variant.addVideo(1, (stream) => { + stream.encrypted = true; + }); + variant.addAudio(2, (stream) => { + stream.encrypted = true; }); }); }); setRequestMediaKeySystemAccessSpy(['drm.abc']); // Key IDs in manifest - manifest.periods[0].variants[0].drmInfos[0].keyIds[0] = + manifest.variants[0].drmInfos[0].keyIds[0] = 'deadbeefdeadbeefdeadbeefdeadbeef'; config.advanced['drm.abc'] = { @@ -1765,7 +1744,7 @@ describe('DrmEngine', () => { }; drmEngine.configure(config); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); expect(drmEngine.initialized()).toBe(true); const drmInfo = drmEngine.getDrmInfo(); @@ -1934,7 +1913,7 @@ describe('DrmEngine', () => { return Promise.resolve(); }); - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); }); @@ -2027,7 +2006,7 @@ describe('DrmEngine', () => { }); async function initAndAttach() { - const variants = Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); await drmEngine.attach(mockVideo); } diff --git a/test/media/media_source_engine_integration.js b/test/media/media_source_engine_integration.js index 2cc356140a..9569ab618e 100644 --- a/test/media/media_source_engine_integration.js +++ b/test/media/media_source_engine_integration.js @@ -66,7 +66,7 @@ describe('MediaSourceEngine', () => { function append(type, segmentNumber) { const segment = generators[type] - .getSegment(segmentNumber, 0, Date.now() / 1000); + .getSegment(segmentNumber, Date.now() / 1000); return mediaSourceEngine.appendBuffer( type, segment, null, null, /* hasClosedCaptions= */ false); } @@ -83,7 +83,7 @@ describe('MediaSourceEngine', () => { // captions. function appendWithClosedCaptions(type, segmentNumber) { const segment = generators[type] - .getSegment(segmentNumber, 0, Date.now() / 1000); + .getSegment(segmentNumber, Date.now() / 1000); return mediaSourceEngine.appendBuffer(type, segment, /* startTime= */ 0, /* endTime= */ 2, /* hasClosedCaptions= */ true); } @@ -97,8 +97,8 @@ describe('MediaSourceEngine', () => { } function remove(type, segmentNumber) { - const start = (segmentNumber - 1) * metadata[type].segmentDuration; - const end = segmentNumber * metadata[type].segmentDuration; + const start = segmentNumber * metadata[type].segmentDuration; + const end = (segmentNumber + 1) * metadata[type].segmentDuration; return mediaSourceEngine.remove(type, start, end); } @@ -116,11 +116,11 @@ describe('MediaSourceEngine', () => { await mediaSourceEngine.setDuration(presentationDuration); await appendInit(ContentType.VIDEO); expect(buffered(ContentType.VIDEO, 0)).toBe(0); - await append(ContentType.VIDEO, 1); + await append(ContentType.VIDEO, 0); expect(buffered(ContentType.VIDEO, 0)).toBeCloseTo(10); - await append(ContentType.VIDEO, 2); + await append(ContentType.VIDEO, 1); expect(buffered(ContentType.VIDEO, 0)).toBeCloseTo(20); - await append(ContentType.VIDEO, 3); + await append(ContentType.VIDEO, 2); expect(buffered(ContentType.VIDEO, 0)).toBeCloseTo(30); }); @@ -131,18 +131,18 @@ describe('MediaSourceEngine', () => { await mediaSourceEngine.setDuration(presentationDuration); await appendInit(ContentType.VIDEO); await Promise.all([ + append(ContentType.VIDEO, 0), append(ContentType.VIDEO, 1), append(ContentType.VIDEO, 2), - append(ContentType.VIDEO, 3), ]); expect(buffered(ContentType.VIDEO, 0)).toBeCloseTo(30); - await remove(ContentType.VIDEO, 1); + await remove(ContentType.VIDEO, 0); expect(bufferStart(ContentType.VIDEO)).toBeCloseTo(10); expect(buffered(ContentType.VIDEO, 10)).toBeCloseTo(20); - await remove(ContentType.VIDEO, 2); + await remove(ContentType.VIDEO, 1); expect(bufferStart(ContentType.VIDEO)).toBe(20); expect(buffered(ContentType.VIDEO, 20)).toBeCloseTo(10); - await remove(ContentType.VIDEO, 3); + await remove(ContentType.VIDEO, 2); expect(bufferStart(ContentType.VIDEO)).toBe(null); }); @@ -154,14 +154,14 @@ describe('MediaSourceEngine', () => { await appendInit(ContentType.VIDEO); await mediaSourceEngine.setDuration(20); expect(mediaSource.duration).toBeCloseTo(20); - await append(ContentType.VIDEO, 1); + await append(ContentType.VIDEO, 0); expect(mediaSource.duration).toBeCloseTo(20); await mediaSourceEngine.setDuration(35); expect(mediaSource.duration).toBeCloseTo(35); await Promise.all([ + append(ContentType.VIDEO, 1), append(ContentType.VIDEO, 2), append(ContentType.VIDEO, 3), - append(ContentType.VIDEO, 4), ]); expect(mediaSource.duration).toBeCloseTo(40); await mediaSourceEngine.setDuration(60); @@ -174,9 +174,9 @@ describe('MediaSourceEngine', () => { await mediaSourceEngine.init(initObject, false); await mediaSourceEngine.setDuration(presentationDuration); await appendInit(ContentType.VIDEO); + await append(ContentType.VIDEO, 0); await append(ContentType.VIDEO, 1); await append(ContentType.VIDEO, 2); - await append(ContentType.VIDEO, 3); await mediaSourceEngine.endOfStream(); expect(mediaSource.duration).toBeCloseTo(30); }); @@ -187,7 +187,7 @@ describe('MediaSourceEngine', () => { await mediaSourceEngine.init(initObject, false); await mediaSourceEngine.setDuration(presentationDuration); await appendInit(ContentType.VIDEO); - await append(ContentType.VIDEO, 1); + await append(ContentType.VIDEO, 0); // Call endOfStream twice. There should be no exception. await mediaSourceEngine.endOfStream(); await mediaSourceEngine.endOfStream(); @@ -211,9 +211,9 @@ describe('MediaSourceEngine', () => { await mediaSourceEngine.init(initObject, false); checkOrder(mediaSourceEngine.setDuration(presentationDuration)); checkOrder(appendInit(ContentType.VIDEO)); + checkOrder(append(ContentType.VIDEO, 0)); checkOrder(append(ContentType.VIDEO, 1)); checkOrder(append(ContentType.VIDEO, 2)); - checkOrder(append(ContentType.VIDEO, 3)); checkOrder(mediaSourceEngine.endOfStream()); await Promise.all(requests); @@ -229,11 +229,11 @@ describe('MediaSourceEngine', () => { // The test operates correctly on real hardware. await appendInit(ContentType.AUDIO); expect(buffered(ContentType.AUDIO, 0)).toBe(0); - await append(ContentType.AUDIO, 1); + await append(ContentType.AUDIO, 0); expect(buffered(ContentType.AUDIO, 0)).toBeCloseTo(10, 1); - await append(ContentType.AUDIO, 2); + await append(ContentType.AUDIO, 1); expect(buffered(ContentType.AUDIO, 0)).toBeCloseTo(20, 1); - await append(ContentType.AUDIO, 3); + await append(ContentType.AUDIO, 2); expect(buffered(ContentType.AUDIO, 0)).toBeCloseTo(30, 1); }); @@ -247,33 +247,33 @@ describe('MediaSourceEngine', () => { const audioStreaming = async () => { await appendInit(ContentType.AUDIO); - await append(ContentType.AUDIO, 1); + await append(ContentType.AUDIO, 0); expect(buffered(ContentType.AUDIO, 0)).toBeCloseTo(10, 1); - await append(ContentType.AUDIO, 2); + await append(ContentType.AUDIO, 1); expect(buffered(ContentType.AUDIO, 0)).toBeCloseTo(20, 1); - await append(ContentType.AUDIO, 3); + await append(ContentType.AUDIO, 2); expect(buffered(ContentType.AUDIO, 0)).toBeCloseTo(30, 1); - await append(ContentType.AUDIO, 4); + await append(ContentType.AUDIO, 3); expect(buffered(ContentType.AUDIO, 0)).toBeCloseTo(40, 1); - await append(ContentType.AUDIO, 5); + await append(ContentType.AUDIO, 4); expect(buffered(ContentType.AUDIO, 0)).toBeCloseTo(50, 1); - await append(ContentType.AUDIO, 6); + await append(ContentType.AUDIO, 5); expect(buffered(ContentType.AUDIO, 0)).toBeCloseTo(60, 1); }; const videoStreaming = async () => { await appendInit(ContentType.VIDEO); - await append(ContentType.VIDEO, 1); + await append(ContentType.VIDEO, 0); expect(buffered(ContentType.VIDEO, 0)).toBeCloseTo(10); - await append(ContentType.VIDEO, 2); + await append(ContentType.VIDEO, 1); expect(buffered(ContentType.VIDEO, 0)).toBeCloseTo(20); - await append(ContentType.VIDEO, 3); + await append(ContentType.VIDEO, 2); expect(buffered(ContentType.VIDEO, 0)).toBeCloseTo(30); - await append(ContentType.VIDEO, 4); + await append(ContentType.VIDEO, 3); expect(buffered(ContentType.VIDEO, 0)).toBeCloseTo(40); - await append(ContentType.VIDEO, 5); + await append(ContentType.VIDEO, 4); expect(buffered(ContentType.VIDEO, 0)).toBeCloseTo(50); - await append(ContentType.VIDEO, 6); + await append(ContentType.VIDEO, 5); expect(buffered(ContentType.VIDEO, 0)).toBeCloseTo(60); }; @@ -293,10 +293,10 @@ describe('MediaSourceEngine', () => { /* appendWindowStart= */ 5, /* appendWindowEnd= */ 18); expect(buffered(ContentType.VIDEO, 0)).toBe(0); - await append(ContentType.VIDEO, 1); + await append(ContentType.VIDEO, 0); expect(bufferStart(ContentType.VIDEO)).toBeCloseTo(5, 1); expect(buffered(ContentType.VIDEO, 5)).toBeCloseTo(5, 1); - await append(ContentType.VIDEO, 2); + await append(ContentType.VIDEO, 1); expect(buffered(ContentType.VIDEO, 5)).toBeCloseTo(13, 1); }); @@ -311,8 +311,8 @@ describe('MediaSourceEngine', () => { /* timestampOffset= */ 0, /* appendWindowStart= */ 0, /* appendWindowEnd= */ 20); + await append(ContentType.VIDEO, 0); await append(ContentType.VIDEO, 1); - await append(ContentType.VIDEO, 2); expect(bufferStart(ContentType.VIDEO)).toBeCloseTo(0, 1); expect(buffered(ContentType.VIDEO, 0)).toBeCloseTo(20, 1); @@ -323,8 +323,8 @@ describe('MediaSourceEngine', () => { /* timestampOffset= */ 15, /* appendWindowStart= */ 20, /* appendWindowEnd= */ 35); + await append(ContentType.VIDEO, 0); await append(ContentType.VIDEO, 1); - await append(ContentType.VIDEO, 2); expect(bufferStart(ContentType.VIDEO)).toBeCloseTo(0, 1); expect(buffered(ContentType.VIDEO, 0)).toBeCloseTo(35, 1); }); @@ -358,7 +358,7 @@ describe('MediaSourceEngine', () => { await mediaSourceEngine.setDuration(presentationDuration); await appendInitWithClosedCaptions(ContentType.VIDEO); mediaSourceEngine.setSelectedClosedCaptionId('CC1'); - await appendWithClosedCaptions(ContentType.VIDEO, 1); + await appendWithClosedCaptions(ContentType.VIDEO, 0); expect(textDisplayer.appendSpy).toHaveBeenCalled(); }); diff --git a/test/media/period_observer_unit.js b/test/media/period_observer_unit.js deleted file mode 100644 index b0f41b4ffd..0000000000 --- a/test/media/period_observer_unit.js +++ /dev/null @@ -1,100 +0,0 @@ -/** @license - * Copyright 2016 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -describe('PeriodObserver', () => { - /** @type {shaka.extern.Manifest} */ - let manifest; - - /** @type {!jasmine.Spy} */ - let onPeriodChanged; - - /** @type {!shaka.media.PeriodObserver} */ - let observer; - - beforeEach(() => { - onPeriodChanged = jasmine.createSpy('onPeriodChanged'); - - manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0); - manifest.addPeriod(10); - manifest.addPeriod(20); - }); - - observer = new shaka.media.PeriodObserver(manifest); - observer.setListeners(shaka.test.Util.spyFunc(onPeriodChanged)); - }); - - afterEach(() => { - observer.release(); - }); - - // When we first update the playhead position, we should see a period changge - // because we are entering our first period. - it('first update calls callback', () => { - // Our first period starts at time=0. - poll(observer, 0); - expect(onPeriodChanged).toHaveBeenCalledOnceMoreWith([manifest.periods[0]]); - }); - - it('does not call callback while in the same period', () => { - // Start in period 0 - poll(observer, 0); - expect(onPeriodChanged).toHaveBeenCalledOnceMoreWith([manifest.periods[0]]); - - // Playing in period 0 (period 1 starts at 10). - for (const time of shaka.util.Iterables.range(10)) { - poll(observer, time); - expect(onPeriodChanged).not.toHaveBeenCalled(); - } - }); - - it('calls callback when changing to later period', () => { - // Start in period 0 - poll(observer, 5); - expect(onPeriodChanged).toHaveBeenCalledOnceMoreWith([manifest.periods[0]]); - - // "Play" into period 1 - poll(observer, 15); - expect(onPeriodChanged).toHaveBeenCalledOnceMoreWith([manifest.periods[1]]); - - // "Play" into period 2 - poll(observer, 25); - expect(onPeriodChanged).toHaveBeenCalledOnceMoreWith([manifest.periods[2]]); - }); - - it('calls callback when changing to previous period', () => { - // Start in period 2 - poll(observer, 25); - expect(onPeriodChanged).toHaveBeenCalledOnceMoreWith([manifest.periods[2]]); - - // "Play" into period 1 - poll(observer, 15); - expect(onPeriodChanged).toHaveBeenCalledOnceMoreWith([manifest.periods[1]]); - - // "Play" into period 0 - poll(observer, 5); - expect(onPeriodChanged).toHaveBeenCalledOnceMoreWith([manifest.periods[0]]); - }); - - it('calls callback once when seeking over Periods', () => { - // Start in period 0 - poll(observer, 5); - expect(onPeriodChanged).toHaveBeenCalledOnceMoreWith([manifest.periods[0]]); - - // Skip period 1 and "play" into period 2 - poll(observer, 25); - expect(onPeriodChanged).toHaveBeenCalledOnceMoreWith([manifest.periods[2]]); - }); - - /** - * @param {!shaka.media.IPlayheadObserver} observer - * @param {number} inSeconds - */ - function poll(observer, inSeconds) { - observer.poll( - /* position= */ inSeconds, - /* seeking= */ false); - } -}); diff --git a/test/media/playhead_unit.js b/test/media/playhead_unit.js index f64259ba03..d9f7305978 100644 --- a/test/media/playhead_unit.js +++ b/test/media/playhead_unit.js @@ -135,7 +135,8 @@ describe('Playhead', () => { timeline.setDuration.and.throwError(new Error()); manifest = { - periods: [], + variants: [], + textStreams: [], presentationTimeline: timeline, minBufferTime: 10, offlineSessionIds: [], diff --git a/test/media/segment_index_unit.js b/test/media/segment_index_unit.js index 64d6288cff..7bf3b105ba 100644 --- a/test/media/segment_index_unit.js +++ b/test/media/segment_index_unit.js @@ -143,7 +143,7 @@ describe('SegmentIndex', /** @suppress {accessControls} */ () => { }); describe('fit', () => { - it('drops references which are outside the period bounds', () => { + it('clamps references to the window bounds', () => { // These negative numbers can occur due to presentationTimeOffset in DASH. const references = [ makeReference(uri(0), -10, -3), @@ -160,7 +160,7 @@ describe('SegmentIndex', /** @suppress {accessControls} */ () => { goog.asserts.assert(positionAtTimeFive != null, 'Null position!'); const referenceAtTimeFive = index.get(positionAtTimeFive); - index.fit(/* periodStart= */ 0, /* periodEnd= */ 15); + index.fit(/* windowStart= */ 0, /* windowEnd= */ 15); const newReferences = [ /* ref 0 dropped because it ends before the period starts */ makeReference(uri(1), -3, 4), @@ -175,19 +175,20 @@ describe('SegmentIndex', /** @suppress {accessControls} */ () => { expect(index.get(positionAtTimeFive)).toBe(referenceAtTimeFive); }); - it('drops references which end exactly at zero', () => { - // The end time is meant to be exclusive, so segments ending at zero - // (after PTO adjustments) should be dropped. + it('drops references which end exactly at window start', () => { + // The end time is meant to be exclusive, so segments ending at window + // start should be dropped. const references = [ makeReference(uri(0), -10, 0), makeReference(uri(1), 0, 10), ]; + const index = new shaka.media.SegmentIndex(references); expect(index.references_).toEqual(references); - index.fit(/* periodStart= */ 0, /* periodEnd= */ 10); + index.fit(/* windowStart= */ 0, /* windowEnd= */ 10); const newReferences = [ - /* ref 0 dropped because it ends before the period starts (at 0) */ + /* ref 0 dropped because it ends when the window start (at 0) */ makeReference(uri(1), 0, 10), ]; expect(index.references_).toEqual(newReferences); diff --git a/test/media/streaming_engine_integration.js b/test/media/streaming_engine_integration.js index b5b95c163f..ea9fa6e54d 100644 --- a/test/media/streaming_engine_integration.js +++ b/test/media/streaming_engine_integration.js @@ -12,6 +12,9 @@ describe('StreamingEngine', () => { /** @type {!shaka.util.EventManager} */ let eventManager; + /** @type {shaka.test.Waiter} */ + let waiter; + /** @type {!HTMLVideoElement} */ let video; let timeline; @@ -29,25 +32,15 @@ describe('StreamingEngine', () => { /** @type {shaka.extern.Variant} */ - let variant1; - /** @type {shaka.extern.Variant} */ - let variant2; + let variant; /** @type {shaka.extern.Manifest} */ let manifest; - /** @type {!jasmine.Spy} */ - let onChooseStreams; - /** @type {!jasmine.Spy} */ - let onCanSwitch; /** @type {!jasmine.Spy} */ let onError; /** @type {!jasmine.Spy} */ let onEvent; - /** @type {!jasmine.Spy} */ - let onInitialStreamsSetup; - /** @type {!jasmine.Spy} */ - let onStartupComplete; beforeAll(() => { video = shaka.test.UiUtils.createVideoElement(); @@ -60,15 +53,13 @@ describe('StreamingEngine', () => { beforeEach(() => { config = shaka.util.PlayerConfiguration.createDefault().streaming; - onChooseStreams = jasmine.createSpy('onChooseStreams'); - onCanSwitch = jasmine.createSpy('onCanSwitch'); - onInitialStreamsSetup = jasmine.createSpy('onInitialStreamsSetup'); - onStartupComplete = jasmine.createSpy('onStartupComplete'); onError = jasmine.createSpy('onError'); onError.and.callFake(fail); onEvent = jasmine.createSpy('onEvent'); eventManager = new shaka.util.EventManager(); + waiter = new shaka.test.Waiter(eventManager); + mediaSourceEngine = new shaka.media.MediaSourceEngine( video, new shaka.test.FakeClosedCaptionParser(), @@ -100,11 +91,11 @@ describe('StreamingEngine', () => { /* isLive= */ false); setupNetworkingEngine( - /* firstPeriodStartTime= */ 0, - /* secondPeriodStartTime= */ 30, /* presentationDuration= */ 60, - {audio: metadata.audio.segmentDuration, - video: metadata.video.segmentDuration}); + { + audio: metadata.audio.segmentDuration, + video: metadata.video.segmentDuration, + }); setupManifest( /* firstPeriodStartTime= */ 0, @@ -138,11 +129,11 @@ describe('StreamingEngine', () => { /* isLive= */ true); setupNetworkingEngine( - /* firstPeriodStartTime= */ 0, - /* secondPeriodStartTime= */ 300, /* presentationDuration= */ Infinity, - {audio: metadata.audio.segmentDuration, - video: metadata.video.segmentDuration}); + { + audio: metadata.audio.segmentDuration, + video: metadata.video.segmentDuration, + }); setupManifest( /* firstPeriodStartTime= */ 0, @@ -181,28 +172,12 @@ describe('StreamingEngine', () => { return generator.init(); } - function setupNetworkingEngine(firstPeriodStartTime, secondPeriodStartTime, - presentationDuration, segmentDurations) { - const periodStartTimes = [firstPeriodStartTime, secondPeriodStartTime]; - - const boundsCheckPosition = (time, number, pos) => { - return shaka.test.StreamingEngineUtil.boundsCheckPosition( - periodStartTimes, presentationDuration, segmentDurations, time, - number, pos); - }; - - const getNumSegments = (type, number) => { - return shaka.test.StreamingEngineUtil.getNumSegments( - periodStartTimes, presentationDuration, segmentDurations, type, - number); - }; - + function setupNetworkingEngine(presentationDuration, segmentDurations) { // Create the fake NetworkingEngine. Note: the StreamingEngine should never // request a segment that does not exist. netEngine = shaka.test.StreamingEngineUtil.createFakeNetworkingEngine( // Init segment generator: (type, periodNumber) => { - expect(periodNumber).toBeLessThan(periodStartTimes.length + 1); const wallClockTime = Date.now() / 1000; const segment = generators[type].getInitSegment(wallClockTime); expect(segment).not.toBeNull(); @@ -210,20 +185,8 @@ describe('StreamingEngine', () => { }, // Media segment generator: (type, periodNumber, position) => { - expect(boundsCheckPosition(type, periodNumber, position)) - .not.toBeNull(); - - // Compute the total number of segments in all Periods before the - // |periodNumber|'th one. - let numPriorSegments = 0; - for (let n = 1; n < periodNumber; ++n) { - numPriorSegments += getNumSegments(type, n); - } - const wallClockTime = Date.now() / 1000; - - const segment = generators[type].getSegment( - position, numPriorSegments, wallClockTime); + const segment = generators[type].getSegment(position, wallClockTime); expect(segment).not.toBeNull(); return segment; }); @@ -245,6 +208,7 @@ describe('StreamingEngine', () => { function setupManifest( firstPeriodStartTime, secondPeriodStartTime, presentationDuration) { manifest = shaka.test.StreamingEngineUtil.createManifest( + /** @type {!shaka.media.PresentationTimeline} */(timeline), [firstPeriodStartTime, secondPeriodStartTime], presentationDuration, /* segmentDurations= */ { audio: metadata.audio.segmentDuration, @@ -255,12 +219,7 @@ describe('StreamingEngine', () => { video: [0, null], }); - manifest.presentationTimeline = - /** @type {!shaka.media.PresentationTimeline} */ (timeline); - manifest.minBufferTime = 2; - - variant1 = manifest.periods[0].variants[0]; - variant2 = manifest.periods[1].variants[0]; + variant = manifest.variants[0]; } function createStreamingEngine() { @@ -269,14 +228,10 @@ describe('StreamingEngine', () => { getBandwidthEstimate: () => 1e6, mediaSourceEngine: mediaSourceEngine, netEngine: /** @type {!shaka.net.NetworkingEngine} */(netEngine), - onChooseStreams: Util.spyFunc(onChooseStreams), - onCanSwitch: Util.spyFunc(onCanSwitch), onError: Util.spyFunc(onError), onEvent: Util.spyFunc(onEvent), onManifestUpdate: () => {}, onSegmentAppended: () => playhead.notifyOfBufferingChange(), - onInitialStreamsSetup: Util.spyFunc(onInitialStreamsSetup), - onStartupComplete: Util.spyFunc(onStartupComplete), }; streamingEngine = new shaka.media.StreamingEngine( /** @type {shaka.extern.Manifest} */(manifest), playerInterface); @@ -289,62 +244,47 @@ describe('StreamingEngine', () => { }); it('plays', async () => { - onStartupComplete.and.callFake(() => { - video.play(); - }); - // Let's go! - onChooseStreams.and.callFake(defaultOnChooseStreams); + streamingEngine.switchVariant(variant); await streamingEngine.start(); - await reachesTheEnd(); + video.play(); + await waiter.timeoutAfter(90).waitForEnd(video); }); it('plays at high playback rates', async () => { - let startupComplete = false; - - onStartupComplete.and.callFake(() => { - startupComplete = true; - video.play(); - video.playbackRate = 10; - }); - // Let's go! - onChooseStreams.and.callFake(defaultOnChooseStreams); + streamingEngine.switchVariant(variant); await streamingEngine.start(); - await reachesTheEnd(); - expect(startupComplete).toBe(true); + video.play(); + video.playbackRate = 10; + await waiter.timeoutAfter(30).waitForEnd(video); }); it('can handle buffered seeks', async () => { - onStartupComplete.and.callFake(() => { - video.play(); - }); - // Let's go! - onChooseStreams.and.callFake(defaultOnChooseStreams); + streamingEngine.switchVariant(variant); await streamingEngine.start(); + video.play(); // After 35 seconds seek back 10 seconds into the first Period. - await passesTime(35); + await waiter.timeoutAfter(60).waitUntilPlayheadReaches(video, 35); video.currentTime = 25; - await reachesTheEnd(); + await waiter.timeoutAfter(60).waitForEnd(video); }); it('can handle unbuffered seeks', async () => { - onStartupComplete.and.callFake(() => { - video.play(); - }); - // Let's go! - onChooseStreams.and.callFake(defaultOnChooseStreams); + streamingEngine.switchVariant(variant); await streamingEngine.start(); - await passesTime(20); + video.play(); + await waiter.timeoutAfter(60).waitUntilPlayheadReaches(video, 20); video.currentTime = 40; - await reachesTheEnd(); + await waiter.timeoutAfter(60).waitForEnd(video); }); }); describe('Live', () => { + /** @type {number} */ let slideSegmentAvailabilityWindow; beforeEach(async () => { @@ -360,67 +300,61 @@ describe('StreamingEngine', () => { }); it('plays through Period transition', async () => { - onStartupComplete.and.callFake(() => { - // firstSegmentNumber = - // [(segmentAvailabilityEnd - rebufferingGoal) / segmentDuration] + 1 - const segmentType = shaka.net.NetworkingEngine.RequestType.SEGMENT; - netEngine.expectRequest('1_video_29', segmentType); - netEngine.expectRequest('1_audio_29', segmentType); - video.play(); - }); - // Let's go! - onChooseStreams.and.callFake(defaultOnChooseStreams); + streamingEngine.switchVariant(variant); await streamingEngine.start(); - await passesTime(305); + + video.play(); + await waiter.timeoutAfter(60).waitUntilPlayheadReaches(video, 305); + + const segmentType = shaka.net.NetworkingEngine.RequestType.SEGMENT; + // firstSegmentNumber = + // [(segmentAvailabilityEnd - rebufferingGoal) / segmentDuration] + 1 + netEngine.expectRequest('0_video_29', segmentType); + netEngine.expectRequest('0_audio_29', segmentType); }); it('can handle seeks ahead of availability window', async () => { - const startUpCompleted = new Promise((resolve) => { - onStartupComplete.and.callFake(() => { - video.play(); - resolve(); - }); - }); - // Let's go! - onChooseStreams.and.callFake(defaultOnChooseStreams); + streamingEngine.switchVariant(variant); await streamingEngine.start(); - await startUpCompleted; + // IE is sensitive and throws InvalidStateError when you seek while + // readyState is 0. + await waiter.timeoutAfter(5).waitForEvent(video, 'loadeddata'); + // Seek outside the availability window right away. The playhead // should adjust the video's current time. video.currentTime = timeline.segmentAvailabilityEnd + 120; + video.play(); // Wait until the repositioning is complete so we don't // immediately hit this case. await shaka.test.Util.delay(/* seconds= */ 1); - await passesTime(305); + await waiter.timeoutAfter(60).waitUntilPlayheadReaches(video, 305); }); it('can handle seeks behind availability window', async () => { - onStartupComplete.and.callFake(() => { - video.play(); - - // Use setTimeout to ensure the playhead has performed it's initial - // seeking. - setTimeout(() => { - // Seek outside the availability window right away. The playhead - // should adjust the video's current time. - video.currentTime = timeline.segmentAvailabilityStart - 120; - expect(video.currentTime).toBeGreaterThan(0); - }, 50); - }); - let seekCount = 0; eventManager.listen(video, 'seeking', () => { seekCount++; }); // Let's go! - onChooseStreams.and.callFake(defaultOnChooseStreams); + streamingEngine.switchVariant(variant); await streamingEngine.start(); - await passesTime(305); + + // IE is sensitive and throws InvalidStateError when you seek while + // readyState is 0. + await waiter.timeoutAfter(5).waitForEvent(video, 'loadeddata'); + + // Seek outside the availability window right away. The playhead + // should adjust the video's current time. + video.currentTime = timeline.segmentAvailabilityStart - 120; + expect(video.currentTime).toBeGreaterThan(0); + + video.play(); + await waiter.timeoutAfter(60).waitUntilPlayheadReaches(video, 305); // We are playing close to the beginning of the availability window. // We should be playing smoothly and not seeking repeatedly as we fall @@ -443,48 +377,52 @@ describe('StreamingEngine', () => { it('jumps small gaps at the beginning', async () => { config.smallGapLimit = 5; await setupGappyContent(/* gapAtStart= */ 1, /* dropSegment= */ false); - onStartupComplete.and.callFake(() => { - expect(video.buffered.length).toBeGreaterThan(0); - expect(video.buffered.start(0)).toBeCloseTo(1); - - video.play(); - }); // Let's go! - onChooseStreams.and.callFake(defaultOnChooseStreams); + streamingEngine.switchVariant(variant); await streamingEngine.start(); - await passesTime(5); + video.play(); + + await waiter.timeoutAfter(5).waitUntilPlayheadReaches(video, 0.01); + expect(video.buffered.length).toBeGreaterThan(0); + expect(video.buffered.start(0)).toBeCloseTo(1); + + await waiter.timeoutAfter(20).waitUntilPlayheadReaches(video, 5); }); it('jumps large gaps at the beginning', async () => { config.smallGapLimit = 1; config.jumpLargeGaps = true; await setupGappyContent(/* gapAtStart= */ 5, /* dropSegment= */ false); - onStartupComplete.and.callFake(() => { - expect(video.buffered.length).toBeGreaterThan(0); - expect(video.buffered.start(0)).toBeCloseTo(5); - - video.play(); - }); // Let's go! - onChooseStreams.and.callFake(defaultOnChooseStreams); + streamingEngine.switchVariant(variant); await streamingEngine.start(); - await passesTime(8); + video.play(); + + await waiter.timeoutAfter(5).waitUntilPlayheadReaches(video, 0.01); + expect(video.buffered.length).toBeGreaterThan(0); + expect(video.buffered.start(0)).toBeCloseTo(5); + + await waiter.timeoutAfter(20).waitUntilPlayheadReaches(video, 8); }); it('jumps small gaps in the middle', async () => { config.smallGapLimit = 20; await setupGappyContent(/* gapAtStart= */ 0, /* dropSegment= */ true); - onStartupComplete.and.callFake(() => { - video.currentTime = 8; - video.play(); - }); // Let's go! - onChooseStreams.and.callFake(defaultOnChooseStreams); + streamingEngine.switchVariant(variant); await streamingEngine.start(); - await passesTime(23); + + // IE is sensitive and throws InvalidStateError when you seek while + // readyState is 0. + await waiter.timeoutAfter(5).waitForEvent(video, 'loadeddata'); + + video.currentTime = 8; + video.play(); + + await waiter.timeoutAfter(60).waitUntilPlayheadReaches(video, 23); // Should be close enough to still have the gap buffered. expect(video.buffered.length).toBe(2); expect(onEvent).not.toHaveBeenCalled(); @@ -493,15 +431,19 @@ describe('StreamingEngine', () => { it('jumps large gaps in the middle', async () => { config.jumpLargeGaps = true; await setupGappyContent(/* gapAtStart= */ 0, /* dropSegment= */ true); - onStartupComplete.and.callFake(() => { - video.currentTime = 8; - video.play(); - }); // Let's go! - onChooseStreams.and.callFake(defaultOnChooseStreams); + streamingEngine.switchVariant(variant); await streamingEngine.start(); - await passesTime(23); + + // IE is sensitive and throws InvalidStateError when you seek while + // readyState is 0. + await waiter.timeoutAfter(5).waitForEvent(video, 'loadeddata'); + + video.currentTime = 8; + video.play(); + + await waiter.timeoutAfter(60).waitUntilPlayheadReaches(video, 23); // Should be close enough to still have the gap buffered. expect(video.buffered.length).toBe(2); expect(onEvent).toHaveBeenCalled(); @@ -510,26 +452,28 @@ describe('StreamingEngine', () => { it('won\'t jump large gaps with preventDefault()', async () => { config.jumpLargeGaps = true; await setupGappyContent(/* gapAtStart= */ 0, /* dropSegment= */ true); - onStartupComplete.and.callFake(() => { - video.currentTime = 8; - video.play(); - }); onEvent.and.callFake((event) => { event.preventDefault(); }); // Let's go! - onChooseStreams.and.callFake(defaultOnChooseStreams); + streamingEngine.switchVariant(variant); await streamingEngine.start(); + // IE is sensitive and throws InvalidStateError when you seek while + // readyState is 0. + await waiter.timeoutAfter(5).waitForEvent(video, 'loadeddata'); + + video.currentTime = 8; + video.play(); + await shaka.test.Util.delay(5); // IE/Edge somehow plays inside the gap. Just make sure we // don't jump the gap. expect(video.currentTime).toBeLessThan(20); }); - /** * @param {number} gapAtStart The gap to introduce before start, in seconds. * @param {boolean} dropSegment Whether to drop a segment in the middle. @@ -551,8 +495,6 @@ describe('StreamingEngine', () => { /* isLive= */ false); setupNetworkingEngine( - /* firstPeriodStartTime= */ 0, - /* secondPeriodStartTime= */ 30, /* presentationDuration= */ 30, { audio: metadata.audio.segmentDuration, @@ -560,14 +502,14 @@ describe('StreamingEngine', () => { }); manifest = setupGappyManifest(gapAtStart, dropSegment); - variant1 = manifest.periods[0].variants[0]; + variant = manifest.variants[0]; setupPlayhead(); createStreamingEngine(); } /** - * TODO: Consolidate with StreamingEngineUtils.createManifest? + * TODO: Consolidate with StreamingEngineUtil.createManifest? * @param {number} gapAtStart * @param {boolean} dropSegment * @return {shaka.extern.Manifest} @@ -581,13 +523,13 @@ describe('StreamingEngine', () => { function createIndex(type, initSegmentReference) { const d = metadata[type].segmentDuration; const refs = []; - let i = 1; + let i = 0; let time = gapAtStart; while (time < 30) { let end = time + d; - // Make segment 1 longer to make the manifest continuous, despite the + // Make segment 0 longer to make the manifest continuous, despite the // dropped segment. - if (i == 1 && dropSegment) { + if (i == 0 && dropSegment) { end += d; } @@ -595,10 +537,10 @@ describe('StreamingEngine', () => { const getUris = () => { // The times in the media are based on the URL; so to drop a // segment, we change the URL. - if (cur >= 2 && dropSegment) { + if (cur >= 1 && dropSegment) { cur++; } - return ['1_' + type + '_' + cur]; + return ['0_' + type + '_' + cur]; }; refs.push(new shaka.media.SegmentReference( /* startTime= */ time, @@ -619,7 +561,7 @@ describe('StreamingEngine', () => { function createInit(type) { const getUris = () => { - return ['1_' + type + '_init']; + return ['0_' + type + '_init']; }; return new shaka.media.InitSegmentReference(getUris, 0, null); } @@ -628,83 +570,36 @@ describe('StreamingEngine', () => { const videoIndex = createIndex('video', videoInit); const audioInit = createInit('audio'); const audioIndex = createIndex('audio', audioInit); + return { presentationTimeline: timeline, offlineSessionIds: [], minBufferTime: 2, - periods: [{ - startTime: 0, - textStreams: [], - variants: [{ - id: 1, - video: { - id: 2, - createSegmentIndex: () => Promise.resolve(), - segmentIndex: videoIndex, - mimeType: 'video/mp4', - codecs: 'avc1.42c01e', - bandwidth: 5000000, - width: 600, - height: 400, - type: shaka.util.ManifestParserUtils.ContentType.VIDEO, - }, - audio: { - id: 3, - createSegmentIndex: () => Promise.resolve(), - segmentIndex: audioIndex, - mimeType: 'audio/mp4', - codecs: 'mp4a.40.2', - bandwidth: 192000, - type: shaka.util.ManifestParserUtils.ContentType.AUDIO, - }, - }], + textStreams: [], + variants: [{ + id: 1, + video: { + id: 2, + createSegmentIndex: () => Promise.resolve(), + segmentIndex: videoIndex, + mimeType: 'video/mp4', + codecs: 'avc1.42c01e', + bandwidth: 5000000, + width: 600, + height: 400, + type: shaka.util.ManifestParserUtils.ContentType.VIDEO, + }, + audio: { + id: 3, + createSegmentIndex: () => Promise.resolve(), + segmentIndex: audioIndex, + mimeType: 'audio/mp4', + codecs: 'mp4a.40.2', + bandwidth: 192000, + type: shaka.util.ManifestParserUtils.ContentType.AUDIO, + }, }], }; } }); - - /** - * Choose streams for the given period. - * - * @param {shaka.extern.Period} period - * @return {!Object.} - */ - function defaultOnChooseStreams(period) { - if (period == manifest.periods[0]) { - return {variant: variant1, text: null}; - } else if (period == manifest.periods[1]) { - return {variant: variant2, text: null}; - } else { - throw new Error(); - } - } - - /** - * @param {number} seconds - * @return {!Promise} - */ - function passesTime(seconds) { - return new Promise((resolve) => { - eventManager.listen(video, 'timeupdate', () => { - if (video.currentTime >= seconds) { - resolve(); - } - }); - }); - } - - /** - * @return {!Promise} - */ - function reachesTheEnd() { - // Safari has a bug where it sometimes doesn't fire the 'ended' event, - // so use 'timeupdate' instead. - return new Promise((resolve) => { - eventManager.listen(video, 'timeupdate', () => { - if (video.ended) { - resolve(); - } - }); - }); - } }); diff --git a/test/media/streaming_engine_unit.js b/test/media/streaming_engine_unit.js index a378c87fd6..a9a24367ae 100644 --- a/test/media/streaming_engine_unit.js +++ b/test/media/streaming_engine_unit.js @@ -37,23 +37,16 @@ describe('StreamingEngine', () => { let netEngine; let timeline; - let audioStream1; - let videoStream1; - let variant1; - let textStream1; - let alternateVariant1; - let alternateVideoStream1; - - let variant2; - let textStream2; + let audioStream; + let videoStream; + let variant; + let textStream; + let alternateVariant; + let alternateVideoStream; /** @type {shaka.extern.Manifest} */ let manifest; - /** @type {!jasmine.Spy} */ - let onChooseStreams; - /** @type {!jasmine.Spy} */ - let onCanSwitch; /** @type {!jasmine.Spy} */ let onError; /** @type {!jasmine.Spy} */ @@ -61,10 +54,6 @@ describe('StreamingEngine', () => { /** @type {!jasmine.Spy} */ let onManifestUpdate; /** @type {!jasmine.Spy} */ - let onInitialStreamsSetup; - /** @type {!jasmine.Spy} */ - let onStartupComplete; - /** @type {!jasmine.Spy} */ let onSegmentAppended; /** @type {!jasmine.Spy} */ let getBandwidthEstimate; @@ -124,8 +113,7 @@ describe('StreamingEngine', () => { makeBuffer(segmentSizes[ContentType.AUDIO]), makeBuffer(segmentSizes[ContentType.AUDIO]), ], - segmentStartTimes: [0, 10, 0, 10], - segmentPeriodTimes: [0, 0, 20, 20], + segmentStartTimes: [0, 10, 20, 30], segmentDuration: 10, }, video: { @@ -138,8 +126,7 @@ describe('StreamingEngine', () => { makeBuffer(segmentSizes[ContentType.VIDEO]), makeBuffer(segmentSizes[ContentType.VIDEO]), ], - segmentStartTimes: [0, 10, 0, 10], - segmentPeriodTimes: [0, 0, 20, 20], + segmentStartTimes: [0, 10, 20, 30], segmentDuration: 10, }, text: { @@ -150,8 +137,7 @@ describe('StreamingEngine', () => { makeBuffer(segmentSizes[ContentType.TEXT]), makeBuffer(segmentSizes[ContentType.TEXT]), ], - segmentStartTimes: [0, 10, 0, 10], - segmentPeriodTimes: [0, 0, 20, 20], + segmentStartTimes: [0, 10, 20, 30], segmentDuration: 10, }, }; @@ -166,8 +152,7 @@ describe('StreamingEngine', () => { makeBuffer(segmentSizes[ContentType.VIDEO]), makeBuffer(segmentSizes[ContentType.VIDEO]), ], - segmentStartTimes: [0, 10, 0, 10], - segmentPeriodTimes: [0, 0, 20, 20], + segmentStartTimes: [0, 10, 20, 30], segmentDuration: 10, }; } @@ -221,7 +206,6 @@ describe('StreamingEngine', () => { makeBuffer(initSegmentSizeAudio)], segments: [], segmentStartTimes: [], - segmentPeriodTimes: [], segmentDuration: 10, }, video: { @@ -230,14 +214,12 @@ describe('StreamingEngine', () => { makeBuffer(initSegmentSizeVideo)], segments: [], segmentStartTimes: [], - segmentPeriodTimes: [], segmentDuration: 10, }, text: { initSegments: [], segments: [], segmentStartTimes: [], - segmentPeriodTimes: [], segmentDuration: 10, }, }; @@ -254,10 +236,6 @@ describe('StreamingEngine', () => { segmentData[ContentType.AUDIO].segmentStartTimes.push(i * 10); segmentData[ContentType.VIDEO].segmentStartTimes.push(i * 10); segmentData[ContentType.TEXT].segmentStartTimes.push(i * 10); - - segmentData[ContentType.AUDIO].segmentPeriodTimes.push(0); - segmentData[ContentType.VIDEO].segmentPeriodTimes.push(0); - segmentData[ContentType.TEXT].segmentPeriodTimes.push(0); } const segmentsInSecondPeriod = 2; @@ -269,16 +247,12 @@ describe('StreamingEngine', () => { segmentData[ContentType.TEXT].segments.push( makeBuffer(segmentSizes[ContentType.TEXT])); - segmentData[ContentType.AUDIO].segmentStartTimes.push(i * 10); - segmentData[ContentType.VIDEO].segmentStartTimes.push(i * 10); - segmentData[ContentType.TEXT].segmentStartTimes.push(i * 10); - - segmentData[ContentType.AUDIO].segmentPeriodTimes.push( - segmentsInFirstPeriod * 10); - segmentData[ContentType.VIDEO].segmentPeriodTimes.push( - segmentsInFirstPeriod * 10); - segmentData[ContentType.TEXT].segmentPeriodTimes.push( - segmentsInFirstPeriod * 10); + segmentData[ContentType.AUDIO].segmentStartTimes.push( + (segmentsInFirstPeriod + i) * 10); + segmentData[ContentType.VIDEO].segmentStartTimes.push( + (segmentsInFirstPeriod + i) * 10); + segmentData[ContentType.TEXT].segmentStartTimes.push( + (segmentsInFirstPeriod + i) * 10); } presentationTimeInSeconds = 110; @@ -314,18 +288,16 @@ describe('StreamingEngine', () => { // request a segment that does not exist. netEngine = shaka.test.StreamingEngineUtil.createFakeNetworkingEngine( // Init segment generator: - (type, periodNumber) => { - expect((periodNumber == 1) || (periodNumber == 2)); - return segmentData[type].initSegments[periodNumber - 1]; + (type, periodIndex) => { + expect((periodIndex == 0) || (periodIndex == 1)); + return segmentData[type].initSegments[periodIndex]; }, // Media segment generator: - (type, periodNumber, position) => { - expect(position).toBeGreaterThan(0); - expect((periodNumber == 1 && position <= segmentsInFirstPeriod) || - (periodNumber == 2 && position <= segmentsInSecondPeriod)); - const i = - (segmentsInFirstPeriod * (periodNumber - 1)) + (position - 1); - return segmentData[type].segments[i]; + (type, periodIndex, position) => { + expect(position).toBeGreaterThan(-1); + expect((periodIndex == 0 && position <= segmentsInFirstPeriod) || + (periodIndex == 1 && position <= segmentsInSecondPeriod)); + return segmentData[type].segments[position]; }); } @@ -341,25 +313,22 @@ describe('StreamingEngine', () => { segmentData['trickvideo'].segmentDuration; } manifest = shaka.test.StreamingEngineUtil.createManifest( - [firstPeriodStartTime, secondPeriodStartTime], presentationDuration, - segmentDurations, initSegmentRanges); - - manifest.presentationTimeline = - /** @type {!shaka.media.PresentationTimeline} */ (timeline); - manifest.minBufferTime = 2; + /** @type {!shaka.media.PresentationTimeline} */(timeline), + [firstPeriodStartTime, secondPeriodStartTime], + presentationDuration, segmentDurations, initSegmentRanges); - audioStream1 = manifest.periods[0].variants[0].audio; - videoStream1 = manifest.periods[0].variants[0].video; - variant1 = manifest.periods[0].variants[0]; - textStream1 = manifest.periods[0].textStreams[0]; + audioStream = manifest.variants[0].audio; + videoStream = manifest.variants[0].video; + variant = manifest.variants[0]; + textStream = manifest.textStreams[0]; // This Stream is only used to verify that StreamingEngine can setup // Streams correctly. - alternateVideoStream1 = + alternateVideoStream = shaka.test.StreamingEngineUtil.createMockVideoStream(8); - alternateVariant1 = { - audio: null, - video: /** @type {shaka.extern.Stream} */ (alternateVideoStream1), + alternateVariant = { + audio: audioStream, + video: /** @type {shaka.extern.Stream} */ (alternateVideoStream), id: 0, language: 'und', primary: false, @@ -368,10 +337,7 @@ describe('StreamingEngine', () => { allowedByApplication: true, allowedByKeySystem: true, }; - manifest.periods[0].variants.push(alternateVariant1); - - variant2 = manifest.periods[1].variants[0]; - textStream2 = manifest.periods[1].textStreams[0]; + manifest.variants.push(alternateVariant); } /** @@ -381,10 +347,6 @@ describe('StreamingEngine', () => { * configuration object which overrides the default one. */ function createStreamingEngine(config) { - onChooseStreams = jasmine.createSpy('onChooseStreams'); - onCanSwitch = jasmine.createSpy('onCanSwitch'); - onInitialStreamsSetup = jasmine.createSpy('onInitialStreamsSetup'); - onStartupComplete = jasmine.createSpy('onStartupComplete'); onError = jasmine.createSpy('onError'); onError.and.callFake(fail); onEvent = jasmine.createSpy('onEvent'); @@ -408,14 +370,10 @@ describe('StreamingEngine', () => { getBandwidthEstimate: Util.spyFunc(getBandwidthEstimate), mediaSourceEngine: mediaSourceEngine, netEngine: /** @type {!shaka.net.NetworkingEngine} */(netEngine), - onChooseStreams: Util.spyFunc(onChooseStreams), - onCanSwitch: Util.spyFunc(onCanSwitch), onError: Util.spyFunc(onError), onEvent: Util.spyFunc(onEvent), onManifestUpdate: Util.spyFunc(onManifestUpdate), onSegmentAppended: Util.spyFunc(onSegmentAppended), - onInitialStreamsSetup: Util.spyFunc(onInitialStreamsSetup), - onStartupComplete: Util.spyFunc(onStartupComplete), }; streamingEngine = new shaka.media.StreamingEngine( /** @type {shaka.extern.Manifest} */(manifest), playerInterface); @@ -433,23 +391,14 @@ describe('StreamingEngine', () => { // This test initializes the StreamingEngine (SE) and allows it to play // through both Periods. // - // After calling start() the following should occur: - // 1. SE should immediately call onChooseStreams() with the first Period. - // 2. SE should setup each of the initial Streams and then call - // onInitialStreamsSetup(). - // 3. SE should start appending the initial Streams' segments and in - // parallel setup the remaining Streams within the Manifest. - // - SE should call onStartupComplete() after it has buffered at least 1 - // segment of each type of content. - // - SE should call onCanSwitch() with the first Period after it has - // setup the remaining Streams within the first Period. - // 4. SE should call onChooseStreams() with the second Period after it has - // both segments within the first Period. - // - We must return the Streams within the second Period. - // 5. SE should call onCanSwitch() with the second Period shortly after - // step 4. - // 6. SE should call MediaSourceEngine.endOfStream() after it has appended - // both segments within the second Period. At this point the playhead + // After construction of StreamingEngine, the following should occur: + // 1. The owner should immediately call switchVariant() with the initial + // variant. + // 2. The owner should call start(). + // 3. SE should setup each of the initial Streams. + // 4. SE should start appending the initial Streams' segments. + // 5. SE should call MediaSourceEngine.endOfStream() after it has appended + // both segments from the second Period. At this point, the playhead // should not be at the end of the presentation, but the test will be // effectively over since SE will have nothing else to do. it('initializes and plays VOD', async () => { @@ -457,84 +406,37 @@ describe('StreamingEngine', () => { mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); createStreamingEngine(); - onStartupComplete.and.callFake(() => { - // Verify buffers. - expect(mediaSourceEngine.initSegments).toEqual({ - audio: [true, false], - video: [true, false], - text: [], - }); - expect(mediaSourceEngine.segments).toEqual({ - audio: [true, false, false, false], - video: [true, false, false, false], - text: [true, false, false, false], - }); + // Here we go! + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; - setupFakeGetTime(0); + // Verify buffers. + expect(mediaSourceEngine.initSegments).toEqual({ + audio: [false, false], + video: [false, false], + text: [], }); - - expect(mediaSourceEngine.reinitText).not.toHaveBeenCalled(); - - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[0]); - - onCanSwitch.and.callFake(() => { - expect(mediaSourceEngine.reinitText).not.toHaveBeenCalled(); - mediaSourceEngine.reinitText.calls.reset(); - onCanSwitch.and.throwError(new Error()); - }); - - // For second Period. - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[1]); - - // Verify buffers. - expect(mediaSourceEngine.initSegments).toEqual({ - audio: [true, false], - video: [true, false], - text: [], - }); - expect(mediaSourceEngine.segments).toEqual({ - audio: [true, true, false, false], - video: [true, true, false, false], - text: [true, true, false, false], - }); - - verifyNetworkingEngineRequestCalls(1); - - onCanSwitch.and.callFake(() => { - expect(mediaSourceEngine.reinitText).toHaveBeenCalled(); - mediaSourceEngine.reinitText.calls.reset(); - onCanSwitch.and.throwError(new Error()); - }); - - // Switch to the second Period. - return defaultOnChooseStreams(period); - }); - - // Init the first Period. - return defaultOnChooseStreams(period); + expect(mediaSourceEngine.segments).toEqual({ + audio: [false, false, false, false], + video: [false, false, false, false], + text: [false, false, false, false], }); - onInitialStreamsSetup.and.callFake(() => { - const expectedObject = new Map(); - expectedObject.set(ContentType.AUDIO, audioStream1); - expectedObject.set(ContentType.VIDEO, videoStream1); - expectedObject.set(ContentType.TEXT, textStream1); - expect(mediaSourceEngine.init) - .toHaveBeenCalledWith(expectedObject, false); - expect(mediaSourceEngine.init).toHaveBeenCalledTimes(1); - mediaSourceEngine.init.calls.reset(); + const expectedMseInit = new Map(); + expectedMseInit.set(ContentType.AUDIO, audioStream); + expectedMseInit.set(ContentType.VIDEO, videoStream); + expectedMseInit.set(ContentType.TEXT, textStream); - expect(mediaSourceEngine.setDuration).toHaveBeenCalledTimes(1); - expect(mediaSourceEngine.setDuration).toHaveBeenCalledWith(40); - mediaSourceEngine.setDuration.calls.reset(); - }); + expect(mediaSourceEngine.init).toHaveBeenCalledWith(expectedMseInit, false); + expect(mediaSourceEngine.init).toHaveBeenCalledTimes(1); - // Here we go! - streamingEngine.start(); + expect(mediaSourceEngine.setDuration).toHaveBeenCalledTimes(1); + expect(mediaSourceEngine.setDuration).toHaveBeenCalledWith(40); await runTest(); + expect(mediaSourceEngine.endOfStream).toHaveBeenCalled(); // Verify buffers. @@ -549,35 +451,43 @@ describe('StreamingEngine', () => { text: [true, true, true, true], }); - verifyNetworkingEngineRequestCalls(2); - }); + netEngine.expectRangeRequest( + '0_audio_init', + initSegmentRanges[ContentType.AUDIO][0], + initSegmentRanges[ContentType.AUDIO][1]); - describe('loadNewTextStream', () => { - it('clears MediaSourceEngine', async () => { - setupVod(); - mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); - createStreamingEngine(); - onStartupComplete.and.callFake(() => { - setupFakeGetTime(0); - }); - onChooseStreams.and.callFake(onChooseStreamsWithUnloadedText); + netEngine.expectRangeRequest( + '0_video_init', + initSegmentRanges[ContentType.VIDEO][0], + initSegmentRanges[ContentType.VIDEO][1]); + + netEngine.expectRangeRequest( + '1_audio_init', + initSegmentRanges[ContentType.AUDIO][0], + initSegmentRanges[ContentType.AUDIO][1]); + + netEngine.expectRangeRequest( + '1_video_init', + initSegmentRanges[ContentType.VIDEO][0], + initSegmentRanges[ContentType.VIDEO][1]); - streamingEngine.start(); + const segmentType = shaka.net.NetworkingEngine.RequestType.SEGMENT; - await runTest(async () => { - if (presentationTimeInSeconds == 20) { - mediaSourceEngine.clear.calls.reset(); - mediaSourceEngine.init.calls.reset(); - await streamingEngine.loadNewTextStream(textStream2); - expect(mediaSourceEngine.clear).toHaveBeenCalledWith('text'); + netEngine.expectRequest('0_audio_0', segmentType); + netEngine.expectRequest('0_video_0', segmentType); + netEngine.expectRequest('0_text_0', segmentType); - const expectedObject = new Map(); - expectedObject.set(ContentType.TEXT, jasmine.any(Object)); - expect(mediaSourceEngine.init).toHaveBeenCalledWith( - expectedObject, false); - } - }); - }); + netEngine.expectRequest('0_audio_1', segmentType); + netEngine.expectRequest('0_video_1', segmentType); + netEngine.expectRequest('0_text_1', segmentType); + + netEngine.expectRequest('1_audio_2', segmentType); + netEngine.expectRequest('1_video_2', segmentType); + netEngine.expectRequest('1_text_2', segmentType); + + netEngine.expectRequest('1_audio_3', segmentType); + netEngine.expectRequest('1_video_3', segmentType); + netEngine.expectRequest('1_text_3', segmentType); }); describe('unloadTextStream', () => { @@ -585,26 +495,26 @@ describe('StreamingEngine', () => { setupVod(); mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); createStreamingEngine(); - onStartupComplete.and.callFake(() => { - setupFakeGetTime(0); - }); - onChooseStreams.and.callFake(onChooseStreamsWithUnloadedText); - streamingEngine.start(); - const segmentType = shaka.net.NetworkingEngine.RequestType.SEGMENT; + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; // Verify that after unloading text stream, no network request for text // is sent. await runTest(() => { + const segmentType = shaka.net.NetworkingEngine.RequestType.SEGMENT; + if (presentationTimeInSeconds == 1) { - netEngine.expectRequest('1_text_1', segmentType); + netEngine.expectRequest('0_text_0', segmentType); netEngine.request.calls.reset(); streamingEngine.unloadTextStream(); } else if (presentationTimeInSeconds == 35) { - netEngine.expectNoRequest('1_text_1', segmentType); + netEngine.expectNoRequest('0_text_0', segmentType); + netEngine.expectNoRequest('0_text_1', segmentType); netEngine.expectNoRequest('1_text_2', segmentType); - netEngine.expectNoRequest('2_text_1', segmentType); - netEngine.expectNoRequest('2_text_2', segmentType); + netEngine.expectNoRequest('1_text_3', segmentType); } }); }); @@ -615,16 +525,12 @@ describe('StreamingEngine', () => { mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); createStreamingEngine(); - presentationTimeInSeconds = 100; - - onStartupComplete.and.callFake(() => { - setupFakeGetTime(100); - }); - - onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p)); - // Here we go! - streamingEngine.start(); + presentationTimeInSeconds = 100; + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(slideSegmentAvailabilityWindow); expect(mediaSourceEngine.endOfStream).toHaveBeenCalled(); @@ -647,101 +553,6 @@ describe('StreamingEngine', () => { } }); - // Start the playhead in the first Period but pass start() Streams from the - // second Period. - it('plays from 1st Period when passed Streams from 2nd', async () => { - setupVod(); - mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); - createStreamingEngine(); - - onStartupComplete.and.callFake(() => { - setupFakeGetTime(0); - }); - - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[0]); - - // Start with Streams from the second Period even though the playhead is - // in the first Period. onChooseStreams() should be called again for the - // first Period and then eventually for the second Period. - - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[0]); - - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[1]); - - return defaultOnChooseStreams(period); - }); - - return defaultOnChooseStreams(period); - }); - - return {variant: variant2, text: textStream2}; - }); - - streamingEngine.start(); - - await runTest(); - // Verify buffers. - expect(mediaSourceEngine.initSegments).toEqual({ - audio: [false, true], - video: [false, true], - text: [], - }); - expect(mediaSourceEngine.segments).toEqual({ - audio: [true, true, true, true], - video: [true, true, true, true], - text: [true, true, true, true], - }); - }); - - // Start the playhead in the second Period but pass start() Streams from the - // first Period. - it('plays from 2nd Period when passed Streams from 1st', async () => { - setupVod(); - mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); - createStreamingEngine(); - - presentationTimeInSeconds = 20; - onStartupComplete.and.callFake(() => { - setupFakeGetTime(20); - }); - - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[1]); - - // Start with Streams from the first Period even though the playhead is - // in the second Period. onChooseStreams() should be called again for the - // second Period. - - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[1]); - - onChooseStreams.and.throwError(new Error()); - - return defaultOnChooseStreams(period); - }); - - return {variant: variant1, text: textStream1}; - }); - - streamingEngine.start(); - - await runTest(); - // Verify buffers. - expect(mediaSourceEngine.initSegments).toEqual({ - audio: [false, true], - video: [false, true], - text: [], - }); - expect(mediaSourceEngine.segments).toEqual({ - audio: [false, false, true, true], - video: [false, false, true, true], - text: [false, false, true, true], - }); - }); - it('plays when a small gap is present at the beginning', async () => { const drift = 0.050; // 50 ms @@ -751,131 +562,32 @@ describe('StreamingEngine', () => { createStreamingEngine(); // Here we go! - onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p)); - streamingEngine.start(); - - await runTest(); - expect(onStartupComplete).toHaveBeenCalled(); - }); - - it('plays when 1st Period doesn\'t have text streams', async () => { - setupVod(); - manifest.periods[0].textStreams = []; - - mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); - createStreamingEngine(); - - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); - onChooseStreams.and.callFake((period) => { - const chosen = defaultOnChooseStreams(period); - if (period == manifest.periods[0]) { - chosen.text = null; - } - return chosen; - }); - - // Here we go! - streamingEngine.start(); - await runTest(); - - expect(mediaSourceEngine.segments).toEqual({ - audio: [true, true, true, true], - video: [true, true, true, true], - text: [false, false, true, true], - }); - }); - - it('doesn\'t get stuck when 2nd Period isn\'t available yet', async () => { - // See: https://github.com/google/shaka-player/pull/839 - setupVod(); - manifest.periods[0].textStreams = []; - - // For the first update, indicate the segment isn't available. This should - // not cause us to fallback to the Playhead time to determine which segment - // to start streaming. - await textStream2.createSegmentIndex(); - const oldGet = textStream2.segmentIndex.get; - textStream2.segmentIndex.get = (idx) => { - if (idx == 1) { - textStream2.segmentIndex.get = oldGet; - return null; - } - return oldGet(idx); - }; - - mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); - createStreamingEngine(); - - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); - onChooseStreams.and.callFake((period) => { - const chosen = defaultOnChooseStreams(period); - if (period == manifest.periods[0]) { - chosen.text = null; - } - return chosen; - }); - - // Here we go! - streamingEngine.start(); - await runTest(); - - expect(mediaSourceEngine.segments).toEqual({ - audio: [true, true, true, true], - video: [true, true, true, true], - text: [false, false, true, true], - }); - }); - - it('only reinitializes text when switching streams', async () => { - // See: https://github.com/google/shaka-player/issues/910 - setupVod(); - mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); - createStreamingEngine(); - - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); - onChooseStreams.and.callFake(defaultOnChooseStreams); - - // When we can switch in the second Period, switch to the playing stream. - onCanSwitch.and.callFake(() => { - onCanSwitch.and.callFake(() => { - expect(streamingEngine.getBufferingText()).toBe(textStream2); - - mediaSourceEngine.reinitText.calls.reset(); - streamingEngine.switchTextStream(textStream2); - }); - }); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; - // Here we go! - streamingEngine.start(); await runTest(); - - expect(mediaSourceEngine.reinitText).not.toHaveBeenCalled(); }); - it('plays when 2nd Period doesn\'t have text streams', async () => { + it('plays with no chosen text streams', async () => { setupVod(); - manifest.periods[1].textStreams = []; + manifest.textStreams = []; mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); createStreamingEngine(); - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); - onChooseStreams.and.callFake((period) => { - const chosen = defaultOnChooseStreams(period); - if (period == manifest.periods[1]) { - chosen.text = null; - } - return chosen; - }); - // Here we go! - streamingEngine.start(); + streamingEngine.switchVariant(variant); + // Don't call switchTextStream. + await streamingEngine.start(); + playing = true; await runTest(); expect(mediaSourceEngine.segments).toEqual({ audio: [true, true, true, true], video: [true, true, true, true], - text: [true, true, false, false], + text: [false, false, false, false], }); }); @@ -884,9 +596,6 @@ describe('StreamingEngine', () => { mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); createStreamingEngine(); - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); - onChooseStreams.and.callFake(defaultOnChooseStreams); - mediaSourceEngine.endOfStream.and.callFake(() => { expect(mediaSourceEngine.setDuration).toHaveBeenCalledWith(40); expect(mediaSourceEngine.setDuration).toHaveBeenCalledTimes(1); @@ -897,7 +606,10 @@ describe('StreamingEngine', () => { }); // Here we go! - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); expect(mediaSourceEngine.endOfStream).toHaveBeenCalled(); @@ -910,9 +622,6 @@ describe('StreamingEngine', () => { mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); createStreamingEngine(); - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); - onChooseStreams.and.callFake(defaultOnChooseStreams); - mediaSourceEngine.endOfStream.and.callFake(() => { expect(mediaSourceEngine.setDuration).toHaveBeenCalledWith(40); expect(mediaSourceEngine.setDuration).toHaveBeenCalledTimes(1); @@ -923,7 +632,10 @@ describe('StreamingEngine', () => { }); // Here we go! - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); expect(mediaSourceEngine.endOfStream).toHaveBeenCalled(); @@ -936,15 +648,15 @@ describe('StreamingEngine', () => { mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); createStreamingEngine(); - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); - onChooseStreams.and.callFake(defaultOnChooseStreams); - // The duration can spuriously be set to 0, so we should ignore this and not // update the duration. mediaSourceEngine.getDuration.and.returnValue(0); // Here we go! - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); expect(mediaSourceEngine.endOfStream).toHaveBeenCalled(); @@ -956,11 +668,11 @@ describe('StreamingEngine', () => { mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); createStreamingEngine(); - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); - onChooseStreams.and.callFake(defaultOnChooseStreams); - // Here we go! - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); // The second Period starts at 20, so we should set the appendWindowStart to @@ -972,7 +684,7 @@ describe('StreamingEngine', () => { asymmetricMatch: (val) => val > 40 && val <= 40.1, }; expect(mediaSourceEngine.setStreamProperties) - .toHaveBeenCalledWith('video', 20, lt20, gt40); + .toHaveBeenCalledWith('video', 0, lt20, gt40); }); it('does not buffer one media type ahead of another', async () => { @@ -1003,6 +715,12 @@ describe('StreamingEngine', () => { maxBuffered = Math.max(maxBuffered, buffered); } + // Simulated playback doesn't start until some of each is buffered. This + // realism is important to the test passing. + if (minBuffered > 0) { + playing = true; + } + // Sanity check. expect(maxBuffered).not.toBeLessThan(minBuffered); // Proof that we didn't get too far ahead (10s == 1 segment). @@ -1012,9 +730,12 @@ describe('StreamingEngine', () => { }); // Here we go! - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); - onChooseStreams.and.callFake(defaultOnChooseStreams); - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + // Simulated playback is started in the appendBuffer fake when some of each + // type is buffered. This realism is important to the test passing. + playing = false; await runTest(); // Make sure appendBuffer was called, so that we know that we executed the @@ -1031,37 +752,35 @@ describe('StreamingEngine', () => { beforeEach(() => { // Set up a manifest with multiple variants and a text stream. manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addAudio(10, (stream) => { - stream.useSegmentTemplate('audio-10-%d.mp4', 10); - }); - variant.addVideo(11, (stream) => { - stream.useSegmentTemplate('video-11-%d.mp4', 10); - }); + manifest.addVariant(0, (variant) => { + variant.addAudio(10, (stream) => { + stream.useSegmentTemplate('audio-10-%d.mp4', 10); }); - period.addVariant(1, (variant) => { - variant.addExistingStream(10); // audio - variant.addVideo(12, (stream) => { - stream.useSegmentTemplate('video-12-%d.mp4', 10); - }); + variant.addVideo(11, (stream) => { + stream.useSegmentTemplate('video-11-%d.mp4', 10); }); - period.addVariant(2, (variant) => { - variant.addAudio(13, (stream) => { - stream.useSegmentTemplate('audio-13-%d.mp4', 10); - }); - variant.addExistingStream(12); // video + }); + manifest.addVariant(1, (variant) => { + variant.addExistingStream(10); // audio + variant.addVideo(12, (stream) => { + stream.useSegmentTemplate('video-12-%d.mp4', 10); }); - period.addTextStream(20, (stream) => { - stream.useSegmentTemplate('text-20-%d.mp4', 10); + }); + manifest.addVariant(2, (variant) => { + variant.addAudio(13, (stream) => { + stream.useSegmentTemplate('audio-13-%d.mp4', 10); }); + variant.addExistingStream(12); // video + }); + manifest.addTextStream(20, (stream) => { + stream.useSegmentTemplate('text-20-%d.mp4', 10); }); }); - initialVariant = manifest.periods[0].variants[0]; - sameAudioVariant = manifest.periods[0].variants[1]; - sameVideoVariant = manifest.periods[0].variants[2]; - initialTextStream = manifest.periods[0].textStreams[0]; + initialVariant = manifest.variants[0]; + sameAudioVariant = manifest.variants[1]; + sameVideoVariant = manifest.variants[2]; + initialTextStream = manifest.textStreams[0]; // For these tests, we don't care about specific data appended. // Just return any old ArrayBuffer for any requested segment. @@ -1102,41 +821,34 @@ describe('StreamingEngine', () => { presentationTimeInSeconds = 0; createStreamingEngine(); - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); - onChooseStreams.and.callFake(() => { - return {variant: initialVariant, text: initialTextStream}; - }); + streamingEngine.switchVariant(initialVariant); + streamingEngine.switchTextStream(initialTextStream); }); it('will not clear buffers if streams have not changed', async () => { - onCanSwitch.and.callFake(async () => { - mediaSourceEngine.clear.calls.reset(); - streamingEngine.switchVariant( - sameAudioVariant, /* clearBuffer= */ true, /* safeMargin= */ 0); - await Util.fakeEventLoop(1); - expect(mediaSourceEngine.clear).not.toHaveBeenCalledWith('audio'); - expect(mediaSourceEngine.clear).toHaveBeenCalledWith('video'); - expect(mediaSourceEngine.clear).not.toHaveBeenCalledWith('text'); - - mediaSourceEngine.clear.calls.reset(); - streamingEngine.switchVariant( - sameVideoVariant, /* clearBuffer= */ true, /* safeMargin= */ 0); - await Util.fakeEventLoop(1); - expect(mediaSourceEngine.clear).toHaveBeenCalledWith('audio'); - expect(mediaSourceEngine.clear).not.toHaveBeenCalledWith('video'); - expect(mediaSourceEngine.clear).not.toHaveBeenCalledWith('text'); + streamingEngine.start().catch(fail); + playing = true; - mediaSourceEngine.clear.calls.reset(); - streamingEngine.switchTextStream(initialTextStream); - await Util.fakeEventLoop(1); - expect(mediaSourceEngine.clear).not.toHaveBeenCalled(); - }); + await Util.fakeEventLoop(1); - streamingEngine.start().catch(fail); + mediaSourceEngine.clear.calls.reset(); + streamingEngine.switchVariant(sameAudioVariant, /* clearBuffer= */ true); + await Util.fakeEventLoop(1); + expect(mediaSourceEngine.clear).not.toHaveBeenCalledWith('audio'); + expect(mediaSourceEngine.clear).toHaveBeenCalledWith('video'); + expect(mediaSourceEngine.clear).not.toHaveBeenCalledWith('text'); - await Util.fakeEventLoop(10); + mediaSourceEngine.clear.calls.reset(); + streamingEngine.switchVariant(sameVideoVariant, /* clearBuffer= */ true); + await Util.fakeEventLoop(1); + expect(mediaSourceEngine.clear).toHaveBeenCalledWith('audio'); + expect(mediaSourceEngine.clear).not.toHaveBeenCalledWith('video'); + expect(mediaSourceEngine.clear).not.toHaveBeenCalledWith('text'); - expect(onCanSwitch).toHaveBeenCalled(); + mediaSourceEngine.clear.calls.reset(); + streamingEngine.switchTextStream(initialTextStream); + await Util.fakeEventLoop(1); + expect(mediaSourceEngine.clear).not.toHaveBeenCalled(); }); }); @@ -1149,41 +861,27 @@ describe('StreamingEngine', () => { mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); createStreamingEngine(); - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); - onTick = jasmine.createSpy('onTick'); onTick.and.stub(); }); it('into buffered regions', async () => { - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[0]); - - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[1]); - - // Seek backwards to a buffered region in the first Period. Note that - // since the buffering goal is 5 seconds and each segment is 10 - // seconds long, the second segment of this Period will be required at - // 6 seconds. Then it will load the next Period, but not require the - // new segments. - expect(presentationTimeInSeconds).toBe(6); + // Here we go! + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; + + let seekComplete = false; + await runTest(() => { + if (presentationTimeInSeconds == 6 && !seekComplete) { + // Seek backwards to a buffered region in the first Period. presentationTimeInSeconds -= 5; streamingEngine.seeked(); - - // Although we're seeking backwards we still have to return some - // Streams from the second Period here. - return defaultOnChooseStreams(period); - }); - - // Init the first Period. - return defaultOnChooseStreams(period); + seekComplete = true; + } }); - // Here we go! - streamingEngine.start(); - - await runTest(); // Verify buffers. expect(mediaSourceEngine.initSegments).toEqual({ audio: [false, true], @@ -1202,49 +900,38 @@ describe('StreamingEngine', () => { // resolution, and after the seek some states are buffered and some // are unbuffered, StreamingEngine should only clear the unbuffered // states. - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[0]); - - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[1]); - - mediaSourceEngine.endOfStream.and.callFake(() => { - // Should have the first Period entirely buffered. - expect(mediaSourceEngine.initSegments).toEqual({ - audio: [false, true], - video: [false, true], - text: [], - }); - expect(mediaSourceEngine.segments).toEqual({ - audio: [true, true, true, true], - video: [true, true, true, true], - text: [true, true, true, true], - }); - - // Fake the audio buffer being removed. - mediaSourceEngine.segments[ContentType.AUDIO] = - [true, true, false, false]; - - // Seek back into the second Period. - expect(presentationTimeInSeconds).toBe(26); - presentationTimeInSeconds -= 5; - streamingEngine.seeked(); - - - mediaSourceEngine.endOfStream.and.returnValue(Promise.resolve()); - return Promise.resolve(); - }); - return defaultOnChooseStreams(period); + mediaSourceEngine.endOfStream.and.callFake(() => { + // Should have the first Period entirely buffered. + expect(mediaSourceEngine.initSegments).toEqual({ + audio: [false, true], + video: [false, true], + text: [], + }); + expect(mediaSourceEngine.segments).toEqual({ + audio: [true, true, true, true], + video: [true, true, true, true], + text: [true, true, true, true], }); - return defaultOnChooseStreams(period); - }); + // Fake the audio buffer being removed. + mediaSourceEngine.segments[ContentType.AUDIO] = + [true, true, false, false]; - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); + // Seek back into the second Period. + presentationTimeInSeconds -= 5; + expect(presentationTimeInSeconds).toBeGreaterThan(19); + streamingEngine.seeked(); + + mediaSourceEngine.endOfStream.and.returnValue(Promise.resolve()); + return Promise.resolve(); + }); // Here we go! - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); // When seeking within the same period, clear the buffer of the @@ -1266,41 +953,24 @@ describe('StreamingEngine', () => { }); }); - it('into buffered regions across Periods', async () => { - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[0]); - - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[1]); - - onChooseStreams.and.throwError(new Error()); - - // Switch to the second Period. - return defaultOnChooseStreams(period); - }); - - mediaSourceEngine.endOfStream.and.callFake(() => { - // Seek backwards to a buffered region in the first Period. Note - // that since the buffering goal is 5 seconds and each segment is - // 10 seconds long, the last segment should be required at 26 seconds. - // Then endOfStream() should be called. - expect(presentationTimeInSeconds).toBe(26); - presentationTimeInSeconds -= 20; - streamingEngine.seeked(); - - // Verify that buffers are not cleared. - expect(mediaSourceEngine.clear).not.toHaveBeenCalled(); + mediaSourceEngine.endOfStream.and.callFake(() => { + // Seek backwards to a buffered region in the first Period. + presentationTimeInSeconds -= 20; + expect(presentationTimeInSeconds).toBeLessThan(20); + streamingEngine.seeked(); - return Promise.resolve(); - }); + // Verify that buffers are not cleared. + expect(mediaSourceEngine.clear).not.toHaveBeenCalled(); - // Init the first Period. - return defaultOnChooseStreams(period); + return Promise.resolve(); }); // Here we go! - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); // Verify buffers. @@ -1317,24 +987,12 @@ describe('StreamingEngine', () => { }); it('into unbuffered regions', async () => { - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[0]); - - onChooseStreams.and.throwError(new Error()); - - // Init the first Period. - return defaultOnChooseStreams(period); - }); - - onStartupComplete.and.callFake(() => { - setupFakeGetTime(0); - - // Seek forward to an unbuffered region in the first Period. - expect(presentationTimeInSeconds).toBe(0); - presentationTimeInSeconds += 15; - streamingEngine.seeked(); + onTick.and.callFake(() => { + if (presentationTimeInSeconds == 6) { + // Note that since the buffering goal is 5 seconds and each segment is + // 10 seconds long, the second segment of this Period will be required + // at 6 seconds. - onChooseStreams.and.callFake((period) => { // Verify that all buffers have been cleared. expect(mediaSourceEngine.clear) .toHaveBeenCalledWith(ContentType.AUDIO); @@ -1343,9 +1001,8 @@ describe('StreamingEngine', () => { expect(mediaSourceEngine.clear) .toHaveBeenCalledWith(ContentType.TEXT); - expect(period).toBe(manifest.periods[1]); - - // Verify buffers. + // Verify buffers. The first segment is present because we start + // off-by-one after a seek. expect(mediaSourceEngine.initSegments).toEqual({ audio: [true, false], video: [true, false], @@ -1356,18 +1013,22 @@ describe('StreamingEngine', () => { video: [false, true, false, false], text: [false, true, false, false], }); - - onChooseStreams.and.throwError(new Error()); - - // Switch to the second Period. - return defaultOnChooseStreams(period); - }); + } }); // Here we go! - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; + + // Seek forward to an unbuffered region in the first Period. + expect(presentationTimeInSeconds).toBe(0); + presentationTimeInSeconds += 15; + streamingEngine.seeked(); await runTest(Util.spyFunc(onTick)); + // Verify buffers. expect(mediaSourceEngine.initSegments).toEqual({ audio: [false, true], @@ -1383,18 +1044,7 @@ describe('StreamingEngine', () => { it('into unbuffered regions across Periods', async () => { // Start from the second Period. - presentationTimeInSeconds = 20; - - onStartupComplete.and.callFake(() => setupFakeGetTime(20)); - - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[1]); - - onChooseStreams.and.throwError(new Error()); - - // Init the second Period. - return defaultOnChooseStreams(period); - }); + presentationTimeInSeconds = 25; mediaSourceEngine.endOfStream.and.callFake(() => { // Verify buffers. @@ -1409,12 +1059,9 @@ describe('StreamingEngine', () => { text: [false, false, true, true], }); - // Seek backwards to an unbuffered region in the first Period. Note - // that since the buffering goal is 5 seconds and each segment is 10 - // seconds long, the last segment should be required at 26 seconds. - // Then endOfStream() should be called. - expect(presentationTimeInSeconds).toBe(26); + // Seek backwards to an unbuffered region in the first Period. presentationTimeInSeconds -= 20; + expect(presentationTimeInSeconds).toBeLessThan(20); streamingEngine.seeked(); onTick.and.callFake(() => { @@ -1428,40 +1075,15 @@ describe('StreamingEngine', () => { onTick.and.stub(); }); - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[0]); - - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[1]); - - // Verify buffers. - expect(mediaSourceEngine.initSegments).toEqual({ - audio: [true, false], - video: [true, false], - text: [], - }); - expect(mediaSourceEngine.segments).toEqual({ - audio: [true, true, false, false], - video: [true, true, false, false], - text: [true, true, false, false], - }); - - onChooseStreams.and.throwError(new Error()); - - // Switch to the second Period. - return defaultOnChooseStreams(period); - }); - - mediaSourceEngine.endOfStream.and.returnValue(Promise.resolve()); - - // Switch to the first Period. - return defaultOnChooseStreams(period); - }); + mediaSourceEngine.endOfStream.and.returnValue(Promise.resolve()); return Promise.resolve(); }); // Here we go! - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(Util.spyFunc(onTick)); // Verify buffers. @@ -1470,65 +1092,39 @@ describe('StreamingEngine', () => { video: [false, true], text: [], }); - expect(mediaSourceEngine.segments).toEqual({ - audio: [true, true, true, true], - video: [true, true, true, true], - text: [true, true, true, true], - }); - }); - - it('into unbuffered regions when nothing is buffered', async () => { - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[0]); - - onChooseStreams.and.throwError(new Error()); - - // Init the first Period. - return defaultOnChooseStreams(period); - }); - - onInitialStreamsSetup.and.callFake(() => { - // Seek forward to an unbuffered region in the first Period. - expect(presentationTimeInSeconds).toBe(0); - presentationTimeInSeconds = 15; - streamingEngine.seeked(); - - onTick.and.callFake(() => { - // Nothing should have been cleared. - expect(mediaSourceEngine.clear).not.toHaveBeenCalled(); - onTick.and.stub(); - }); - - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[1]); - - // Verify buffers. - expect(mediaSourceEngine.initSegments).toEqual({ - audio: [true, false], - video: [true, false], - text: [], - }); - expect(mediaSourceEngine.segments).toEqual({ - audio: [false, true, false, false], - video: [false, true, false, false], - text: [false, true, false, false], - }); + expect(mediaSourceEngine.segments).toEqual({ + audio: [true, true, true, true], + video: [true, true, true, true], + text: [true, true, true, true], + }); + }); - onChooseStreams.and.throwError(new Error()); + it('into unbuffered regions when nothing is buffered', async () => { + // Here we go! + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; - // Switch to the second Period. - return defaultOnChooseStreams(period); - }); + // Nothing is buffered yet. + expect(mediaSourceEngine.segments).toEqual({ + audio: [false, false, false, false], + video: [false, false, false, false], + text: [false, false, false, false], }); - // This happens after onInitialStreamsSetup(), so pass 15 so the playhead - // resumes from 15. - onStartupComplete.and.callFake(() => setupFakeGetTime(15)); + // Seek forward to an unbuffered region in the first Period. + presentationTimeInSeconds = 15; + streamingEngine.seeked(); - // Here we go! - streamingEngine.start(); + onTick.and.callFake(() => { + // Nothing should have been cleared. + expect(mediaSourceEngine.clear).not.toHaveBeenCalled(); + onTick.and.stub(); + }); await runTest(Util.spyFunc(onTick)); + // Verify buffers. expect(mediaSourceEngine.initSegments).toEqual({ audio: [false, true], @@ -1543,29 +1139,24 @@ describe('StreamingEngine', () => { }); it('into unbuffered regions near segment start', async () => { - onChooseStreams.and.callFake(defaultOnChooseStreams); + // Here we go! + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; - onInitialStreamsSetup.and.callFake(() => { - // Seek forward to an unbuffered region in the first Period. - expect(presentationTimeInSeconds).toBe(0); - presentationTimeInSeconds = 11; - streamingEngine.seeked(); + // Seek forward to an unbuffered region in the first Period. + presentationTimeInSeconds = 11; + streamingEngine.seeked(); - onTick.and.callFake(() => { - // Nothing should have been cleared. - expect(mediaSourceEngine.clear).not.toHaveBeenCalled(); - onTick.and.stub(); - }); + onTick.and.callFake(() => { + // Nothing should have been cleared. + expect(mediaSourceEngine.clear).not.toHaveBeenCalled(); + onTick.and.stub(); }); - // This happens after onInitialStreamsSetup(), so pass 11 so the playhead - // resumes from 11. - onStartupComplete.and.callFake(() => setupFakeGetTime(11)); - - // Here we go! - streamingEngine.start(); - await runTest(Util.spyFunc(onTick)); + // Verify buffers. expect(mediaSourceEngine.initSegments).toEqual({ audio: [false, true], @@ -1586,25 +1177,18 @@ describe('StreamingEngine', () => { // Start from the second segment in the second Period. presentationTimeInSeconds = 30; - onStartupComplete.and.callFake(() => setupFakeGetTime(20)); - - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[1]); - - // Init the second Period. - return defaultOnChooseStreams(period); - }); - mediaSourceEngine.endOfStream.and.callFake(() => { // Seek backwards to an unbuffered region in the second Period. Do not // call seeked(). - expect(presentationTimeInSeconds).toBe(26); - presentationTimeInSeconds -= 10; + presentationTimeInSeconds = 20; return Promise.resolve(); }); // Here we go! - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); // Verify buffers. Segment 3 should not be buffered since we never @@ -1626,32 +1210,22 @@ describe('StreamingEngine', () => { // case where the playhead moves past the end of the buffer, which may // occur on some browsers depending on the playback rate. it('forward into unbuffered regions without seeked()', async () => { - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[0]); - - // Init the first Period. - return defaultOnChooseStreams(period); - }); - - onStartupComplete.and.callFake(() => { - setupFakeGetTime(0); - - // Seek forward to an unbuffered region in the first Period. Do not - // call seeked(). - presentationTimeInSeconds += 15; - - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[1]); + // Here we go! + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; - // Switch to the second Period. - return defaultOnChooseStreams(period); - }); + let seekStarted = false; + await runTest(() => { + if (!seekStarted) { + // Seek forward to an unbuffered region in the first Period. Do not + // call seeked(). + presentationTimeInSeconds += 15; + seekStarted = true; + } }); - // Here we go! - streamingEngine.start(); - - await runTest(); // Verify buffers. expect(mediaSourceEngine.initSegments).toEqual({ audio: [false, true], @@ -1667,61 +1241,47 @@ describe('StreamingEngine', () => { it('into partially buffered regions across periods', async () => { // Seeking into a region where some buffers (text) are buffered and some - // are not should work despite the media states requiring different - // periods. - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[0]); - - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[1]); - - // Should get another call for the unbuffered Period transition. - onChooseStreams.and.callFake(defaultOnChooseStreams); - - mediaSourceEngine.endOfStream.and.callFake(() => { - // Should have the first Period entirely buffered. - expect(mediaSourceEngine.initSegments).toEqual({ - audio: [false, true], - video: [false, true], - text: [], - }); - expect(mediaSourceEngine.segments).toEqual({ - audio: [true, true, true, true], - video: [true, true, true, true], - text: [true, true, true, true], - }); - - // Fake the audio/video buffers being removed. - mediaSourceEngine.segments[ContentType.AUDIO] = - [false, false, true, true]; - mediaSourceEngine.segments[ContentType.VIDEO] = - [false, false, true, true]; - - // Seek back into the first Period. - expect(presentationTimeInSeconds).toBe(26); - presentationTimeInSeconds -= 20; - streamingEngine.seeked(); - - // When seeking across periods, if at least one stream is - // unbuffered, we clear all the buffers. - expect(mediaSourceEngine.clear).toHaveBeenCalledWith('audio'); - expect(mediaSourceEngine.clear).toHaveBeenCalledWith('video'); - expect(mediaSourceEngine.clear).toHaveBeenCalledWith('text'); - - mediaSourceEngine.endOfStream.and.returnValue(Promise.resolve()); - return Promise.resolve(); - }); + // are not should work. - return defaultOnChooseStreams(period); + mediaSourceEngine.endOfStream.and.callFake(() => { + // Should have the first Period entirely buffered. + expect(mediaSourceEngine.initSegments).toEqual({ + audio: [false, true], + video: [false, true], + text: [], + }); + expect(mediaSourceEngine.segments).toEqual({ + audio: [true, true, true, true], + video: [true, true, true, true], + text: [true, true, true, true], }); - return defaultOnChooseStreams(period); - }); + // Fake the audio/video buffers being removed. + // Now only text is buffered from the first period. + mediaSourceEngine.segments[ContentType.AUDIO] = + [false, false, true, true]; + mediaSourceEngine.segments[ContentType.VIDEO] = + [false, false, true, true]; + + // Seek back into the first Period. + presentationTimeInSeconds -= 20; + expect(presentationTimeInSeconds).toBeLessThan(29); + streamingEngine.seeked(); + + // Only the unbuffered streams were cleared. + expect(mediaSourceEngine.clear).toHaveBeenCalledWith('audio'); + expect(mediaSourceEngine.clear).toHaveBeenCalledWith('video'); + expect(mediaSourceEngine.clear).not.toHaveBeenCalledWith('text'); - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); + mediaSourceEngine.endOfStream.and.returnValue(Promise.resolve()); + return Promise.resolve(); + }); // Here we go! - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); // Verify buffers. @@ -1743,8 +1303,7 @@ describe('StreamingEngine', () => { setupLive(); mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData, 0); createStreamingEngine(); - - onStartupComplete.and.callFake(() => setupFakeGetTime(100)); + presentationTimeInSeconds = 100; }); it('outside segment availability window', async () => { @@ -1753,59 +1312,42 @@ describe('StreamingEngine', () => { presentationTimeInSeconds = 90; - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[0]); - - onChooseStreams.and.throwError(new Error()); - - // Init the first Period. - return defaultOnChooseStreams(period); - }); - - onStartupComplete.and.callFake(() => { - // Seek forward to an unbuffered and unavailable region in the second - // Period; set playing to false since the playhead can't move at the - // seek target. - expect(timeline.getSegmentAvailabilityEnd()).toBeLessThan(125); - presentationTimeInSeconds = 125; - playing = false; - streamingEngine.seeked(); - - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[1]); - - onChooseStreams.and.throwError(new Error()); + // Here we go! + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; + + // Seek forward to an unbuffered and unavailable region in the second + // Period; set playing to false since the playhead can't move at the + // seek target. + expect(timeline.getSegmentAvailabilityEnd()).toBeLessThan(125); + presentationTimeInSeconds = 125; + playing = false; + streamingEngine.seeked(); - // Switch to the second Period. - return defaultOnChooseStreams(period); - }); + // Eventually StreamingEngine should request the first segment (since + // it needs the second segment) of the second Period when it becomes + // available. + const originalAppendBuffer = + // eslint-disable-next-line no-restricted-syntax + shaka.test.FakeMediaSourceEngine.prototype.appendBufferImpl; + mediaSourceEngine.appendBuffer.and.callFake( + (type, data, startTime, endTime) => { + expect(presentationTimeInSeconds).toBe(125); + if (startTime >= 100) { + // Ignore a possible call for the first Period. + expect(timeline.getSegmentAvailabilityStart()).toBe(100); + expect(timeline.getSegmentAvailabilityEnd()).toBe(120); + playing = true; + mediaSourceEngine.appendBuffer.and.callFake( + originalAppendBuffer); + } - // Eventually StreamingEngine should request the first segment (since - // it needs the second segment) of the second Period when it becomes - // available. - const originalAppendBuffer = // eslint-disable-next-line no-restricted-syntax - shaka.test.FakeMediaSourceEngine.prototype.appendBufferImpl; - mediaSourceEngine.appendBuffer.and.callFake( - (type, data, startTime, endTime) => { - expect(presentationTimeInSeconds).toBe(125); - if (startTime >= 100) { - // Ignore a possible call for the first Period. - expect(timeline.getSegmentAvailabilityStart()).toBe(100); - expect(timeline.getSegmentAvailabilityEnd()).toBe(120); - playing = true; - mediaSourceEngine.appendBuffer.and.callFake( - originalAppendBuffer); - } - - // eslint-disable-next-line no-restricted-syntax - return originalAppendBuffer.call( - mediaSourceEngine, type, data, startTime, endTime); - }); - }); - - // Here we go! - streamingEngine.start(); + return originalAppendBuffer.call( + mediaSourceEngine, type, data, startTime, endTime); + }); await runTest(slideSegmentAvailabilityWindow); // Verify buffers. @@ -1835,20 +1377,21 @@ describe('StreamingEngine', () => { it('from Stream setup', async () => { // Don't use returnValue with Promise.reject, or it may be detected as an // unhandled Promise rejection. - videoStream1.createSegmentIndex.and.callFake( + videoStream.createSegmentIndex.and.callFake( () => Promise.reject('FAKE_ERROR')); onError.and.callFake((error) => { expect(error).toBe('FAKE_ERROR'); }); - onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p)); - // Here we go! - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); - expect(videoStream1.createSegmentIndex).toHaveBeenCalled(); + expect(videoStream.createSegmentIndex).toHaveBeenCalled(); expect(onError).toHaveBeenCalled(); }); @@ -1860,28 +1403,25 @@ describe('StreamingEngine', () => { // Don't use returnValue with Promise.reject, or it may be detected as an // unhandled Promise rejection. - alternateVideoStream1.createSegmentIndex.and.callFake( + alternateVideoStream.createSegmentIndex.and.callFake( () => Promise.reject(expectedError)); onError.and.callFake((error) => { - expect(onInitialStreamsSetup).toHaveBeenCalled(); - expect(onStartupComplete).toHaveBeenCalled(); expect(error).toBe(expectedError); }); - onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p)); - onCanSwitch.and.callFake(() => { - streamingEngine.switchVariant( - alternateVariant1, /* clear_buffer= */ true, /* safe_margin= */ 0); - }); - // Here we go! - streamingEngine.start().catch(fail); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; + + streamingEngine.switchVariant( + alternateVariant, /* clear_buffer= */ true, /* safe_margin= */ 0); + await runTest(); - expect(videoStream1.createSegmentIndex).toHaveBeenCalled(); - expect(onCanSwitch).toHaveBeenCalled(); - expect(alternateVideoStream1.createSegmentIndex).toHaveBeenCalled(); + expect(alternateVideoStream.createSegmentIndex).toHaveBeenCalled(); expect(onError).toHaveBeenCalled(); }); @@ -1892,36 +1432,29 @@ describe('StreamingEngine', () => { shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_FAILED); onError.and.callFake((error) => { - expect(onInitialStreamsSetup).toHaveBeenCalled(); - expect(onStartupComplete).not.toHaveBeenCalled(); Util.expectToEqualError(error, expectedError); }); - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[0]); - - const streamsByType = defaultOnChooseStreams(period); - - const originalAppendBuffer = - // eslint-disable-next-line no-restricted-syntax - shaka.test.FakeMediaSourceEngine.prototype.appendBufferImpl; - mediaSourceEngine.appendBuffer.and.callFake( - (type, data, startTime, endTime) => { - // Reject the first video init segment. - if (data == segmentData[ContentType.VIDEO].initSegments[0]) { - return Promise.reject(expectedError); - } else { - // eslint-disable-next-line no-restricted-syntax - return originalAppendBuffer.call( - mediaSourceEngine, type, data, startTime, endTime); - } - }); - - return streamsByType; - }); + const originalAppendBuffer = + // eslint-disable-next-line no-restricted-syntax + shaka.test.FakeMediaSourceEngine.prototype.appendBufferImpl; + mediaSourceEngine.appendBuffer.and.callFake( + (type, data, startTime, endTime) => { + // Reject the first video init segment. + if (data == segmentData[ContentType.VIDEO].initSegments[0]) { + return Promise.reject(expectedError); + } else { + // eslint-disable-next-line no-restricted-syntax + return originalAppendBuffer.call( + mediaSourceEngine, type, data, startTime, endTime); + } + }); // Here we go! - streamingEngine.start().catch(fail); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); expect(onError).toHaveBeenCalled(); }); @@ -1933,36 +1466,29 @@ describe('StreamingEngine', () => { shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_FAILED); onError.and.callFake((error) => { - expect(onInitialStreamsSetup).toHaveBeenCalled(); - expect(onStartupComplete).not.toHaveBeenCalled(); Util.expectToEqualError(error, expectedError); }); - onChooseStreams.and.callFake((period) => { - expect(period).toBe(manifest.periods[0]); - - const streamsByType = defaultOnChooseStreams(period); - - const originalAppendBuffer = - // eslint-disable-next-line no-restricted-syntax - shaka.test.FakeMediaSourceEngine.prototype.appendBufferImpl; - mediaSourceEngine.appendBuffer.and.callFake( - (type, data, startTime, endTime) => { - // Reject the first audio segment. - if (data == segmentData[ContentType.AUDIO].segments[0]) { - return Promise.reject(expectedError); - } else { - // eslint-disable-next-line no-restricted-syntax - return originalAppendBuffer.call( - mediaSourceEngine, type, data, startTime, endTime); - } - }); - - return streamsByType; - }); + const originalAppendBuffer = + // eslint-disable-next-line no-restricted-syntax + shaka.test.FakeMediaSourceEngine.prototype.appendBufferImpl; + mediaSourceEngine.appendBuffer.and.callFake( + (type, data, startTime, endTime) => { + // Reject the first audio segment. + if (data == segmentData[ContentType.AUDIO].segments[0]) { + return Promise.reject(expectedError); + } else { + // eslint-disable-next-line no-restricted-syntax + return originalAppendBuffer.call( + mediaSourceEngine, type, data, startTime, endTime); + } + }); // Here we go! - streamingEngine.start().catch(fail); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); expect(onError).toHaveBeenCalled(); }); @@ -1975,14 +1501,16 @@ describe('StreamingEngine', () => { mediaSourceEngine.clear.and.returnValue(Promise.reject(expectedError)); onError.and.stub(); - onChooseStreams.and.callFake((period) => defaultOnChooseStreams(period)); - onStartupComplete.and.callFake(() => { - streamingEngine.switchVariant( - variant2, /* clear_buffer= */ true, /* safe_margin= */ 0); - }); // Here we go! - streamingEngine.start().catch(fail); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; + + streamingEngine.switchVariant( + alternateVariant, /* clear_buffer= */ true, /* safe_margin= */ 0); + await runTest(); expect(onError).toHaveBeenCalledWith(Util.jasmineError(expectedError)); }); @@ -1991,7 +1519,7 @@ describe('StreamingEngine', () => { describe('handles network errors', () => { it('ignores text stream failures if configured to', async () => { setupVod(); - const textUri = '1_text_1'; + const textUri = '0_text_0'; const originalNetEngine = netEngine; netEngine = { request: jasmine.createSpy('request'), @@ -2010,13 +1538,11 @@ describe('StreamingEngine', () => { config.ignoreTextStreamFailures = true; createStreamingEngine(config); - onStartupComplete.and.callFake(() => { - setupFakeGetTime(0); - }); - // Here we go! - onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p)); - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); expect(onError).not.toHaveBeenCalled(); @@ -2027,7 +1553,7 @@ describe('StreamingEngine', () => { setupLive(); // Wrap the NetworkingEngine to cause errors. - const targetUri = '1_audio_init'; + const targetUri = '0_audio_init'; failFirstRequestForTarget(netEngine, targetUri, shaka.util.Error.Code.BAD_HTTP_STATUS); @@ -2038,9 +1564,6 @@ describe('StreamingEngine', () => { createStreamingEngine(config); presentationTimeInSeconds = 100; - onStartupComplete.and.callFake(() => { - setupFakeGetTime(100); - }); onError.and.callFake((error) => { expect(error.severity).toBe(shaka.util.Error.Severity.CRITICAL); @@ -2049,8 +1572,10 @@ describe('StreamingEngine', () => { }); // Here we go! - onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p)); - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); expect(onError).toHaveBeenCalledTimes(1); @@ -2062,7 +1587,7 @@ describe('StreamingEngine', () => { setupLive(); // Wrap the NetworkingEngine to cause errors. - const targetUri = '1_audio_init'; + const targetUri = '0_audio_init'; failFirstRequestForTarget(netEngine, targetUri, shaka.util.Error.Code.BAD_HTTP_STATUS); @@ -2073,9 +1598,6 @@ describe('StreamingEngine', () => { createStreamingEngine(config); presentationTimeInSeconds = 100; - onStartupComplete.and.callFake(() => { - setupFakeGetTime(100); - }); onError.and.callFake((error) => { expect(error.severity).toBe(shaka.util.Error.Severity.CRITICAL); @@ -2084,8 +1606,10 @@ describe('StreamingEngine', () => { }); // Here we go! - onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p)); - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); expect(onError).toHaveBeenCalledTimes(1); @@ -2097,7 +1621,7 @@ describe('StreamingEngine', () => { setupLive(); // Wrap the NetworkingEngine to cause errors. - const targetUri = '1_audio_init'; + const targetUri = '0_audio_init'; failFirstRequestForTarget(netEngine, targetUri, shaka.util.Error.Code.BAD_HTTP_STATUS); @@ -2110,17 +1634,16 @@ describe('StreamingEngine', () => { createStreamingEngine(config); presentationTimeInSeconds = 100; - onStartupComplete.and.callFake(() => { - setupFakeGetTime(100); - }); onError.and.callFake((error) => { error.handled = true; }); // Here we go! - onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p)); - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); expect(onError).toHaveBeenCalledTimes(1); @@ -2131,7 +1654,7 @@ describe('StreamingEngine', () => { setupLive(); // Wrap the NetworkingEngine to cause errors. - const targetUri = '1_audio_init'; + const targetUri = '0_audio_init'; failFirstRequestForTarget(netEngine, targetUri, shaka.util.Error.Code.BAD_HTTP_STATUS); @@ -2153,14 +1676,13 @@ describe('StreamingEngine', () => { createStreamingEngine(config); presentationTimeInSeconds = 100; - onStartupComplete.and.callFake(() => { - setupFakeGetTime(100); - }); onError.and.stub(); // Here we go! - onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p)); - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; const startTime = Date.now(); await runTest(); @@ -2175,7 +1697,7 @@ describe('StreamingEngine', () => { setupVod(); // Wrap the NetworkingEngine to cause errors. - const targetUri = '1_audio_init'; + const targetUri = '0_audio_init'; const originalNetEngineRequest = netEngine.request; failFirstRequestForTarget(netEngine, targetUri, shaka.util.Error.Code.BAD_HTTP_STATUS); @@ -2183,10 +1705,6 @@ describe('StreamingEngine', () => { mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); createStreamingEngine(); - onStartupComplete.and.callFake(() => { - setupFakeGetTime(0); - }); - onError.and.callFake((error) => { // Restore the original fake request function. netEngine.request = originalNetEngineRequest; @@ -2197,8 +1715,10 @@ describe('StreamingEngine', () => { }); // Here we go! - onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p)); - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); // We definitely called onError(). @@ -2216,22 +1736,6 @@ describe('StreamingEngine', () => { mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); createStreamingEngine(); - onStartupComplete.and.callFake(() => { - setupFakeGetTime(0); - - // Now that setup is complete, throw QuotaExceededError on every segment - // to quickly trigger the quota error. - const appendBufferSpy = jasmine.createSpy('appendBuffer'); - appendBufferSpy.and.callFake((type, data, startTime, endTime) => { - throw new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MEDIA, - shaka.util.Error.Code.QUOTA_EXCEEDED_ERROR, - type); - }); - mediaSourceEngine.appendBuffer = appendBufferSpy; - }); - onError.and.callFake((error) => { expect(error.code).toBe(shaka.util.Error.Code.QUOTA_EXCEEDED_ERROR); @@ -2241,10 +1745,26 @@ describe('StreamingEngine', () => { }); // Here we go! - onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p)); - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; - await runTest(); + await runTest(() => { + if (presentationTimeInSeconds == 2) { + // Now that we're streaming, throw QuotaExceededError on every segment + // to quickly trigger the quota error. + const appendBufferSpy = jasmine.createSpy('appendBuffer'); + appendBufferSpy.and.callFake((type, data, startTime, endTime) => { + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.MEDIA, + shaka.util.Error.Code.QUOTA_EXCEEDED_ERROR, + type); + }); + mediaSourceEngine.appendBuffer = appendBufferSpy; + } + }); // We definitely called onError(). expect(onError).toHaveBeenCalledTimes(1); @@ -2261,17 +1781,14 @@ describe('StreamingEngine', () => { mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); createStreamingEngine(); - onStartupComplete.and.callFake(() => { - setupFakeGetTime(0); - }); - - onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p)); - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; // Here we go! - let count = 0; await runTest(() => { - if (++count == 3) { + if (presentationTimeInSeconds == 3) { streamingEngine.destroy(); // Retry streaming, which should fail and return false. @@ -2306,8 +1823,6 @@ describe('StreamingEngine', () => { mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); createStreamingEngine(config); - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); - const originalRemove = // eslint-disable-next-line no-restricted-syntax shaka.test.FakeMediaSourceEngine.prototype.removeImpl @@ -2331,8 +1846,10 @@ describe('StreamingEngine', () => { }); // Here we go! - onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p)); - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; // Since StreamingEngine is free to peform audio, video, and text updates // in any order, there are many valid ways in which StreamingEngine can @@ -2377,9 +1894,10 @@ describe('StreamingEngine', () => { // Create StreamingEngine. mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); createStreamingEngine(config); - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); - onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p)); - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(() => { if (presentationTimeInSeconds == 8) { @@ -2414,8 +1932,6 @@ describe('StreamingEngine', () => { mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); createStreamingEngine(config); - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); - const originalAppendBuffer = // eslint-disable-next-line no-restricted-syntax shaka.test.FakeMediaSourceEngine.prototype.appendBufferImpl; @@ -2444,8 +1960,10 @@ describe('StreamingEngine', () => { }); // Here we go! - onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p)); - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); expect(mediaSourceEngine.endOfStream).toHaveBeenCalled(); @@ -2476,8 +1994,6 @@ describe('StreamingEngine', () => { mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); createStreamingEngine(config); - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); - const originalAppendBuffer = // eslint-disable-next-line no-restricted-syntax shaka.test.FakeMediaSourceEngine.prototype.appendBufferImpl; @@ -2510,8 +2026,10 @@ describe('StreamingEngine', () => { }); // Here we go! - onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p)); - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; // Stop the playhead after 10 seconds since will not append any // segments after this time. @@ -2538,11 +2056,11 @@ describe('StreamingEngine', () => { new shaka.test.FakeMediaSourceEngine(segmentData, drift); createStreamingEngine(); - onStartupComplete.and.callFake(() => setupFakeGetTime(drift)); - // Here we go! - onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p)); - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); expect(mediaSourceEngine.endOfStream).toHaveBeenCalled(); @@ -2568,11 +2086,11 @@ describe('StreamingEngine', () => { new shaka.test.FakeMediaSourceEngine(segmentData, drift); createStreamingEngine(); - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); - // Here we go! - onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p)); - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); expect(mediaSourceEngine.endOfStream).toHaveBeenCalled(); @@ -2610,11 +2128,11 @@ describe('StreamingEngine', () => { presentationTimeInSeconds = 100; - onStartupComplete.and.callFake(() => setupFakeGetTime(100)); - // Here we go! - onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p)); - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(slideSegmentAvailabilityWindow); expect(mediaSourceEngine.endOfStream).toHaveBeenCalled(); @@ -2652,11 +2170,11 @@ describe('StreamingEngine', () => { config.bufferingGoal = 1; createStreamingEngine(config); - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); - // Here we go! - onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p)); - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(() => { if (presentationTimeInSeconds == 1) { @@ -2760,17 +2278,17 @@ describe('StreamingEngine', () => { setupVod(); mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); createStreamingEngine(); - - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); - onChooseStreams.and.callFake((p) => defaultOnChooseStreams(p)); }); it('raises an event for registered embedded emsg boxes', async () => { segmentData[ContentType.VIDEO].segments[0] = emsgSegment; - videoStream1.emsgSchemeIdUris = [emsgObj.schemeIdUri]; + videoStream.emsgSchemeIdUris = [emsgObj.schemeIdUri]; // Here we go! - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); expect(onEvent).toHaveBeenCalledTimes(1); @@ -2784,10 +2302,13 @@ describe('StreamingEngine', () => { shaka.util.Uint8ArrayUtils.fromHex('0000000c6672656501020304'); segmentData[ContentType.VIDEO].segments[0] = shaka.util.Uint8ArrayUtils.concat(emsgSegment, dummyBox, emsgSegment); - videoStream1.emsgSchemeIdUris = [emsgObj.schemeIdUri]; + videoStream.emsgSchemeIdUris = [emsgObj.schemeIdUri]; // Here we go! - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); expect(onEvent).toHaveBeenCalledTimes(2); @@ -2797,17 +2318,23 @@ describe('StreamingEngine', () => { segmentData[ContentType.VIDEO].segments[0] = emsgSegment; // Here we go! - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); expect(onEvent).not.toHaveBeenCalled(); }); it('won\'t raise an event when no emsg boxes present', async () => { - videoStream1.emsgSchemeIdUris = [emsgObj.schemeIdUri]; + videoStream.emsgSchemeIdUris = [emsgObj.schemeIdUri]; // Here we go! - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); expect(onEvent).not.toHaveBeenCalled(); @@ -2817,7 +2344,10 @@ describe('StreamingEngine', () => { segmentData[ContentType.VIDEO].segments[0] = emsgSegment; // Here we go! - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); expect(onEvent).not.toHaveBeenCalled(); @@ -2833,10 +2363,13 @@ describe('StreamingEngine', () => { '6d7065673a646173683a6576656e743a' + '32303132000000000031000000080000' + '00ff0000000c74657374'); - videoStream1.emsgSchemeIdUris = ['urn:mpeg:dash:event:2012']; + videoStream.emsgSchemeIdUris = ['urn:mpeg:dash:event:2012']; // Here we go! - streamingEngine.start(); + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; await runTest(); expect(onEvent).not.toHaveBeenCalled(); @@ -2845,6 +2378,8 @@ describe('StreamingEngine', () => { }); describe('network downgrading', () => { + /** @type {shaka.extern.Variant} */ + let initialVariant; /** @type {shaka.extern.Variant} */ let newVariant; /** @type {!Array.} */ @@ -2859,28 +2394,26 @@ describe('StreamingEngine', () => { beforeEach(() => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.presentationTimeline.setDuration(60); - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.bandwidth = 500; - variant.addVideo(10, (stream) => { - stream.useSegmentTemplate( - 'video-10-%d.mp4', /* segmentDuration= */ 10, - /* segmentSize= */ 50); - }); + manifest.addVariant(0, (variant) => { + variant.bandwidth = 500; + variant.addVideo(10, (stream) => { + stream.useSegmentTemplate( + 'video-10-%d.mp4', /* segmentDuration= */ 10, + /* segmentSize= */ 50); }); - period.addVariant(1, (variant) => { - variant.bandwidth = 100; - variant.addVideo(11, (stream) => { - stream.useSegmentTemplate( - 'video-11-%d.mp4', /* segmentDuration= */ 10, - /* segmentSize= */ 10); - }); + }); + manifest.addVariant(1, (variant) => { + variant.bandwidth = 100; + variant.addVideo(11, (stream) => { + stream.useSegmentTemplate( + 'video-11-%d.mp4', /* segmentDuration= */ 10, + /* segmentSize= */ 10); }); }); }); - const initialVariant = manifest.periods[0].variants[0]; - newVariant = manifest.periods[0].variants[1]; + initialVariant = manifest.variants[0]; + newVariant = manifest.variants[1]; requestUris = []; delayedRequests = []; lastPendingRequest = null; @@ -2955,9 +2488,6 @@ describe('StreamingEngine', () => { createStreamingEngine(config); getBandwidthEstimate.and.returnValue(1); // very slow by default - - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); - onChooseStreams.and.callFake(() => ({variant: initialVariant})); }); it('aborts pending requests', async () => { @@ -2978,7 +2508,7 @@ describe('StreamingEngine', () => { it('still aborts if previous segment size unknown', async () => { // This should use the "bytes remaining" from the request instead of the // previous stream's size. - const videoStream = manifest.periods[0].variants[0].video; + const videoStream = manifest.variants[0].video; await videoStream.createSegmentIndex(); const segmentIndex = videoStream.segmentIndex; const oldGet = segmentIndex.get; @@ -3075,8 +2605,7 @@ describe('StreamingEngine', () => { }; // This should abort the pending request for the second segment. - streamingEngine.switchVariant( - newVariant, /* clear_buffer= */ false, /* safe_margin= */ 0); + streamingEngine.switchVariant(newVariant); await bufferAndCheck(/* didAbort= */ true); @@ -3084,7 +2613,7 @@ describe('StreamingEngine', () => { }); it('still aborts if new segment size unknown', async () => { - const videoStream = manifest.periods[0].variants[1].video; + const videoStream = manifest.variants[1].video; videoStream.bandwidth = 10; await videoStream.createSegmentIndex(); const segmentIndex = videoStream.segmentIndex; @@ -3124,7 +2653,9 @@ describe('StreamingEngine', () => { * it should be waiting for the second segment request to complete. */ async function prepareForAbort() { + streamingEngine.switchVariant(initialVariant); streamingEngine.start().catch(fail); + playing = true; await Util.fakeEventLoop(1); // Finish the first segment request. @@ -3132,8 +2663,7 @@ describe('StreamingEngine', () => { flushDelayedRequests(); await Util.fakeEventLoop(10); - // We should have buffered the first segment and finished startup. - expect(onCanSwitch).toHaveBeenCalled(); + // We should have buffered the first segment. expect(Util.invokeSpy(mediaSourceEngine.bufferEnd, 'video')).toBe(10); // Confirm that the first segment is buffered. @@ -3182,37 +2712,38 @@ describe('StreamingEngine', () => { }); describe('embedded text tracks', () => { + /** @type {!jasmine.Spy} */ + let onTick; + + /** @type {shaka.extern.Stream} */ + let externalTextStream; + + /** @type {shaka.extern.Stream} */ + let embeddedTextStream; + beforeEach(() => { - // Set up a manifest with multiple Periods and text streams. manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(110, (stream) => { - stream.useSegmentTemplate('video-110-%d.mp4', 10); - }); - }); - period.addTextStream(120, (stream) => { - stream.useSegmentTemplate('video-120-%d.mp4', 10); - }); - period.addTextStream(121, (stream) => { - stream.mimeType = shaka.util.MimeUtils.CLOSED_CAPTION_MIMETYPE; + manifest.addVariant(0, (variant) => { + variant.addVideo(1, (stream) => { + stream.useSegmentTemplate('video-110-%d.mp4', 10); }); }); - manifest.addPeriod(10, (period) => { - period.addVariant(1, (variant) => { - variant.addVideo(210, (stream) => { - stream.useSegmentTemplate('video-210-%d.mp4', 10); - }); - }); - period.addTextStream(220, (stream) => { - stream.useSegmentTemplate('text-220-%d.mp4', 10); - }); - period.addTextStream(221, (stream) => { - stream.mimeType = shaka.util.MimeUtils.CLOSED_CAPTION_MIMETYPE; - }); + manifest.addTextStream(2, (stream) => { + stream.useSegmentTemplate('video-120-%d.mp4', 10); + }); + manifest.addTextStream(3, (stream) => { + stream.mimeType = shaka.util.MimeUtils.CLOSED_CAPTION_MIMETYPE; }); }); + // Capture the stream objects from the generated manifest by ID. + externalTextStream = manifest.textStreams.filter((s) => s.id == 2)[0]; + goog.asserts.assert( + externalTextStream, 'Should have found external text!'); + embeddedTextStream = manifest.textStreams.filter((s) => s.id == 3)[0]; + goog.asserts.assert( + embeddedTextStream, 'Should have found embedded text!'); + // For these tests, we don't care about specific data appended. // Just return any old ArrayBuffer for any requested segment. netEngine = { @@ -3260,181 +2791,129 @@ describe('StreamingEngine', () => { presentationTimeInSeconds = 0; createStreamingEngine(config); - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); - }); + // Each test will switch text streams to simulate various activities, but + // all will use this variant. + streamingEngine.switchVariant(manifest.variants[0]); - describe('period transition', () => { - it('initializes new embedded captions', async () => { - onChooseStreams.and.callFake((period) => { - if (period == manifest.periods[0]) { - return {variant: period.variants[0]}; - } else { - return {variant: period.variants[0], text: period.textStreams[1]}; - } - }); - await runEmbeddedCaptionTest(); - }); + onTick = jasmine.createSpy('onTick'); + }); - it('initializes embedded captions from external text', async () => { - onChooseStreams.and.callFake((period) => { - if (period == manifest.periods[0]) { - return {variant: period.variants[0], text: period.textStreams[0]}; - } else { - return {variant: period.variants[0], text: period.textStreams[1]}; - } - }); - await runEmbeddedCaptionTest(); - }); + it('initializes embedded captions after nothing', async () => { + // Start without text. + streamingEngine.start().catch(fail); + playing = true; - it('switches to external text after embedded captions', async () => { - onChooseStreams.and.callFake((period) => { - if (period == manifest.periods[0]) { - return {variant: period.variants[0], text: period.textStreams[1]}; - } else { - return {variant: period.variants[0], text: period.textStreams[0]}; - } - }); - await runEmbeddedCaptionTest(); - }); + onTick.and.callFake(() => { + // Switch to embedded text. + streamingEngine.switchTextStream(embeddedTextStream); - it('doesn\'t re-initialize', async () => { - onChooseStreams.and.callFake((period) => { - return {variant: period.variants[0], text: period.textStreams[1]}; - }); - await runEmbeddedCaptionTest(); + onTick.and.stub(); }); - async function runEmbeddedCaptionTest() { - streamingEngine.start().catch(fail); - await Util.fakeEventLoop(10); + await Util.fakeEventLoop(10, (time) => Util.invokeSpy(onTick)); - // We have buffered through the Period transition. - expect(onChooseStreams).toHaveBeenCalledTimes(2); - expect(Util.invokeSpy(mediaSourceEngine.bufferEnd, 'video')) - .toBeGreaterThan(12); + // We have buffered through the Period transition. + expect(Util.invokeSpy(mediaSourceEngine.bufferEnd, 'video')) + .toBeGreaterThan(12); - expect(mediaSourceEngine.setSelectedClosedCaptionId) - .toHaveBeenCalledTimes(1); - } + expect(mediaSourceEngine.setSelectedClosedCaptionId) + .toHaveBeenCalledTimes(1); }); - }); - - it('calls createSegmentIndex on demand', async () => { - setupVod(); - mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); - createStreamingEngine(); - onStartupComplete.and.callFake(() => setupFakeGetTime(0)); - onChooseStreams.and.callFake((period) => defaultOnChooseStreams(period)); + it('initializes embedded captions after external text', async () => { + // Start with external text. + streamingEngine.switchTextStream(externalTextStream); + streamingEngine.start().catch(fail); + playing = true; - // None of the first period streams have been set up yet because we haven't - // started yet. - expect(audioStream1.createSegmentIndex).not.toHaveBeenCalled(); - expect(videoStream1.createSegmentIndex).not.toHaveBeenCalled(); - expect(alternateVideoStream1.createSegmentIndex).not.toHaveBeenCalled(); + onTick.and.callFake(() => { + // Switch to embedded text. + streamingEngine.switchTextStream(embeddedTextStream); - // None of the second period streams have been set up yet, either. - expect(variant2.video.createSegmentIndex).not.toHaveBeenCalled(); - expect(variant2.audio.createSegmentIndex).not.toHaveBeenCalled(); + onTick.and.stub(); + }); - onInitialStreamsSetup.and.callFake(() => { - // Once we're streaming, the first period audio & video streams have been - // set up. - expect(audioStream1.createSegmentIndex).toHaveBeenCalled(); - expect(videoStream1.createSegmentIndex).toHaveBeenCalled(); + await Util.fakeEventLoop(10, (time) => Util.invokeSpy(onTick)); - // But not this alternate video stream from the first period. - expect(alternateVideoStream1.createSegmentIndex).not.toHaveBeenCalled(); + // We have buffered through the Period transition. + expect(Util.invokeSpy(mediaSourceEngine.bufferEnd, 'video')) + .toBeGreaterThan(12); - // And not the streams from the second period. - expect(variant2.video.createSegmentIndex).not.toHaveBeenCalled(); - expect(variant2.audio.createSegmentIndex).not.toHaveBeenCalled(); + expect(mediaSourceEngine.setSelectedClosedCaptionId) + .toHaveBeenCalledTimes(1); }); - // Here we go! - streamingEngine.start(); + it('switches to external text after embedded captions', async () => { + // Start with embedded text. + streamingEngine.switchTextStream(embeddedTextStream); + streamingEngine.start().catch(fail); + playing = true; - await runTest(); + onTick.and.callFake(() => { + // Switch to external text. + streamingEngine.switchTextStream(externalTextStream); - // Because we never switched to this stream, it was never set up at any time - // during this simulated playback. - expect(alternateVideoStream1.createSegmentIndex).not.toHaveBeenCalled(); - }); + onTick.and.stub(); + }); - /** - * Verifies calls to NetworkingEngine.request(). Expects every segment - * in the given Period to have been requested. - * - * @param {number} period The Period number (one-based). - */ - function verifyNetworkingEngineRequestCalls(period) { - netEngine.expectRangeRequest( - period + '_audio_init', - initSegmentRanges[ContentType.AUDIO][0], - initSegmentRanges[ContentType.AUDIO][1]); + await Util.fakeEventLoop(10, (time) => Util.invokeSpy(onTick)); - netEngine.expectRangeRequest( - period + '_video_init', - initSegmentRanges[ContentType.VIDEO][0], - initSegmentRanges[ContentType.VIDEO][1]); + // We have buffered through the Period transition. + expect(Util.invokeSpy(mediaSourceEngine.bufferEnd, 'video')) + .toBeGreaterThan(12); - const segmentType = shaka.net.NetworkingEngine.RequestType.SEGMENT; - netEngine.expectRequest(period + '_audio_1', segmentType); - netEngine.expectRequest(period + '_video_1', segmentType); - netEngine.expectRequest(period + '_text_1', segmentType); + expect(mediaSourceEngine.setSelectedClosedCaptionId) + .toHaveBeenCalledTimes(1); + }); + + it('plays embedded text throughout', async () => { + // Start with embedded text. + streamingEngine.switchTextStream(embeddedTextStream); + streamingEngine.start().catch(fail); + playing = true; - netEngine.expectRequest(period + '_audio_2', segmentType); - netEngine.expectRequest(period + '_video_2', segmentType); - netEngine.expectRequest(period + '_text_2', segmentType); + await Util.fakeEventLoop(10, (time) => Util.invokeSpy(onTick)); - netEngine.request.calls.reset(); - } + // We have buffered through the Period transition. + expect(Util.invokeSpy(mediaSourceEngine.bufferEnd, 'video')) + .toBeGreaterThan(12); - /** - * Choose streams for the given period. - * - * @param {shaka.extern.Period} period - * @return {!Object.} - */ - function defaultOnChooseStreams(period) { - if (period == manifest.periods[0]) { - return {variant: variant1, text: textStream1}; - } else if (period == manifest.periods[1]) { - return {variant: variant2, text: textStream2}; - } else { - throw new Error(); - } - } + expect(mediaSourceEngine.setSelectedClosedCaptionId) + .toHaveBeenCalledTimes(1); + }); + }); - /** - * Choose streams for the given period, used for testing unload text stream. - * The text stream of the second period is not choosen. - * - * @param {shaka.extern.Period} period - * @return {!Object.} - */ - function onChooseStreamsWithUnloadedText(period) { - if (period == manifest.periods[0]) { - return {variant: variant1, text: textStream1}; - } else if (period == manifest.periods[1]) { - expect(streamingEngine.unloadTextStream).toHaveBeenCalled(); - return {variant: variant2}; - } else { - throw new Error(); - } - } + it('calls createSegmentIndex on demand', async () => { + setupVod(); + mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); + createStreamingEngine(); - /** - * Makes the mock Playhead object behave as a fake Playhead object which - * begins playback at the given time. - * - * @param {number} startTime the playhead's starting time with respect to - * the presentation timeline. - */ - function setupFakeGetTime(startTime) { - presentationTimeInSeconds = startTime; + // None of the streams have been set up yet because we haven't started yet. + expect(audioStream.createSegmentIndex).not.toHaveBeenCalled(); + expect(videoStream.createSegmentIndex).not.toHaveBeenCalled(); + expect(alternateVideoStream.createSegmentIndex).not.toHaveBeenCalled(); + + // Here we go! + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); playing = true; - } + + await runTest(() => { + if (presentationTimeInSeconds == 1) { + // Once we're streaming, the audio & video streams have been set up. + expect(audioStream.createSegmentIndex).toHaveBeenCalled(); + expect(videoStream.createSegmentIndex).toHaveBeenCalled(); + + // But not this alternate video stream. + expect(alternateVideoStream.createSegmentIndex).not.toHaveBeenCalled(); + } + }); + + // Because we never switched to this stream, it was never set up at any time + // during this simulated playback. + expect(alternateVideoStream.createSegmentIndex).not.toHaveBeenCalled(); + }); /** * Slides the segment availability window forward by 1 second. diff --git a/test/offline/indexeddb_storage_unit.js b/test/offline/indexeddb_storage_unit.js index 0b705b958c..bb32e2e6b8 100644 --- a/test/offline/indexeddb_storage_unit.js +++ b/test/offline/indexeddb_storage_unit.js @@ -178,11 +178,10 @@ filterDescribe('IndexeddbStorageCell', () => window.indexedDB, () => { * @return {shaka.extern.StorageCell} */ function makeCell(connection) { - const cell = new shaka.offline.indexeddb.V2StorageCell( + const cell = new shaka.offline.indexeddb.V5StorageCell( connection, segmentStore, - manifestStore, - /* allow= */ false); + manifestStore); // Track the cell so that we can destroy it when the test is over. cells.push(cell); diff --git a/test/offline/manifest_convert_unit.js b/test/offline/manifest_convert_unit.js index 5b5d5c1adf..b20c8d9bb0 100644 --- a/test/offline/manifest_convert_unit.js +++ b/test/offline/manifest_convert_unit.js @@ -24,8 +24,7 @@ describe('ManifestConverter', () => { /** @type {!Map.} */ const variants = createConverter().createVariants( - audios, videos, timeline, /* periodStart= */ 0, - /* periodDuration= */ 10); + audios, videos, timeline); expect(variants.size).toBe(2); expect(variants.has(0)).toBeTruthy(); @@ -50,8 +49,7 @@ describe('ManifestConverter', () => { /** @type {!Map.} */ const variants = createConverter().createVariants( - audios, videos, timeline, /* periodStart= */ 0, - /* periodDuration= */ 10); + audios, videos, timeline); expect(variants.size).toBe(2); }); @@ -68,105 +66,138 @@ describe('ManifestConverter', () => { /** @type {!Map.} */ const variants = createConverter().createVariants( - audios, videos, timeline, /* periodStart= */ 0, - /* periodDuration= */ 10); + audios, videos, timeline); expect(variants.size).toBe(2); }); }); // describe('createVariants') - describe('fromPeriodDB', () => { - const arbitraryPeriodDuration = 180; - - it('will reconstruct Periods correctly', () => { - /** @type {shaka.extern.PeriodDB} */ - const periodDb = { - startTime: 60, + describe('fromManifestDB', () => { + it('will reconstruct Manifest correctly', () => { + /** @type {shaka.extern.ManifestDB} */ + const manifestDb = { + originalManifestUri: 'http://example.com/foo', + duration: 60, + size: 1234, + expiration: Infinity, streams: [ - createVideoStreamDB(1, 60, [0]), - createAudioStreamDB(2, 60, [0]), + createVideoStreamDB(1, [0]), + createAudioStreamDB(2, [0]), ], + sessionIds: [1, 2, 3, 4], + drmInfo: { + keySystem: 'com.foo.bar', + licenseServerUri: 'http://example.com/drm', + distinctiveIdentifierRequired: true, + persistentStateRequired: true, + audioRobustness: 'very', + videoRobustness: 'kinda_sorta', + serverCertificate: new Uint8Array([1, 2, 3]), + initData: [{ + initData: new Uint8Array([4, 5, 6]), + initDataType: 'cenc', + keyId: 'abc', + }], + keyIds: [ + 'abc', + 'def', + ], + }, + appMetadata: null, }; - const timeline = createTimeline(); - - const period = createConverter().fromPeriodDB( - periodDb, arbitraryPeriodDuration, timeline); - expect(period).toBeTruthy(); - expect(period.startTime).toBe(periodDb.startTime); - expect(period.textStreams).toEqual([]); - expect(period.variants.length).toBe(1); + const manifest = createConverter().fromManifestDB(manifestDb); + expect(manifest.presentationTimeline.getDuration()) + .toBe(manifestDb.duration); + expect(manifest.textStreams).toEqual([]); + expect(manifest.offlineSessionIds).toEqual(manifestDb.sessionIds); + expect(manifest.variants.length).toBe(1); - const variant = period.variants[0]; + const variant = manifest.variants[0]; expect(variant.id).toEqual(jasmine.any(Number)); - expect(variant.language).toBe(periodDb.streams[1].language); + expect(variant.language).toBe(manifestDb.streams[1].language); expect(variant.primary).toBe(false); expect(variant.bandwidth).toEqual(jasmine.any(Number)); expect(variant.allowedByApplication).toBe(true); expect(variant.allowedByKeySystem).toBe(true); + expect(variant.drmInfos).toEqual([manifestDb.drmInfo]); - verifyStream(periodDb, variant.video, periodDb.streams[0]); - verifyStream(periodDb, variant.audio, periodDb.streams[1]); + verifyStream(variant.video, manifestDb.streams[0]); + verifyStream(variant.audio, manifestDb.streams[1]); }); it('supports video-only content', () => { - /** @type {shaka.extern.PeriodDB} */ - const periodDb = { - startTime: 60, + /** @type {shaka.extern.ManifestDB} */ + const manifestDb = { + originalManifestUri: 'http://example.com/foo', + duration: 60, + size: 1234, + expiration: Infinity, + sessionIds: [], + drmInfo: null, + appMetadata: null, streams: [ - createVideoStreamDB(1, 60, [0]), - createVideoStreamDB(2, 60, [1]), + createVideoStreamDB(1, [0]), + createVideoStreamDB(2, [1]), ], }; - const timeline = createTimeline(); + const manifest = createConverter().fromManifestDB(manifestDb); + expect(manifest.variants.length).toBe(2); + + expect(manifest.variants[0].audio).toBe(null); + expect(manifest.variants[0].video).toBeTruthy(); - const period = createConverter().fromPeriodDB( - periodDb, arbitraryPeriodDuration, timeline); - expect(period).toBeTruthy(); - expect(period.variants.length).toBe(2); - expect(period.variants[0].audio).toBe(null); - expect(period.variants[0].video).toBeTruthy(); + expect(manifest.variants[1].audio).toBe(null); + expect(manifest.variants[1].video).toBeTruthy(); }); it('supports audio-only content', () => { - /** @type {shaka.extern.PeriodDB} */ - const periodDb = { - startTime: 60, + /** @type {shaka.extern.ManifestDB} */ + const manifestDb = { + originalManifestUri: 'http://example.com/foo', + duration: 60, + size: 1234, + expiration: Infinity, + sessionIds: [], + drmInfo: null, + appMetadata: null, streams: [ - createAudioStreamDB(1, 60, [0]), - createAudioStreamDB(2, 60, [1]), + createAudioStreamDB(1, [0]), + createAudioStreamDB(2, [1]), ], }; - const timeline = createTimeline(); + const manifest = createConverter().fromManifestDB(manifestDb); + expect(manifest.variants.length).toBe(2); + + expect(manifest.variants[0].audio).toBeTruthy(); + expect(manifest.variants[0].video).toBe(null); - const period = createConverter().fromPeriodDB( - periodDb, arbitraryPeriodDuration, timeline); - expect(period).toBeTruthy(); - expect(period.variants.length).toBe(2); - expect(period.variants[0].audio).toBeTruthy(); - expect(period.variants[0].video).toBe(null); + expect(manifest.variants[1].audio).toBeTruthy(); + expect(manifest.variants[1].video).toBe(null); }); it('supports text streams', () => { - /** @type {shaka.extern.PeriodDB} */ - const periodDb = { - startTime: 60, + /** @type {shaka.extern.ManifestDB} */ + const manifestDb = { + originalManifestUri: 'http://example.com/foo', + duration: 60, + size: 1234, + expiration: Infinity, + sessionIds: [], + drmInfo: null, + appMetadata: null, streams: [ - createVideoStreamDB(1, 60, [0]), - createTextStreamDB(2, 60), + createVideoStreamDB(1, [0]), + createTextStreamDB(2), ], }; - const timeline = createTimeline(); + const manifest = createConverter().fromManifestDB(manifestDb); + expect(manifest.variants.length).toBe(1); + expect(manifest.textStreams.length).toBe(1); - const period = createConverter().fromPeriodDB( - periodDb, arbitraryPeriodDuration, timeline); - expect(period).toBeTruthy(); - expect(period.variants.length).toBe(1); - expect(period.textStreams.length).toBe(1); - - verifyStream(periodDb, period.textStreams[0], periodDb.streams[1]); + verifyStream(manifest.textStreams[0], manifestDb.streams[1]); }); it('combines Variants according to variantIds field', () => { @@ -179,37 +210,37 @@ describe('ManifestConverter', () => { const variant2 = 1; const variant3 = 2; - /** @type {shaka.extern.PeriodDB} */ - const periodDb = { - startTime: 60, + /** @type {shaka.extern.ManifestDB} */ + const manifestDb = { + originalManifestUri: 'http://example.com/foo', + duration: 60, + size: 1234, + expiration: Infinity, + sessionIds: [], + drmInfo: null, + appMetadata: null, streams: [ // Audio - createAudioStreamDB(audio1, 60, [variant2]), - createAudioStreamDB(audio2, 60, [variant1, variant3]), + createAudioStreamDB(audio1, [variant2]), + createAudioStreamDB(audio2, [variant1, variant3]), // Video - createVideoStreamDB(video1, 60, [variant1]), - createVideoStreamDB(video2, 60, [variant2, variant3]), + createVideoStreamDB(video1, [variant1]), + createVideoStreamDB(video2, [variant2, variant3]), ], }; - const timeline = createTimeline(); - - /** @type {shaka.extern.Period} */ - const period = createConverter().fromPeriodDB( - periodDb, arbitraryPeriodDuration, timeline); - - expect(period).toBeTruthy(); - expect(period.variants.length).toBe(3); + const manifest = createConverter().fromManifestDB(manifestDb); + expect(manifest.variants.length).toBe(3); // Variant 1 - expect(findVariant(period.variants, audio2, video1)).toBeTruthy(); + expect(findVariant(manifest.variants, audio2, video1)).toBeTruthy(); // Variant 2 - expect(findVariant(period.variants, audio1, video2)).toBeTruthy(); + expect(findVariant(manifest.variants, audio1, video2)).toBeTruthy(); // Variant 3 - expect(findVariant(period.variants, audio2, video2)).toBeTruthy(); + expect(findVariant(manifest.variants, audio2, video2)).toBeTruthy(); }); - }); // describe('fromPeriodDB') + }); // describe('fromManifestDB') /** @return {!shaka.offline.ManifestConverter} */ function createConverter() { @@ -233,7 +264,6 @@ describe('ManifestConverter', () => { id: id, originalId: id.toString(), primary: false, - presentationTimeOffset: 0, contentType: type, mimeType: '', codecs: '', @@ -241,11 +271,14 @@ describe('ManifestConverter', () => { label: null, width: null, height: null, - initSegmentKey: null, encrypted: false, - keyId: null, + keyIds: [], segments: [], variantIds: variants, + roles: [], + channelsCount: null, + audioSamplingRate: null, + closedCaptions: null, }; return streamDB; @@ -260,9 +293,13 @@ describe('ManifestConverter', () => { function createSegmentDB(startTime, endTime, dataKey) { /** @type {shaka.extern.SegmentDB} */ const segment = { - startTime: startTime, - endTime: endTime, - dataKey: dataKey, + startTime, + endTime, + dataKey, + initSegmentKey: null, + appendWindowStart: 0, + appendWindowEnd: Infinity, + timestampOffset: 0, }; return segment; @@ -270,17 +307,15 @@ describe('ManifestConverter', () => { /** * @param {number} id - * @param {number} periodStart * @param {!Array.} variantIds * @return {shaka.extern.StreamDB} */ - function createVideoStreamDB(id, periodStart, variantIds) { + function createVideoStreamDB(id, variantIds) { const ContentType = shaka.util.ManifestParserUtils.ContentType; return { id: id, originalId: id.toString(), primary: false, - presentationTimeOffset: 25, contentType: ContentType.VIDEO, mimeType: 'video/mp4', codecs: 'avc1.42c01e', @@ -291,40 +326,41 @@ describe('ManifestConverter', () => { label: null, width: 250, height: 100, - initSegmentKey: null, encrypted: true, - keyId: 'key1', + keyIds: ['key1'], segments: [ createSegmentDB( - /* startTime= */ periodStart, - /* endTime= */ periodStart + 10, + /* startTime= */ 0, + /* endTime= */ 10, /* dataKey= */ 1), createSegmentDB( - /* startTime= */ periodStart + 10, - /* endTime= */ periodStart + 20, + /* startTime= */ 10, + /* endTime= */ 20, /* dataKey= */ 2), createSegmentDB( - /* startTime= */ periodStart + 20, - /* endTime= */ periodStart + 25, + /* startTime= */ 20, + /* endTime= */ 25, /* dataKey= */ 3), ], variantIds: variantIds, + roles: [], + channelsCount: null, + audioSamplingRate: null, + closedCaptions: null, }; } /** * @param {number} id - * @param {number} periodStart * @param {!Array.} variantIds * @return {shaka.extern.StreamDB} */ - function createAudioStreamDB(id, periodStart, variantIds) { + function createAudioStreamDB(id, variantIds) { const ContentType = shaka.util.ManifestParserUtils.ContentType; return { id: id, originalId: id.toString(), primary: false, - presentationTimeOffset: 10, contentType: ContentType.AUDIO, mimeType: 'audio/mp4', codecs: 'mp4a.40.2', @@ -335,39 +371,40 @@ describe('ManifestConverter', () => { label: null, width: null, height: null, - initSegmentKey: 0, encrypted: false, - keyId: null, + keyIds: [], segments: [ createSegmentDB( - /* startTime= */ periodStart, - /* endTime= */ periodStart + 10, + /* startTime= */ 0, + /* endTime= */ 10, /* dataKey= */ 1), createSegmentDB( - /* startTime= */ periodStart + 10, - /* endTime= */ periodStart + 20, + /* startTime= */ 10, + /* endTime= */ 20, /* dataKey= */ 2), createSegmentDB( - /* startTime= */ periodStart + 20, - /* endTime= */ periodStart + 25, + /* startTime= */ 20, + /* endTime= */ 25, /* dataKey= */ 3), ], variantIds: variantIds, + roles: [], + channelsCount: null, + audioSamplingRate: null, + closedCaptions: null, }; } /** * @param {number} id - * @param {number} periodStart * @return {shaka.extern.StreamDB} */ - function createTextStreamDB(id, periodStart) { + function createTextStreamDB(id) { const ContentType = shaka.util.ManifestParserUtils.ContentType; return { id: id, originalId: id.toString(), primary: false, - presentationTimeOffset: 10, contentType: ContentType.TEXT, mimeType: 'text/vtt', codecs: '', @@ -378,33 +415,35 @@ describe('ManifestConverter', () => { label: null, width: null, height: null, - initSegmentKey: 0, encrypted: false, - keyId: null, + keyIds: [], segments: [ createSegmentDB( - /* startTime= */ periodStart, - /* endTime= */ periodStart + 10, + /* startTime= */ 0, + /* endTime= */ 10, /* dataKey= */ 1), createSegmentDB( - /* startTime= */ periodStart + 10, - /* endTime= */ periodStart + 20, + /* startTime= */ 10, + /* endTime= */ 20, /* dataKey= */ 2), createSegmentDB( - /* startTime= */ periodStart + 20, - /* endTime= */ periodStart + 25, + /* startTime= */ 20, + /* endTime= */ 25, /* dataKey= */ 3), ], variantIds: [5], + roles: [], + channelsCount: null, + audioSamplingRate: null, + closedCaptions: null, }; } /** - * @param {?shaka.extern.PeriodDB} periodDb * @param {?shaka.extern.Stream} stream * @param {?shaka.extern.StreamDB} streamDb */ - function verifyStream(periodDb, stream, streamDb) { + function verifyStream(stream, streamDb) { if (!streamDb) { expect(stream).toBeFalsy(); return; @@ -418,49 +457,52 @@ describe('ManifestConverter', () => { mimeType: streamDb.mimeType, codecs: streamDb.codecs, frameRate: streamDb.frameRate, - pixelAspectRatio: streamDb.pixelAspectRatio || undefined, + pixelAspectRatio: streamDb.pixelAspectRatio, width: streamDb.width || undefined, height: streamDb.height || undefined, kind: streamDb.kind, encrypted: streamDb.encrypted, - keyId: streamDb.keyId, + keyIds: streamDb.keyIds, language: streamDb.language, label: streamDb.label, type: streamDb.contentType, primary: streamDb.primary, trickModeVideo: null, emsgSchemeIdUris: null, - roles: [], - channelsCount: null, - audioSamplingRate: null, - closedCaptions: null, + roles: streamDb.roles, + channelsCount: streamDb.channelsCount, + audioSamplingRate: streamDb.audioSamplingRate, + closedCaptions: streamDb.closedCaptions, }; expect(stream).toEqual(expectedStream); // Assume that we don't have to call createSegmentIndex. - const initSegmentReference = streamDb.initSegmentKey != null ? - jasmine.any(shaka.media.InitSegmentReference) : - null; - const presentationTimeOffset = streamDb.presentationTimeOffset; - const iterator = stream.segmentIndex[Symbol.iterator](); streamDb.segments.forEach((segmentDb, i) => { const uri = shaka.offline.OfflineUri.segment( 'mechanism', 'cell', segmentDb.dataKey); + const initSegmentReference = segmentDb.initSegmentKey != null ? + jasmine.any(shaka.media.InitSegmentReference) : + null; + /** @type {shaka.media.SegmentReference} */ - const segment = iterator.seek(periodDb.startTime + segmentDb.startTime); - expect(segment.startTime).toBe(periodDb.startTime + segmentDb.startTime); - expect(segment.endTime).toBe(periodDb.startTime + segmentDb.endTime); + const segment = iterator.seek(segmentDb.startTime); + + /** @type {shaka.media.SegmentReference} */ + const sameSegment = iterator.seek(segmentDb.endTime - 0.1); + + expect(segment).toBe(sameSegment); + expect(segment.startTime).toBe(segmentDb.startTime); + expect(segment.endTime).toBe(segmentDb.endTime); expect(segment.startByte).toBe(0); expect(segment.endByte).toBe(null); expect(segment.getUris()).toEqual([uri.toString()]); expect(segment.initSegmentReference).toEqual(initSegmentReference); - expect(segment.timestampOffset).toBe( - periodDb.startTime - presentationTimeOffset); + expect(segment.timestampOffset).toBe(segmentDb.timestampOffset); }); } diff --git a/test/offline/offline_integration.js b/test/offline/offline_integration.js index e48122620f..f1d2fc5f39 100644 --- a/test/offline/offline_integration.js +++ b/test/offline/offline_integration.js @@ -3,7 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -describe('Offline', () => { +/** @return {boolean} */ +const supportsStorage = () => shaka.offline.Storage.support(); + +// TODO: Merge with storage_integration.js. No obvious difference in purpose. +filterDescribe('Offline', supportsStorage, () => { /** @type {!shaka.Player} */ let player; /** @type {!shaka.offline.Storage} */ @@ -28,11 +32,9 @@ describe('Offline', () => { eventManager = new shaka.util.EventManager(); - if (supportsStorage()) { - // Make sure we are starting with a blank slate. - await shaka.offline.Storage.deleteAll(); - storage = new shaka.offline.Storage(player); - } + // Make sure we are starting with a blank slate. + await shaka.offline.Storage.deleteAll(); + storage = new shaka.offline.Storage(player); }); afterEach(async () => { @@ -43,9 +45,7 @@ describe('Offline', () => { } // Make sure we don't leave anything in storage after the test. - if (supportsStorage()) { - await shaka.offline.Storage.deleteAll(); - } + await shaka.offline.Storage.deleteAll(); if (player) { await player.destroy(); @@ -53,11 +53,6 @@ describe('Offline', () => { }); it('stores, plays, and deletes clear content', async () => { - if (!supportsStorage()) { - pending('Storage is not supported.'); - return; - } - const content = await storage.store('test:sintel'); expect(content).toBeTruthy(); @@ -77,11 +72,6 @@ describe('Offline', () => { drmIt( 'stores, plays, and deletes protected content with a persistent license', async () => { - if (!supportsStorage()) { - pending('Storage is not supported on this platform.'); - return; - } - const support = await shaka.Player.probeSupport(); const widevineSupport = support.drm['com.widevine.alpha']; @@ -117,11 +107,6 @@ describe('Offline', () => { drmIt( 'stores, plays, and deletes protected content with a temporary license', async () => { - if (!supportsStorage()) { - pending('Storage is not supported.'); - return; - } - const support = await shaka.Player.probeSupport(); const widevineSupport = support.drm['com.widevine.alpha']; const playreadySupport = support.drm['com.microsoft.playready']; @@ -160,9 +145,4 @@ describe('Offline', () => { await shaka.test.Util.waitUntilPlayheadReaches( eventManager, video, endSeconds, timeoutSeconds); } - - /** @return {boolean} */ - function supportsStorage() { - return shaka.offline.Storage.support(); - } }); diff --git a/test/offline/offline_manifest_parser_unit.js b/test/offline/offline_manifest_parser_unit.js index 79505fb879..e7e4914de6 100644 --- a/test/offline/offline_manifest_parser_unit.js +++ b/test/offline/offline_manifest_parser_unit.js @@ -202,7 +202,7 @@ filterDescribe('OfflineManifestParser', offlineManifestParserSupport, () => { duration: 600 * seconds, size: 100 * mb, expiration: Infinity, - periods: [], + streams: [], sessionIds: [sessionId], drmInfo: null, appMetadata: {}, diff --git a/test/offline/storage_compatibility_unit.js b/test/offline/storage_compatibility_unit.js index 3c272ec0d1..8bf01987c0 100644 --- a/test/offline/storage_compatibility_unit.js +++ b/test/offline/storage_compatibility_unit.js @@ -33,8 +33,7 @@ const compatibilityTestsMetadata = [ makeCell: (connection) => new shaka.offline.indexeddb.V2StorageCell( connection, /* segmentStore= */ 'segment-v2', - /* manifestStore= */ 'manifest-v2', - /* isFixedKey= */ true), // TODO: Drop isFixedKey when v4 is out. + /* manifestStore= */ 'manifest-v2'), }, { // This is the "clean" version of the v2 database format, as created from @@ -47,39 +46,50 @@ const compatibilityTestsMetadata = [ makeCell: (connection) => new shaka.offline.indexeddb.V2StorageCell( connection, /* segmentStore= */ 'segment-v2', - /* manifestStore= */ 'manifest-v2', - /* isFixedKey= */ true), // TODO: Drop isFixedKey when v4 is out. + /* manifestStore= */ 'manifest-v2'), }, { // This is the v3 version of the database, which is actually identical to // the "clean" version of the v2 database. The version number was // incremented to overcome the "broken" v2 databases. This format was - // introduced in v2.3.2. + // introduced in v2.3.2 and deprecated in v2.6. name: 'v3', dbImagePath: '/base/test/test/assets/db-dump-v3.json', manifestKey: 1, - readOnly: false, + readOnly: true, makeCell: (connection) => new shaka.offline.indexeddb.V2StorageCell( connection, /* segmentStore= */ 'segment-v3', - /* manifestStore= */ 'manifest-v3', - /* isFixedKey= */ false), // TODO: Drop isFixedKey when v4 is out. + /* manifestStore= */ 'manifest-v3'), }, { - // This is the v3 version of the database as written by v2.5.0 - v2.5.9. A + // This is the v4 version of the database as written by v2.5.0 - v2.5.9. A // bug in v2.5 caused the stream metadata from all periods to be written to // each period. This was corrected in v2.5.10. // See https://github.com/google/shaka-player/issues/2389 - name: 'v3-broken', - dbImagePath: '/base/test/test/assets/db-dump-v3-broken.json', + name: 'v4-broken', + dbImagePath: '/base/test/test/assets/db-dump-v4-broken.json', manifestKey: 1, - readOnly: false, + readOnly: true, makeCell: (connection) => new shaka.offline.indexeddb.V2StorageCell( connection, + // V4 of the database still used the V3 store names and structures. /* segmentStore= */ 'segment-v3', - /* manifestStore= */ 'manifest-v3', - /* isFixedKey= */ false), // TODO: Drop isFixedKey when v4 is out. + /* manifestStore= */ 'manifest-v3'), + }, + /* FIXME(#1339): Dump V5 database and enable this test case + { + // This is the v5 version of the database, introduced in v2.6. + name: 'v5', + dbImagePath: '/base/test/test/assets/db-dump-v5.json', + manifestKey: 1, + readOnly: false, + makeCell: (connection) => new shaka.offline.indexeddb.V5StorageCell( + connection, + /* segmentStore= * 'segment-v5', + /* manifestStore= * 'manifest-v5'), }, + */ ]; filterDescribe('Storage Compatibility', () => window.indexedDB, () => { @@ -261,7 +271,8 @@ filterDescribe('Storage Compatibility', () => window.indexedDB, () => { await checkMissingManifests(manifestKeys); }); - it('correctly converts to the current manifest format', async () => { + // FIXME(#1339): Re-enable this test! + xit('correctly converts to the current manifest format', async () => { // There should be one manifest. const manifestDb = (await cell.getManifests([metadata.manifestKey]))[0]; const converter = new shaka.offline.ManifestConverter( @@ -272,49 +283,20 @@ filterDescribe('Storage Compatibility', () => window.indexedDB, () => { manifest.anyTimeline(); manifest.minBufferTime = 2; - manifest.addPeriod(0, (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.frameRate = 29.97; - stream.mime('video/webm', 'vp9'); - stream.size(640, 480); - }); - }); - }); - - manifest.addPeriod(Util.closeTo(2.06874), (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.frameRate = 29.97; - stream.mime('video/webm', 'vp9'); - stream.size(640, 480); - }); - }); - }); - - manifest.addPeriod(Util.closeTo(4.20413), (period) => { - period.addPartialVariant((variant) => { - variant.addPartialStream(ContentType.VIDEO, (stream) => { - stream.frameRate = 29.97; - stream.mime('video/webm', 'vp9'); - stream.size(320, 240); - }); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.frameRate = 29.97; + stream.mime('video/webm', 'vp9'); + stream.size(640, 480); }); }); }); expect(actual).toEqual(expected); - const segmentIndex0 = actual.periods[0].variants[0].video.segmentIndex; - const segmentIndex1 = actual.periods[1].variants[0].video.segmentIndex; - const segmentIndex2 = actual.periods[2].variants[0].video.segmentIndex; - goog.asserts.assert(segmentIndex0, 'Null segment index!'); - goog.asserts.assert(segmentIndex1, 'Null segment index!'); - goog.asserts.assert(segmentIndex2, 'Null segment index!'); - - const segment0 = Array.from(segmentIndex0)[0]; - const segment1 = Array.from(segmentIndex1)[0]; - const segment2 = Array.from(segmentIndex2)[0]; + const segmentIndex = actual.variants[0].video.segmentIndex; + goog.asserts.assert(segmentIndex != null, 'Null segmentIndex!'); + const [segment0, segment1, segment2] = Array.from(segmentIndex); expect(segment0).toEqual(jasmine.objectContaining({ startTime: 0, @@ -346,21 +328,19 @@ filterDescribe('Storage Compatibility', () => window.indexedDB, () => { * @return {!Array.} */ function getAllSegmentKeys(manifest) { - const keys = []; + const keys = new Set(); - for (const period of manifest.periods) { - for (const stream of period.streams) { - if (stream.initSegmentKey != null) { - keys.push(stream.initSegmentKey); + for (const stream of manifest.streams) { + for (const segment of stream.segments) { + if (segment.initSegmentKey != null) { + keys.add(segment.initSegmentKey); } - for (const segment of stream.segments) { - keys.push(segment.dataKey); - } + keys.add(segment.dataKey); } } - return keys; + return Array.from(keys); } } // makeTests }); diff --git a/test/offline/storage_integration.js b/test/offline/storage_integration.js index e2555af847..92b33b9c81 100644 --- a/test/offline/storage_integration.js +++ b/test/offline/storage_integration.js @@ -23,7 +23,7 @@ filterDescribe('Storage', storageSupport, () => { const Util = shaka.test.Util; const englishUS = 'en-us'; - const frenchCanadian= 'fr-ca'; + const frenchCanadian = 'fr-ca'; const fakeMimeType = 'application/test'; const manifestWithPerStreamBandwidthUri = @@ -32,7 +32,6 @@ filterDescribe('Storage', storageSupport, () => { 'fake:manifest-without-per-stream-bandwidth'; const manifestWithNonZeroStartUri = 'fake:manifest-with-non-zero-start'; const manifestWithLiveTimelineUri = 'fake:manifest-with-live-timeline'; - const manifestWithThreePeriodsUri = 'fake:manifest-with-three-periods'; const segment1Uri = 'fake:segment-1'; const segment2Uri = 'fake:segment-2'; @@ -583,23 +582,21 @@ filterDescribe('Storage', storageSupport, () => { function makeWithStreamBandwidth() { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.presentationTimeline.setDuration(20); - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.language = englishUS; - variant.bandwidth = kbps(13); - variant.addVideo(1, (stream) => { - stream.bandwidth = kbps(10); - stream.size(100, 200); - }); - variant.addAudio(2, (stream) => { - stream.language = englishUS; - stream.bandwidth = kbps(3); - }); + manifest.addVariant(0, (variant) => { + variant.language = englishUS; + variant.bandwidth = kbps(13); + variant.addVideo(1, (stream) => { + stream.bandwidth = kbps(10); + stream.size(100, 200); + }); + variant.addAudio(2, (stream) => { + stream.language = englishUS; + stream.bandwidth = kbps(3); }); }); }); - const audio = manifest.periods[0].variants[0].audio; + const audio = manifest.variants[0].audio; goog.asserts.assert(audio, 'Created manifest with audio, where is it?'); overrideSegmentIndex(audio, [ makeReference(audioSegment1Uri, 0, 1), @@ -608,7 +605,7 @@ filterDescribe('Storage', storageSupport, () => { makeReference(audioSegment4Uri, 3, 4), ]); - const video = manifest.periods[0].variants[0].video; + const video = manifest.variants[0].video; goog.asserts.assert(video, 'Created manifest with video, where is it?'); overrideSegmentIndex(video, [ makeReference(videoSegment1Uri, 0, 1), @@ -634,13 +631,10 @@ filterDescribe('Storage', storageSupport, () => { // the per-stream values. const manifest = makeWithStreamBandwidth(); goog.asserts.assert( - manifest.periods.length == 1, - 'Expecting manifest to only have one period'); - goog.asserts.assert( - manifest.periods[0].variants.length == 1, + manifest.variants.length == 1, 'Expecting manifest to only have one variant'); - const variant = manifest.periods[0].variants[0]; + const variant = manifest.variants[0]; goog.asserts.assert( variant.audio, 'Expecting manifest to have audio stream'); @@ -701,7 +695,6 @@ filterDescribe('Storage', storageSupport, () => { manifestWithPerStreamBandwidthUri, manifestWithoutPerStreamBandwidthUri, manifestWithNonZeroStartUri, - manifestWithThreePeriodsUri, ]; // NOTE: We're working around an apparent compiler bug here, with Closure @@ -740,8 +733,6 @@ filterDescribe('Storage', storageSupport, () => { }, }); - // Stored content should reflect the tracks in the first period, so we - // should only find track there. const stored = await storage.store( manifestWithPerStreamBandwidthUri, noMetadata, fakeMimeType); expect(stored.tracks.length).toBe(1); @@ -762,13 +753,11 @@ filterDescribe('Storage', storageSupport, () => { expect(manifests.length).toBe(1); const manifest = manifests[0]; - expect(manifest.periods.length).toBe(1); - - const period = manifest.periods[0]; // There should be 2 streams, an audio and a video stream. - expect(period.streams.length).toBe(2); + expect(manifest.streams.length).toBe(2); - const audio = period.streams.filter((s) => s.contentType == 'audio')[0]; + const audio = manifest.streams.filter( + (s) => s.contentType == 'audio')[0]; expect(audio.language).toBe(frenchCanadian); } finally { await muxer.destroy(); @@ -809,7 +798,7 @@ filterDescribe('Storage', storageSupport, () => { overrideDrmAndManifest( storage, drm, - makeManifestWithPerStreamBandwidth(1)); + makeManifestWithPerStreamBandwidth()); const stored = await storage.store(manifestWithPerStreamBandwidthUri); @@ -856,7 +845,7 @@ filterDescribe('Storage', storageSupport, () => { overrideDrmAndManifest( storage, drm, - makeManifestWithPerStreamBandwidth(1)); + makeManifestWithPerStreamBandwidth()); storage.configure('offline.usePersistentLicense', false); const stored = await storage.store(manifestWithPerStreamBandwidthUri); @@ -923,7 +912,7 @@ filterDescribe('Storage', storageSupport, () => { }); it('throws an error if destroyed mid-store', async () => { - const manifest = makeManifestWithPerStreamBandwidth(1); + const manifest = makeManifestWithPerStreamBandwidth(); /** * Block storage when it goes to parse the manifest. Since we don't want @@ -1022,7 +1011,7 @@ filterDescribe('Storage', storageSupport, () => { // Get the stream from the manifest. The segment count is based on how // we created manifest in the "make*Manifest" functions. - const stream = manifest.periods[0].streams[0]; + const stream = manifest.streams[0]; expect(stream).toBeTruthy(); expect(stream.segments.length).toBe(4); @@ -1088,55 +1077,6 @@ filterDescribe('Storage', storageSupport, () => { expect(progressSteps).toBeTruthy(); expect(progressSteps.length).toBe(0); }); - - it('stores multi-period content', async () => { - const storedContent = await storage.store( - manifestWithThreePeriodsUri, noMetadata, fakeMimeType); - - let parsed = false; - - eventManager.listen(player, 'manifestparsed', async () => { - const manifest = player.getManifest(); - expect(manifest.periods.length).toBe(3); - - const start0 = manifest.periods[0].startTime; - const start1 = manifest.periods[1].startTime; - const start2 = manifest.periods[2].startTime; - - const stream0 = manifest.periods[0].variants[0].video; - const stream1 = manifest.periods[1].variants[0].video; - const stream2 = manifest.periods[2].variants[0].video; - - await stream0.createSegmentIndex(); - await stream1.createSegmentIndex(); - await stream2.createSegmentIndex(); - - const position0 = stream0.segmentIndex.find(start0); - const position1 = stream1.segmentIndex.find(start1); - const position2 = stream2.segmentIndex.find(start2); - - expect(position0).not.toBe(null); - expect(position1).not.toBe(null); - expect(position2).not.toBe(null); - - const segment0 = stream0.segmentIndex.get(position0); - const segment1 = stream1.segmentIndex.get(position1); - const segment2 = stream2.segmentIndex.get(position2); - - expect(segment0.startTime).toBe(start0); - expect(segment1.startTime).toBe(start1); - expect(segment2.startTime).toBe(start2); - - parsed = true; - }); - - await player.load( - storedContent.offlineUri, 0, 'application/x-offline-manifest'); - - // Make sure the listener with the expectations actually fired and - // completed. - expect(parsed).toBe(true); - }); }); describe('storage without player', () => { @@ -1232,65 +1172,48 @@ filterDescribe('Storage', storageSupport, () => { }; } - /** - * @param {number} numPeriods - * @return {shaka.extern.Manifest} - */ - function makeManifestWithPerStreamBandwidth(numPeriods) { - const periodDuration = 4; - const idsPerPeriod = 6; - + /** @return {shaka.extern.Manifest} */ + function makeManifestWithPerStreamBandwidth() { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.presentationTimeline.setDuration(20); - for (let i = 0; i < numPeriods; ++i) { - const startTime = i * periodDuration; - const baseId = i * idsPerPeriod; - - manifest.addPeriod(startTime, (period) => { - period.addVariant(baseId + 0, (variant) => { - variant.language = englishUS; - variant.bandwidth = kbps(13); - variant.addVideo(baseId + 1, (stream) => { - stream.bandwidth = kbps(10); - stream.size(100, 200); - }); - variant.addAudio(baseId + 2, (stream) => { - stream.language = englishUS; - stream.bandwidth = kbps(3); - }); - }); - period.addVariant(baseId + 3, (variant) => { - variant.language = frenchCanadian; - variant.bandwidth = kbps(13); - variant.addVideo(baseId + 4, (stream) => { - stream.bandwidth = kbps(10); - stream.size(100, 200); - }); - variant.addAudio(baseId + 5, (stream) => { - stream.language = frenchCanadian; - stream.bandwidth = kbps(3); - }); - }); + manifest.addVariant(0, (variant) => { + variant.language = englishUS; + variant.bandwidth = kbps(13); + variant.addVideo(1, (stream) => { + stream.bandwidth = kbps(10); + stream.size(100, 200); }); - } + variant.addAudio(2, (stream) => { + stream.language = englishUS; + stream.bandwidth = kbps(3); + }); + }); + manifest.addVariant(3, (variant) => { + variant.language = frenchCanadian; + variant.bandwidth = kbps(13); + variant.addVideo(4, (stream) => { + stream.bandwidth = kbps(10); + stream.size(100, 200); + }); + variant.addAudio(5, (stream) => { + stream.language = frenchCanadian; + stream.bandwidth = kbps(3); + }); + }); }); - for (let i = 0; i < numPeriods; ++i) { - for (const stream of getAllStreams(manifest, i)) { - const startTime = i * periodDuration; - - // Make a new copy each time, as the segment index can modify each - // reference. - const refs = [ - makeReference(segment1Uri, startTime + 0, startTime + 1), - makeReference(segment2Uri, startTime + 1, startTime + 2), - makeReference(segment3Uri, startTime + 2, startTime + 3), - makeReference(segment4Uri, startTime + 3, startTime + 4), - ]; + for (const stream of getAllStreams(manifest)) { + // Make a new copy each time, as the segment index can modify each + // reference. + const refs = [ + makeReference(segment1Uri, 0, 1), + makeReference(segment2Uri, 1, 2), + makeReference(segment3Uri, 2, 3), + makeReference(segment4Uri, 3, 4), + ]; - overrideSegmentIndex(stream, refs); - } + overrideSegmentIndex(stream, refs); } return manifest; @@ -1300,10 +1223,10 @@ filterDescribe('Storage', storageSupport, () => { * @return {shaka.extern.Manifest} */ function makeManifestWithoutPerStreamBandwidth() { - const manifest = makeManifestWithPerStreamBandwidth(1); + const manifest = makeManifestWithPerStreamBandwidth(); // Remove the per stream bandwidth. - for (const stream of getAllStreams(manifest, 0)) { + for (const stream of getAllStreams(manifest)) { stream.bandwidth = undefined; } @@ -1314,9 +1237,9 @@ filterDescribe('Storage', storageSupport, () => { * @return {shaka.extern.Manifest} */ function makeManifestWithNonZeroStart() { - const manifest = makeManifestWithPerStreamBandwidth(1); + const manifest = makeManifestWithPerStreamBandwidth(); - for (const stream of getAllStreams(manifest, 0)) { + for (const stream of getAllStreams(manifest)) { const refs = [ makeReference(segment1Uri, 10, 11), makeReference(segment2Uri, 11, 12), @@ -1334,7 +1257,7 @@ filterDescribe('Storage', storageSupport, () => { * @return {shaka.extern.Manifest} */ function makeManifestWithLiveTimeline() { - const manifest = makeManifestWithPerStreamBandwidth(1); + const manifest = makeManifestWithPerStreamBandwidth(); manifest.presentationTimeline.setDuration(Infinity); manifest.presentationTimeline.setStatic(false); return manifest; @@ -1342,13 +1265,12 @@ filterDescribe('Storage', storageSupport, () => { /** * @param {shaka.extern.Manifest} manifest - * @param {number} periodIndex * @return {!Array.} */ - function getAllStreams(manifest, periodIndex) { + function getAllStreams(manifest) { const streams = []; - for (const variant of manifest.periods[periodIndex].variants) { + for (const variant of manifest.variants) { if (variant.audio) { streams.push(variant.audio); } @@ -1356,7 +1278,7 @@ filterDescribe('Storage', storageSupport, () => { streams.push(variant.video); } } - for (const stream of manifest.periods[periodIndex].textStreams) { + for (const stream of manifest.textStreams) { streams.push(stream); } @@ -1427,15 +1349,13 @@ filterDescribe('Storage', storageSupport, () => { constructor() { this.map_ = {}; this.map_[manifestWithPerStreamBandwidthUri] = - makeManifestWithPerStreamBandwidth(1); + makeManifestWithPerStreamBandwidth(); this.map_[manifestWithoutPerStreamBandwidthUri] = makeManifestWithoutPerStreamBandwidth(); this.map_[manifestWithNonZeroStartUri] = makeManifestWithNonZeroStart(); this.map_[manifestWithLiveTimelineUri] = makeManifestWithLiveTimeline(); - this.map_[manifestWithThreePeriodsUri] = - makeManifestWithPerStreamBandwidth(3); } /** @override */ @@ -1517,7 +1437,7 @@ filterDescribe('Storage', storageSupport, () => { try { drm.configure(player.getConfiguration().drm); - const variants = shaka.util.Periods.getAllVariantsFrom(manifest.periods); + const variants = manifest.variants; await drm.initForStorage(variants, /* usePersistentLicenses= */ true); await action(drm); } finally { diff --git a/test/player_integration.js b/test/player_integration.js index f5cb80393a..739a96d98b 100644 --- a/test/player_integration.js +++ b/test/player_integration.js @@ -240,7 +240,7 @@ describe('Player', () => { }); // Repro for https://github.com/google/shaka-player/issues/1879. - it('actually appends cues when enabled initially', async () => { + it('appends cues when enabled initially', async () => { let cues = []; /** @const {!shaka.test.FakeTextDisplayer} */ const displayer = new shaka.test.FakeTextDisplayer(); @@ -254,14 +254,17 @@ describe('Player', () => { player.configure({preferredTextLanguage: preferredTextLanguage}); await player.load('test:sintel_realistic_compiled'); - await Util.delay(1); // Allow the first segments to be appended. + + // Play until a time at which the external cues would be on screen. + video.play(); + await waitUntilPlayheadReaches(eventManager, video, 4, 20); expect(player.isTextTrackVisible()).toBe(true); expect(displayer.isTextVisible()).toBe(true); expect(cues.length).toBeGreaterThan(0); }); - it('actually appends cues for external text', async () => { + it('appends cues for external text', async () => { let cues = []; /** @const {!shaka.test.FakeTextDisplayer} */ const displayer = new shaka.test.FakeTextDisplayer(); @@ -275,23 +278,22 @@ describe('Player', () => { /** @type {shaka.test.Waiter} */ const waiter = new shaka.test.Waiter(eventManager); - await player.load('test:sintel_no_text_compiled'); const locationUri = new goog.Uri(location.href); const partialUri = new goog.Uri('/base/test/test/assets/text-clip.vtt'); const absoluteUri = locationUri.resolve(partialUri); - await player.addTextTrack(absoluteUri.toString(), 'en', 'subtitles', - 'text/vtt'); + const newTrack = player.addTextTrack( + absoluteUri.toString(), 'en', 'subtitles', 'text/vtt'); - const textTracks = player.getTextTracks(); - expect(textTracks).toBeTruthy(); - expect(textTracks.length).toBe(1); + expect(player.getTextTracks()).toEqual([newTrack]); + player.selectTextTrack(newTrack); player.setTextTrackVisibility(true); await waiter.waitForEvent(player, 'texttrackvisibility'); - // Wait for the text cues to get appended. - // TODO: this should be based on an event instead. - await Util.delay(1); + + // Play until a time at which the external cues would be on screen. + video.play(); + await waitUntilPlayheadReaches(eventManager, video, 4, 20); expect(player.isTextTrackVisible()).toBe(true); expect(displayer.isTextVisible()).toBe(true); @@ -308,15 +310,14 @@ describe('Player', () => { const locationUri = new goog.Uri(location.href); const partialUri = new goog.Uri('/base/test/test/assets/text-clip.vtt'); const absoluteUri = locationUri.resolve(partialUri); - await player.addTextTrack(absoluteUri.toString(), 'en', 'subtitles', - 'text/vtt'); + const newTrack = player.addTextTrack( + absoluteUri.toString(), 'en', 'subtitles', 'text/vtt'); - const textTracks = player.getTextTracks(); - expect(textTracks).toBeTruthy(); - expect(textTracks.length).toBe(1); + expect(newTrack.language).toBe('en'); + expect(player.getTextTracks()).toEqual([newTrack]); - expect(textTracks[0].active).toBe(true); - expect(textTracks[0].language).toBe('en'); + player.selectTextTrack(newTrack); + expect(player.getTextTracks()[0].active).toBe(true); }); it('with cea closed captions', async () => { @@ -328,32 +329,6 @@ describe('Player', () => { expect(textTracks[0].language).toBe('en'); }); - it('while changing languages with short Periods', async () => { - // See: https://github.com/google/shaka-player/issues/797 - player.configure({preferredAudioLanguage: 'en'}); - await player.load('test:sintel_short_periods_compiled'); - video.play(); - await waitUntilPlayheadReaches(eventManager, video, 8, 30); - - // The Period changes at 10 seconds. Assert that we are in the previous - // Period and have buffered into the next one. - expect(video.currentTime).toBeLessThan(9); - // The two periods might not be in a single contiguous buffer, so don't - // check end(0). Gap-jumping will deal with any discontinuities. - const bufferEnd = video.buffered.end(video.buffered.length - 1); - expect(bufferEnd).toBeGreaterThan(11); - - // Change to a different language; this should clear the buffers and - // cause a Period transition again. - expect(getActiveLanguage()).toBe('en'); - player.selectAudioLanguage('es'); - await waitUntilPlayheadReaches(eventManager, video, 21, 30); - - // Should have gotten past the next Period transition and still be - // playing the new language. - expect(getActiveLanguage()).toBe('es'); - }); - it('at higher playback rates', async () => { await player.load('test:sintel_compiled'); video.play(); @@ -587,10 +562,7 @@ describe('Player', () => { const waiter = (new shaka.test.Waiter(eventManager)).timeoutAfter(10); const canPlayThrough = waiter.waitForEvent(video, 'canplaythrough'); - // Important: use a stream that starts somewhere other than zero, so that - // the video element's time is initially different from the start time of - // playback, and there is no content at time zero. - await player.load('test:sintel_start_at_3_compiled', 5); + await player.load('test:sintel_compiled', 5); shaka.log.debug('load resolved'); // When load is resolved(), tracks should definitely exist. diff --git a/test/player_src_equals_integration.js b/test/player_src_equals_integration.js index 9676085b83..4d77d9785d 100644 --- a/test/player_src_equals_integration.js +++ b/test/player_src_equals_integration.js @@ -335,13 +335,13 @@ describe('Player Src Equals', () => { it('cannot add text tracks', async () => { await loadWithSrcEquals(SMALL_MP4_CONTENT_URI, /* startTime= */ null); - const pendingAdd = player.addTextTrack( - 'test:need-a-uri-for-text', - 'en-US', - 'main', - 'text/mp4'); - - await expectAsync(pendingAdd).toBeRejected(); + expect(() => { + player.addTextTrack( + 'test:need-a-uri-for-text', + 'en-US', + 'main', + 'text/mp4'); + }).toThrow(); }); // Since we are not in-charge of streaming, calling |retryStreaming| should diff --git a/test/player_unit.js b/test/player_unit.js index 2c3e49e146..5c5f1e244b 100644 --- a/test/player_unit.js +++ b/test/player_unit.js @@ -23,8 +23,6 @@ describe('Player', () => { let onError; /** @type {shaka.extern.Manifest} */ let manifest; - /** @type {number} */ - let periodIndex; /** @type {!shaka.Player} */ let player; /** @type {!shaka.test.FakeAbrManager} */ @@ -69,20 +67,15 @@ describe('Player', () => { // Many tests assume the existence of a manifest, so create a basic one. // Test suites can override this with more specific manifests. manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addAudio(1); - variant.addVideo(2); - }); + manifest.addVariant(0, (variant) => { + variant.addAudio(1); + variant.addVideo(2); }); - manifest.addPeriod(1, (period) => { - period.addVariant(1, (variant) => { - variant.addAudio(3); - variant.addVideo(4); - }); + manifest.addVariant(1, (variant) => { + variant.addAudio(3); + variant.addVideo(4); }); }); - periodIndex = 0; shaka.media.ManifestParser.registerParserByMime( fakeMimeType, () => new shaka.test.FakeManifestParser(manifest)); @@ -97,8 +90,7 @@ describe('Player', () => { drmEngine = new shaka.test.FakeDrmEngine(); playhead = new shaka.test.FakePlayhead(); - streamingEngine = new shaka.test.FakeStreamingEngine( - onChooseStreams, onCanSwitch); + streamingEngine = new shaka.test.FakeStreamingEngine(); mediaSourceEngine = { init: jasmine.createSpy('init').and.returnValue(Promise.resolve()), open: jasmine.createSpy('open').and.returnValue(Promise.resolve()), @@ -161,19 +153,17 @@ describe('Player', () => { expect(streamingEngine.destroy).toHaveBeenCalled(); const segmentIndexes = []; - for (const period of manifest.periods) { - for (const variant of period.variants) { - if (variant.audio) { - segmentIndexes.push(variant.audio.segmentIndex); - } - if (variant.video) { - segmentIndexes.push(variant.video.segmentIndex); - } + for (const variant of manifest.variants) { + if (variant.audio) { + segmentIndexes.push(variant.audio.segmentIndex); } - for (const textStream of period.textStreams) { - segmentIndexes.push(textStream.segmentIndex); + if (variant.video) { + segmentIndexes.push(variant.video.segmentIndex); } } + for (const textStream of manifest.textStreams) { + segmentIndexes.push(textStream.segmentIndex); + } for (const segmentIndex of segmentIndexes) { if (segmentIndex) { expect(segmentIndex.release).toHaveBeenCalled(); @@ -243,22 +233,20 @@ describe('Player', () => { // We must have two different sets of codecs for some of our tests. manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addAudio(1, (stream) => { - stream.mime('audio/mp4', 'mp4a.40.2'); - }); - variant.addVideo(2, (stream) => { - stream.mime('video/mp4', 'avc1.4d401f'); - }); + manifest.addVariant(0, (variant) => { + variant.addAudio(1, (stream) => { + stream.mime('audio/mp4', 'mp4a.40.2'); }); - period.addVariant(1, (variant) => { - variant.addAudio(3, (stream) => { - stream.mime('audio/webm', 'opus'); - }); - variant.addVideo(4, (stream) => { - stream.mime('video/webm', 'vp9'); - }); + variant.addVideo(2, (stream) => { + stream.mime('video/mp4', 'avc1.4d401f'); + }); + }); + manifest.addVariant(1, (variant) => { + variant.addAudio(3, (stream) => { + stream.mime('audio/webm', 'opus'); + }); + variant.addVideo(4, (stream) => { + stream.mime('video/webm', 'vp9'); }); }); }); @@ -305,33 +293,33 @@ describe('Player', () => { describe('setTextTrackVisibility', () => { beforeEach(() => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addAudio(1); - variant.addVideo(2); - }); - period.addTextStream(3, (stream) => { - stream.bandwidth = 100; - stream.kind = 'caption'; - stream.label = 'Spanish'; - stream.language = 'es'; - }); + manifest.addVariant(0, (variant) => { + variant.addAudio(1); + variant.addVideo(2); + }); + manifest.addTextStream(3, (stream) => { + stream.bandwidth = 100; + stream.kind = 'caption'; + stream.label = 'Spanish'; + stream.language = 'es'; }); }); }); it('load text stream if caption is visible', async () => { - await player.load(fakeManifestUri, 0, fakeMimeType); await player.setTextTrackVisibility(true); - expect(streamingEngine.loadNewTextStream).toHaveBeenCalled(); - expect(streamingEngine.getBufferingText()).not.toBe(null); + await player.load(fakeManifestUri, 0, fakeMimeType); + expect(streamingEngine.switchTextStream).toHaveBeenCalled(); + expect(shaka.test.Util.invokeSpy(streamingEngine.getCurrentTextStream)) + .not.toBe(null); }); it('does not load text stream if caption is invisible', async () => { - await player.load(fakeManifestUri, 0, fakeMimeType); await player.setTextTrackVisibility(false); - expect(streamingEngine.loadNewTextStream).not.toHaveBeenCalled(); - expect(streamingEngine.getBufferingText()).toBe(null); + await player.load(fakeManifestUri, 0, fakeMimeType); + expect(streamingEngine.switchTextStream).not.toHaveBeenCalled(); + expect(shaka.test.Util.invokeSpy(streamingEngine.getCurrentTextStream)) + .toBe(null); }); it('loads text stream if alwaysStreamText is set', async () => { @@ -339,14 +327,17 @@ describe('Player', () => { player.configure({streaming: {alwaysStreamText: true}}); await player.load(fakeManifestUri, 0, fakeMimeType); - expect(streamingEngine.getBufferingText()).not.toBe(null); + expect(streamingEngine.switchTextStream).toHaveBeenCalled(); + expect(shaka.test.Util.invokeSpy(streamingEngine.getCurrentTextStream)) + .not.toBe(null); + streamingEngine.switchTextStream.calls.reset(); await player.setTextTrackVisibility(true); - expect(streamingEngine.loadNewTextStream).not.toHaveBeenCalled(); + expect(streamingEngine.switchTextStream).not.toHaveBeenCalled(); expect(streamingEngine.unloadTextStream).not.toHaveBeenCalled(); await player.setTextTrackVisibility(false); - expect(streamingEngine.loadNewTextStream).not.toHaveBeenCalled(); + expect(streamingEngine.switchTextStream).not.toHaveBeenCalled(); expect(streamingEngine.unloadTextStream).not.toHaveBeenCalled(); }); }); @@ -655,10 +646,8 @@ describe('Player', () => { timeline.setStatic(true); manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.presentationTimeline = timeline; - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(1); - }); + manifest.addVariant(0, (variant) => { + variant.addVideo(1); }); }); goog.asserts.assert(manifest, 'manifest must be non-null'); @@ -669,9 +658,8 @@ describe('Player', () => { }); it('does not switch for plain configuration changes', async () => { - const switchVariantSpy = spyOn(player, 'switchVariant_'); - await player.load(fakeManifestUri, 0, fakeMimeType); + streamingEngine.switchVariant.calls.reset(); player.configure({abr: {enabled: false}}); player.configure({streaming: {bufferingGoal: 9001}}); @@ -679,7 +667,7 @@ describe('Player', () => { // Delay to ensure that the switch would have been called. await shaka.test.Util.shortDelay(); - expect(switchVariantSpy).not.toHaveBeenCalled(); + expect(streamingEngine.switchVariant).not.toHaveBeenCalled(); }); it('accepts parameters in a (fieldName, value) format', () => { @@ -800,23 +788,19 @@ describe('Player', () => { expect(abrManager.chooseVariant).toHaveBeenCalled(); }); - it('does not enable before stream startup', async () => { + it('enables automatically', async () => { await player.load(fakeManifestUri, 0, fakeMimeType); - expect(abrManager.enable).not.toHaveBeenCalled(); - streamingEngine.onCanSwitch(); expect(abrManager.enable).toHaveBeenCalled(); }); it('does not enable if adaptation is disabled', async () => { player.configure({abr: {enabled: false}}); await player.load(fakeManifestUri, 0, fakeMimeType); - streamingEngine.onCanSwitch(); expect(abrManager.enable).not.toHaveBeenCalled(); }); it('enables/disables though configure', async () => { await player.load(fakeManifestUri, 0, fakeMimeType); - streamingEngine.onCanSwitch(); abrManager.enable.calls.reset(); abrManager.disable.calls.reset(); @@ -827,16 +811,6 @@ describe('Player', () => { expect(abrManager.enable).toHaveBeenCalled(); }); - it('waits to enable if in-between Periods', async () => { - player.configure({abr: {enabled: false}}); - await player.load(fakeManifestUri, 0, fakeMimeType); - player.configure({abr: {enabled: true}}); - expect(abrManager.enable).not.toHaveBeenCalled(); - // Until onCanSwitch is called, the first period hasn't been set up yet. - streamingEngine.onCanSwitch(); - expect(abrManager.enable).toHaveBeenCalled(); - }); - it('reuses AbrManager instance', async () => { /** @type {!jasmine.Spy} */ const spy = @@ -875,58 +849,29 @@ describe('Player', () => { describe('filterTracks', () => { it('retains only video+audio variants if they exist', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(10, (variant) => { - variant.addAudio(1); - }); - period.addVariant(11, (variant) => { - variant.addAudio(2); - variant.addVideo(3); - }); - period.addVariant(12, (variant) => { - variant.addVideo(4); - }); + manifest.addVariant(10, (variant) => { + variant.addAudio(1); }); - manifest.addPeriod(1, (period) => { - period.addVariant(20, (variant) => { - variant.addAudio(5); - }); - period.addVariant(21, (variant) => { - variant.addVideo(6); - }); - period.addVariant(22, (variant) => { - variant.addAudio(7); - variant.addVideo(8); - }); + manifest.addVariant(11, (variant) => { + variant.addAudio(2); + variant.addVideo(3); + }); + manifest.addVariant(12, (variant) => { + variant.addVideo(4); }); }); - const variantTracks1 = [ + const variantTracks = [ jasmine.objectContaining({ id: 11, active: true, type: 'variant', }), ]; - const variantTracks2 = [ - jasmine.objectContaining({ - id: 22, - active: false, - type: 'variant', - }), - ]; await player.load(fakeManifestUri, 0, fakeMimeType); - // Check the first period's variant tracks. - const actualVariantTracks1 = player.getVariantTracks(); - expect(actualVariantTracks1).toEqual(variantTracks1); - - // Check the second period's variant tracks. - playhead.getTime.and.callFake(() => { - return 100; - }); - const actualVariantTracks2 = player.getVariantTracks(); - expect(actualVariantTracks2).toEqual(variantTracks2); + const actualVariantTracks = player.getVariantTracks(); + expect(actualVariantTracks).toEqual(variantTracks); }); }); @@ -939,146 +884,119 @@ describe('Player', () => { beforeEach(async () => { // A manifest we can use to test track expectations. manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(100, (variant) => { // main surround, low res - variant.bandwidth = 1300; - variant.language = 'en'; - variant.addVideo(1, (stream) => { - stream.originalId = 'video-1kbps'; - stream.bandwidth = 1000; - stream.width = 100; - stream.height = 200; - stream.frameRate = 1000000 / 42000; - stream.pixelAspectRatio = '59:54'; - stream.roles = ['main']; - }); - variant.addAudio(3, (stream) => { - stream.originalId = 'audio-en-6c'; - stream.bandwidth = 300; - stream.channelsCount = 6; - stream.audioSamplingRate = 48000; - stream.roles = ['main']; - }); - }); - period.addVariant(101, (variant) => { // main surround, high res - variant.bandwidth = 2300; - variant.language = 'en'; - variant.addVideo(2, (stream) => { - stream.originalId = 'video-2kbps'; - stream.bandwidth = 2000; - stream.frameRate = 24; - stream.pixelAspectRatio = '59:54'; - stream.size(200, 400); - }); - variant.addExistingStream(3); // audio - }); - period.addVariant(102, (variant) => { // main stereo, low res - variant.bandwidth = 1100; - variant.language = 'en'; - variant.addExistingStream(1); // video - variant.addAudio(4, (stream) => { - stream.originalId = 'audio-en-2c'; - stream.bandwidth = 100; - stream.channelsCount = 2; - stream.audioSamplingRate = 48000; - stream.roles = ['main']; - }); - }); - period.addVariant(103, (variant) => { // main stereo, high res - variant.bandwidth = 2100; - variant.language = 'en'; - variant.addExistingStream(2); // video - variant.addExistingStream(4); // audio - }); - period.addVariant(104, (variant) => { // commentary stereo, low res - variant.bandwidth = 1100; - variant.language = 'en'; - variant.addExistingStream(1); // video - variant.addAudio(5, (stream) => { - stream.originalId = 'audio-commentary'; - stream.bandwidth = 100; - stream.channelsCount = 2; - stream.audioSamplingRate = 48000; - stream.roles = ['commentary']; - }); - }); - period.addVariant(105, (variant) => { // commentary stereo, low res - variant.bandwidth = 2100; - variant.language = 'en'; - variant.addExistingStream(2); // video - variant.addExistingStream(5); // audio - }); - period.addVariant(106, (variant) => { // spanish stereo, low res - variant.language = 'es'; - variant.bandwidth = 1100; - variant.addExistingStream(1); // video - variant.addAudio(6, (stream) => { - stream.originalId = 'audio-es'; - stream.bandwidth = 100; - stream.channelsCount = 2; - stream.audioSamplingRate = 48000; - }); - }); - period.addVariant(107, (variant) => { // spanish stereo, high res - variant.language = 'es'; - variant.bandwidth = 2100; - variant.addExistingStream(2); // video - variant.addExistingStream(6); // audio + manifest.addVariant(100, (variant) => { // main surround, low res + variant.bandwidth = 1300; + variant.language = 'en'; + variant.addVideo(1, (stream) => { + stream.originalId = 'video-1kbps'; + stream.bandwidth = 1000; + stream.width = 100; + stream.height = 200; + stream.frameRate = 1000000 / 42000; + stream.pixelAspectRatio = '59:54'; + stream.roles = ['main']; }); - - // All text tracks should remain, even with different MIME types. - period.addTextStream(50, (stream) => { - stream.originalId = 'text-es'; - stream.language = 'es'; - stream.label = 'Spanish'; - stream.bandwidth = 10; - stream.mimeType = 'text/vtt'; - stream.kind = 'caption'; + variant.addAudio(3, (stream) => { + stream.originalId = 'audio-en-6c'; + stream.bandwidth = 300; + stream.channelsCount = 6; + stream.audioSamplingRate = 48000; + stream.roles = ['main']; }); - period.addTextStream(51, (stream) => { - stream.originalId = 'text-en'; - stream.language = 'en'; - stream.label = 'English'; - stream.bandwidth = 10; - stream.mimeType = 'application/ttml+xml'; - stream.kind = 'caption'; + }); + manifest.addVariant(101, (variant) => { // main surround, high res + variant.bandwidth = 2300; + variant.language = 'en'; + variant.addVideo(2, (stream) => { + stream.originalId = 'video-2kbps'; + stream.bandwidth = 2000; + stream.frameRate = 24; + stream.pixelAspectRatio = '59:54'; + stream.size(200, 400); + }); + variant.addExistingStream(3); // audio + }); + manifest.addVariant(102, (variant) => { // main stereo, low res + variant.bandwidth = 1100; + variant.language = 'en'; + variant.addExistingStream(1); // video + variant.addAudio(4, (stream) => { + stream.originalId = 'audio-en-2c'; + stream.bandwidth = 100; + stream.channelsCount = 2; + stream.audioSamplingRate = 48000; stream.roles = ['main']; }); - period.addTextStream(52, (stream) => { - stream.originalId = 'text-commentary'; - stream.language = 'en'; - stream.label = 'English'; - stream.bandwidth = 10; - stream.mimeType = 'application/ttml+xml'; - stream.kind = 'caption'; + }); + manifest.addVariant(103, (variant) => { // main stereo, high res + variant.bandwidth = 2100; + variant.language = 'en'; + variant.addExistingStream(2); // video + variant.addExistingStream(4); // audio + }); + manifest.addVariant(104, (variant) => { // commentary stereo, low res + variant.bandwidth = 1100; + variant.language = 'en'; + variant.addExistingStream(1); // video + variant.addAudio(5, (stream) => { + stream.originalId = 'audio-commentary'; + stream.bandwidth = 100; + stream.channelsCount = 2; + stream.audioSamplingRate = 48000; stream.roles = ['commentary']; }); }); - manifest.addPeriod(1, (period) => { - period.addVariant(200, (variant) => { - variant.bandwidth = 1100; - variant.language = 'en'; - variant.addVideo(10, (stream) => { - stream.bandwidth = 1000; - stream.size(100, 200); - }); - variant.addAudio(11, (stream) => { - stream.bandwidth = 100; - stream.channelsCount = 2; - stream.audioSamplingRate = 48000; - }); - }); - period.addVariant(201, (variant) => { - variant.bandwidth = 1300; - variant.language = 'en'; - variant.addExistingStream(10); // video - variant.addAudio(12, (stream) => { - stream.bandwidth = 300; - stream.channelsCount = 6; - stream.audioSamplingRate = 48000; - }); + manifest.addVariant(105, (variant) => { // commentary stereo, low res + variant.bandwidth = 2100; + variant.language = 'en'; + variant.addExistingStream(2); // video + variant.addExistingStream(5); // audio + }); + manifest.addVariant(106, (variant) => { // spanish stereo, low res + variant.language = 'es'; + variant.bandwidth = 1100; + variant.addExistingStream(1); // video + variant.addAudio(6, (stream) => { + stream.originalId = 'audio-es'; + stream.bandwidth = 100; + stream.channelsCount = 2; + stream.audioSamplingRate = 48000; }); }); + manifest.addVariant(107, (variant) => { // spanish stereo, high res + variant.language = 'es'; + variant.bandwidth = 2100; + variant.addExistingStream(2); // video + variant.addExistingStream(6); // audio + }); + + // All text tracks should remain, even with different MIME types. + manifest.addTextStream(50, (stream) => { + stream.originalId = 'text-es'; + stream.language = 'es'; + stream.label = 'Spanish'; + stream.bandwidth = 10; + stream.mimeType = 'text/vtt'; + stream.kind = 'caption'; + }); + manifest.addTextStream(51, (stream) => { + stream.originalId = 'text-en'; + stream.language = 'en'; + stream.label = 'English'; + stream.bandwidth = 10; + stream.mimeType = 'application/ttml+xml'; + stream.kind = 'caption'; + stream.roles = ['main']; + }); + manifest.addTextStream(52, (stream) => { + stream.originalId = 'text-commentary'; + stream.language = 'en'; + stream.label = 'English'; + stream.bandwidth = 10; + stream.mimeType = 'application/ttml+xml'; + stream.kind = 'caption'; + stream.roles = ['commentary']; + }); }); variantTracks = [ @@ -1417,17 +1335,18 @@ describe('Player', () => { }); await player.load(fakeManifestUri, 0, fakeMimeType); + streamingEngine.switchVariant.calls.reset(); + streamingEngine.switchTextStream.calls.reset(); }); it('returns the correct tracks', () => { - streamingEngine.onCanSwitch(); - expect(player.getVariantTracks()).toEqual(variantTracks); expect(player.getTextTracks()).toEqual(textTracks); }); it('returns empty arrays before tracks can be determined', async () => { const parser = new shaka.test.FakeManifestParser(manifest); + parser.start.and.callFake((manifestUri, playerInterface) => { // The player does not yet have a manifest. expect(player.getVariantTracks()).toEqual([]); @@ -1436,27 +1355,14 @@ describe('Player', () => { parser.playerInterface = playerInterface; return Promise.resolve(manifest); }); - drmEngine.initForPlayback.and.callFake(() => { - // The player does not yet have a playhead. - expect(player.getVariantTracks()).toEqual([]); - expect(player.getTextTracks()).toEqual([]); - - return Promise.resolve(); - }); - shaka.media.ManifestParser.registerParserByMime( - fakeMimeType, () => parser); await player.load(fakeManifestUri, 0, fakeMimeType); - // Make sure the interruptions didn't mess up the tracks. - streamingEngine.onCanSwitch(); expect(player.getVariantTracks()).toEqual(variantTracks); expect(player.getTextTracks()).toEqual(textTracks); }); it('doesn\'t disable AbrManager if switching variants', () => { - streamingEngine.onCanSwitch(); - let config = player.getConfiguration(); expect(config.abr.enabled).toBe(true); @@ -1468,8 +1374,6 @@ describe('Player', () => { }); it('doesn\'t disable AbrManager if switching text', () => { - streamingEngine.onCanSwitch(); - let config = player.getConfiguration(); expect(config.abr.enabled).toBe(true); @@ -1481,59 +1385,15 @@ describe('Player', () => { }); it('switches streams', () => { - streamingEngine.onCanSwitch(); - - const newTrack = player.getVariantTracks().filter((t) => !t.active)[0]; - player.selectVariantTrack(newTrack); - - expect(streamingEngine.switchVariant).toHaveBeenCalled(); - const variant = streamingEngine.switchVariant.calls.argsFor(0)[0]; - expect(variant.id).toBe(newTrack.id); - }); - - it('still switches streams if called during startup', () => { - // startup is not complete until onCanSwitch is called. - - // pick a track - const newTrack = player.getVariantTracks().filter((t) => !t.active)[0]; - // ask the player to switch to it - player.selectVariantTrack(newTrack); - // nothing happens yet - expect(streamingEngine.switchVariant).not.toHaveBeenCalled(); - - // after startup is complete, the manual selection takes effect. - streamingEngine.onCanSwitch(); - expect(streamingEngine.switchVariant).toHaveBeenCalled(); - const variant = streamingEngine.switchVariant.calls.argsFor(0)[0]; - expect(variant.id).toBe(newTrack.id); - }); - - it('still switches streams if called while switching Periods', () => { - // startup is complete after onCanSwitch. - streamingEngine.onCanSwitch(); - - // startup doesn't call switchVariant - expect(streamingEngine.switchVariant).not.toHaveBeenCalled(); - - // pick a track const newTrack = player.getVariantTracks().filter((t) => !t.active)[0]; - - // simulate the transition to period 1 - transitionPeriod(1); - - // select the new track (from period 0, which is fine) player.selectVariantTrack(newTrack); - expect(streamingEngine.switchVariant).not.toHaveBeenCalled(); - // after transition is completed by onCanSwitch, switchVariant is called - streamingEngine.onCanSwitch(); expect(streamingEngine.switchVariant).toHaveBeenCalled(); const variant = streamingEngine.switchVariant.calls.argsFor(0)[0]; expect(variant.id).toBe(newTrack.id); }); it('switching audio doesn\'t change selected text track', () => { - streamingEngine.onCanSwitch(); player.configure({ preferredTextLanguage: 'es', }); @@ -1542,7 +1402,6 @@ describe('Player', () => { const englishTextTrack = player.getTextTracks().filter((t) => t.language == 'en')[0]; - streamingEngine.switchTextStream.calls.reset(); player.selectTextTrack(englishTextTrack); expect(streamingEngine.switchTextStream).toHaveBeenCalled(); // We have selected an English text track explicitly. @@ -1559,13 +1418,10 @@ describe('Player', () => { it('selectAudioLanguage() takes precedence over ' + 'preferredAudioLanguage', () => { - streamingEngine.onCanSwitch(); - // This preference is set in beforeEach, before load(). expect(player.getConfiguration().preferredAudioLanguage).toBe('en'); expect(getActiveVariantTrack().language).toBe('en'); - streamingEngine.switchVariant.calls.reset(); player.selectAudioLanguage('es'); expect(streamingEngine.switchVariant).toHaveBeenCalled(); @@ -1576,10 +1432,8 @@ describe('Player', () => { }); it('selectAudioLanguage() respects selected role', () => { - streamingEngine.onCanSwitch(); expect(getActiveVariantTrack().roles).not.toContain('commentary'); - streamingEngine.switchVariant.calls.reset(); player.selectAudioLanguage('en', 'commentary'); expect(streamingEngine.switchVariant).toHaveBeenCalled(); @@ -1590,7 +1444,6 @@ describe('Player', () => { }); it('selectAudioLanguage() applies role only to audio', () => { - streamingEngine.onCanSwitch(); expect(getActiveVariantTrack().roles).not.toContain('commentary'); player.selectAudioLanguage('en', 'commentary'); let args = streamingEngine.switchVariant.calls.argsFor(0); @@ -1635,13 +1488,10 @@ describe('Player', () => { it('selectTextLanguage() takes precedence over ' + 'preferredTextLanguage', () => { - streamingEngine.onCanSwitch(); - // This preference is set in beforeEach, before load(). expect(player.getConfiguration().preferredTextLanguage).toBe('es'); expect(getActiveTextTrack().language).toBe('es'); - streamingEngine.switchTextStream.calls.reset(); player.selectTextLanguage('en'); expect(streamingEngine.switchTextStream).toHaveBeenCalled(); @@ -1651,10 +1501,8 @@ describe('Player', () => { }); it('selectTextLanguage() respects selected role', () => { - streamingEngine.onCanSwitch(); expect(getActiveTextTrack().roles).not.toContain('commentary'); - streamingEngine.switchTextStream.calls.reset(); player.selectTextLanguage('en', 'commentary'); expect(streamingEngine.switchTextStream).toHaveBeenCalled(); @@ -1664,8 +1512,6 @@ describe('Player', () => { }); it('changing current audio language changes active stream', () => { - streamingEngine.onCanSwitch(); - expect(getActiveVariantTrack().language).not.toBe('es'); expect(streamingEngine.switchVariant).not.toHaveBeenCalled(); player.selectAudioLanguage('es'); @@ -1678,8 +1524,6 @@ describe('Player', () => { }); it('changing current text language changes active stream', () => { - streamingEngine.onCanSwitch(); - expect(getActiveTextTrack().language).not.toBe('en'); expect(streamingEngine.switchTextStream).not.toHaveBeenCalled(); player.selectTextLanguage('en'); @@ -1692,20 +1536,18 @@ describe('Player', () => { // https://github.com/google/shaka-player/issues/2010 it('changing text lang changes active stream when not streaming', () => { - streamingEngine.onCanSwitch(); player.setTextTrackVisibility(false); - expect(getActiveTextTrack().language).not.toBe('en'); + expect(getActiveTextTrack()).toBe(null); expect(streamingEngine.switchTextStream).not.toHaveBeenCalled(); player.selectTextLanguage('en'); + player.setTextTrackVisibility(true); - expect(streamingEngine.switchTextStream).not.toHaveBeenCalled(); + expect(streamingEngine.switchTextStream).toHaveBeenCalled(); expect(getActiveTextTrack().language).toBe('en'); }); it('remembers the channel count when ABR is reenabled', () => { - streamingEngine.onCanSwitch(); - // We prefer 6 channels, and we are currently playing 6 channels. expect(player.getConfiguration().preferredAudioChannelCount).toBe(6); expect(getActiveVariantTrack().channelsCount).toBe(6); @@ -1741,15 +1583,13 @@ describe('Player', () => { // Simulate an encrypted stream. Mark half of the audio streams with key // ID 'aaa', and the other half with 'bbb'. Remove all roles, so that our // choices are limited only by channel count and key status. - for (const variant of manifest.periods[0].variants) { + for (const variant of manifest.variants) { const keyId = (variant.audio.id % 2) ? 'aaa' : 'bbb'; variant.audio.keyId = keyId; variant.video.roles = []; variant.audio.roles = []; } - streamingEngine.onCanSwitch(); - // We prefer 6 channels, and we are currently playing 6 channels. expect(player.getConfiguration().preferredAudioChannelCount).toBe(6); expect(getActiveVariantTrack().channelsCount).toBe(6); @@ -1783,6 +1623,98 @@ describe('Player', () => { // See that we are still playing a 2-channel track. expect(getActiveVariantTrack().channelsCount).toBe(2); }); + + describe('only fires change event when something changes', () => { + /** @type {jasmine.Spy} */ + let textChanged; + + /** @type {jasmine.Spy} */ + let variantChanged; + + beforeEach(() => { + textChanged = jasmine.createSpy('textChanged'); + player.addEventListener('textchanged', Util.spyFunc(textChanged)); + + variantChanged = jasmine.createSpy('variantChanged'); + player.addEventListener('variantchanged', Util.spyFunc(variantChanged)); + }); + + it('in selectTextTrack', async () => { + // Any text track we're not already streaming. + const newTrack = player.getTextTracks().filter((t) => !t.active)[0]; + + // Call selectTextTrack with a new track. Expect an event to fire. + player.selectTextTrack(newTrack); + await shaka.test.Util.shortDelay(); + expect(textChanged).toHaveBeenCalled(); + textChanged.calls.reset(); + + // Call again with the same track, and expect no event to fire, since + // nothing changed this time. + player.selectTextTrack(newTrack); + await shaka.test.Util.shortDelay(); + expect(textChanged).not.toHaveBeenCalled(); + }); + + it('in selectVariantTrack', async () => { + // Any variant track we're not already streaming. + const newTrack = player.getVariantTracks().filter((t) => !t.active)[0]; + + // Call selectVariantTrack with a new track. Expect an event to fire. + player.selectVariantTrack(newTrack); + await shaka.test.Util.shortDelay(); + expect(variantChanged).toHaveBeenCalled(); + variantChanged.calls.reset(); + + // Call again with the same track, and expect no event to fire, since + // nothing changed this time. + player.selectVariantTrack(newTrack); + await shaka.test.Util.shortDelay(); + expect(variantChanged).not.toHaveBeenCalled(); + }); + + it('in selectTextLanguage', async () => { + // The current text language. + const currentLanguage = player.getTextTracks() + .filter((t) => t.active)[0].language; + const newLanguage = player.getTextTracks() + .filter((t) => t.language != currentLanguage)[0].language; + + // Call selectTextLanguage with a new language. Expect an event to + // fire. + player.selectTextLanguage(newLanguage); + await shaka.test.Util.shortDelay(); + expect(textChanged).toHaveBeenCalled(); + textChanged.calls.reset(); + + // Call again with the same language, and expect no event to fire, + // since nothing changed this time. + player.selectTextLanguage(newLanguage); + await shaka.test.Util.shortDelay(); + expect(textChanged).not.toHaveBeenCalled(); + }); + + it('in selectAudioLanguage', async () => { + // The current audio language. + const currentLanguage = player.getVariantTracks() + .filter((t) => t.active)[0].language; + const newLanguage = player.getVariantTracks() + .filter((t) => t.language != currentLanguage)[0].language; + + // Call selectAudioLanguage with a new language. Expect an event to + // fire. + player.selectAudioLanguage(newLanguage); + await shaka.test.Util.shortDelay(); + expect(variantChanged).toHaveBeenCalled(); + variantChanged.calls.reset(); + + // Call again with the same language, and expect no event to fire, + // since nothing changed this time. + player.selectAudioLanguage(newLanguage); + await shaka.test.Util.shortDelay(); + expect(variantChanged).not.toHaveBeenCalled(); + }); + }); }); // describe('tracks') describe('languages', () => { @@ -1813,21 +1745,19 @@ describe('Player', () => { it('enables text if its language differs from audio at start', async () => { // A manifest we can use to test text visibility. manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.language = 'pt'; - variant.addAudio(0); - }); - period.addVariant(1, (variant) => { - variant.language = 'en'; - variant.addAudio(1); - }); - period.addTextStream(2, (stream) => { - stream.language = 'pt'; - }); - period.addTextStream(3, (stream) => { - stream.language = 'fr'; - }); + manifest.addVariant(0, (variant) => { + variant.language = 'pt'; + variant.addAudio(0); + }); + manifest.addVariant(1, (variant) => { + variant.language = 'en'; + variant.addAudio(1); + }); + manifest.addTextStream(2, (stream) => { + stream.language = 'pt'; + }); + manifest.addTextStream(3, (stream) => { + stream.language = 'fr'; }); }); @@ -1858,15 +1788,13 @@ describe('Player', () => { // The Player shouldn't allow changing between languages, so it should // choose an arbitrary language when none is given. manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.language = 'pt'; - variant.addAudio(0); - }); - period.addVariant(1, (variant) => { - variant.language = 'en'; - variant.addAudio(1); - }); + manifest.addVariant(0, (variant) => { + variant.language = 'pt'; + variant.addAudio(0); + }); + manifest.addVariant(1, (variant) => { + variant.language = 'en'; + variant.addAudio(1); }); }); @@ -1893,23 +1821,21 @@ describe('Player', () => { async function runTest(languages, preference, expectedIndex) { // A manifest we can use to test language selection. manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - const enumerate = (it) => shaka.util.Iterables.enumerate(it); - for (const {i, item: lang} of enumerate(languages)) { - if (lang.charAt(0) == '*') { - period.addVariant(i, (variant) => { - variant.primary = true; - variant.language = lang.substr(1); - variant.addAudio(i); - }); - } else { - period.addVariant(i, (variant) => { - variant.language = lang; - variant.addAudio(i); - }); - } + const enumerate = (it) => shaka.util.Iterables.enumerate(it); + for (const {i, item: lang} of enumerate(languages)) { + if (lang.charAt(0) == '*') { + manifest.addVariant(i, (variant) => { + variant.primary = true; + variant.language = lang.substr(1); + variant.addAudio(i); + }); + } else { + manifest.addVariant(i, (variant) => { + variant.language = lang; + variant.addAudio(i); + }); } - }); + } }); // Set the user preferences, which must happen before load(). @@ -1920,7 +1846,7 @@ describe('Player', () => { await player.load(fakeManifestUri, 0, fakeMimeType); expect(getActiveVariantTrack().id).toBe(expectedIndex); } - }); + }); // describe('languages') describe('getStats', () => { const oldDateNow = Date.now; @@ -1934,44 +1860,39 @@ describe('Player', () => { // A manifest we can use to test stats. manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.bandwidth = 200; - variant.addAudio(1, (stream) => { - stream.bandwidth = 100; - }); - variant.addVideo(2, (stream) => { - stream.bandwidth = 100; - stream.size(100, 200); - }); + manifest.addVariant(0, (variant) => { + variant.bandwidth = 200; + variant.addAudio(1, (stream) => { + stream.bandwidth = 100; }); - period.addVariant(1, (variant) => { - variant.bandwidth = 300; - variant.addExistingStream(1); // audio - variant.addVideo(3, (stream) => { - stream.bandwidth = 200; - stream.size(200, 400); - }); + variant.addVideo(2, (stream) => { + stream.bandwidth = 100; + stream.size(100, 200); }); - period.addVariant(2, (variant) => { - variant.bandwidth = 300; - variant.addAudio(4, (stream) => { - stream.bandwidth = 200; - }); - variant.addExistingStream(2); // video + }); + manifest.addVariant(1, (variant) => { + variant.bandwidth = 300; + variant.addExistingStream(1); // audio + variant.addVideo(3, (stream) => { + stream.bandwidth = 200; + stream.size(200, 400); }); - period.addVariant(3, (variant) => { - variant.bandwidth = 400; - variant.addExistingStream(4); // audio - variant.addExistingStream(3); // video + }); + manifest.addVariant(2, (variant) => { + variant.bandwidth = 300; + variant.addAudio(4, (stream) => { + stream.bandwidth = 200; }); + variant.addExistingStream(2); // video + }); + manifest.addVariant(3, (variant) => { + variant.bandwidth = 400; + variant.addExistingStream(4); // audio + variant.addExistingStream(3); // video }); }); await player.load(fakeManifestUri, 0, fakeMimeType); - - // Initialize the fake streams. - streamingEngine.onCanSwitch(); }); afterEach(() => { @@ -2069,7 +1990,7 @@ describe('Player', () => { it('includes selectVariantTrack choices', () => { const track = player.getVariantTracks()[3]; - const variants = manifest.periods[0].variants; + const variants = manifest.variants; const variant = variants.find((variant) => variant.id == track.id); player.selectVariantTrack(track); @@ -2085,7 +2006,7 @@ describe('Player', () => { }); it('includes adaptation choices', () => { - const variant = manifest.periods[0].variants[3]; + const variant = manifest.variants[3]; switch_(variant); checkHistory(jasmine.arrayContaining([ @@ -2247,233 +2168,85 @@ describe('Player', () => { } }); - describe('unplayable periods', () => { - beforeEach(() => { - // overriding for good / bad codecs. - window.MediaSource.isTypeSupported = - (mimeType) => mimeType.includes('good'); - }); + describe('unplayable content', () => { + it('throws CONTENT_UNSUPPORTED_BY_BROWSER', async () => { + window.MediaSource.isTypeSupported = (mimeType) => false; - it('success when one period is playable', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(0, (stream) => { - stream.mime('video/mp4', 'good'); - }); - }); + manifest.addVariant(0, (variant) => { + variant.addVideo(0); }); }); - await player.load(fakeManifestUri, 0, fakeMimeType); + const expected = Util.jasmineError(new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.MANIFEST, + shaka.util.Error.Code.CONTENT_UNSUPPORTED_BY_BROWSER)); + const load = player.load(fakeManifestUri, 0, fakeMimeType); + await expectAsync(load).toBeRejectedWith(expected); }); + }); - it('success when all periods are playable', async () => { + describe('restrictions', () => { + it('switches if active is restricted by application', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(0, (stream) => { - stream.mime('video/mp4', 'good'); - }); - }); + manifest.addVariant(0, (variant) => { + variant.bandwidth = 500; + variant.addVideo(1); }); - manifest.addPeriod(1, (period) => { - period.addVariant(1, (variant) => { - variant.addVideo(1, (stream) => { - stream.mime('video/mp4', 'good'); - }); - }); + manifest.addVariant(1, (variant) => { + variant.bandwidth = 100; + variant.addVideo(2); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); + let activeVariant = getActiveVariantTrack(); + expect(activeVariant.id).toBe(0); + + // Ask AbrManager to choose the 0th variant from those it is given. + abrManager.chooseIndex = 0; + abrManager.chooseVariant.calls.reset(); + + // This restriction should make it so that the first variant (bandwidth + // 500, id 0) cannot be selected. + player.configure({ + restrictions: {maxBandwidth: 200}, + }); + + // The restriction change should trigger a call to AbrManager. + expect(abrManager.chooseVariant).toHaveBeenCalled(); + + // The first variant is disallowed. + expect(manifest.variants[0].id).toBe(0); + expect(manifest.variants[0].allowedByApplication) + .toBe(false); + + // AbrManager chose the second variant (id 1). + activeVariant = getActiveVariantTrack(); + expect(activeVariant.id).toBe(1); }); - it('throw UNPLAYABLE_PERIOD when some periods are unplayable', async () => { + it('updates AbrManager for restriction changes', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(0, (stream) => { - stream.mime('video/mp4', 'good'); - }); - }); + manifest.addVariant(1, (variant) => { + variant.bandwidth = 500; + variant.addVideo(10); }); - manifest.addPeriod(1, (period) => { - period.addVariant(1, (variant) => { - variant.addVideo(1, (stream) => { - stream.mime('video/mp4', 'bad'); - }); - }); + manifest.addVariant(2, (variant) => { + variant.bandwidth = 100; + variant.addVideo(20); }); }); - const expected = Util.jasmineError(new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.UNPLAYABLE_PERIOD)); - const load = player.load(fakeManifestUri, 0, fakeMimeType); - await expectAsync(load).toBeRejectedWith(expected); - }); - - it('throw CONTENT_UNSUPPORTED_BY_BROWSER when the only period is ' + - 'unplayable', async () => { - manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(0, (stream) => { - stream.mime('video/mp4', 'bad'); - }); - }); - }); - }); - - const expected = Util.jasmineError(new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.CONTENT_UNSUPPORTED_BY_BROWSER)); - const load = player.load(fakeManifestUri, 0, fakeMimeType); - await expectAsync(load).toBeRejectedWith(expected); - }); - - it('throw CONTENT_UNSUPPORTED_BY_BROWSER when all periods are unplayable', - async () => { - manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(0, (stream) => { - stream.mime('video/mp4', 'bad'); - }); - }); - period.addVariant(1, (variant) => { - variant.addVideo(1, (stream) => { - stream.mime('video/mp4', 'bad'); - }); - }); - }); - manifest.addPeriod(1, (period) => { - period.addVariant(2, (variant) => { - variant.addVideo(2, (stream) => { - stream.mime('video/mp4', 'bad'); - }); - }); - }); - }); - - const expected = Util.jasmineError(new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.CONTENT_UNSUPPORTED_BY_BROWSER)); - const load = player.load(fakeManifestUri, 0, fakeMimeType); - await expectAsync(load).toBeRejectedWith(expected); - }); - - it('throw UNPLAYABLE_PERIOD when the new period unplayable', async () => { - manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(0, (stream) => { - stream.mime('video/mp4', 'good'); - }); - }); - period.addVariant(1, (variant) => { - variant.addVideo(1, (stream) => { - stream.mime('video/mp4', 'good'); - }); - }); - }); - }); - - /** @type {!shaka.test.FakeManifestParser} */ - const parser = new shaka.test.FakeManifestParser(manifest); - shaka.media.ManifestParser.registerParserByMime( - fakeMimeType, () => parser); await player.load(fakeManifestUri, 0, fakeMimeType); - - const manifest2 = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(10, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(0, (stream) => { - stream.mime('video/mp4', 'bad'); - }); - }); - }); - }); - manifest.periods.push(manifest2.periods[0]); - - const expected = Util.jasmineError(new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.UNPLAYABLE_PERIOD)); - const test = - () => parser.playerInterface.filterNewPeriod(manifest2.periods[0]); - expect(test).toThrow(expected); - }); - }); - - describe('restrictions', () => { - it('switches if active is restricted by application', async () => { - manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.bandwidth = 500; - variant.addVideo(1); - }); - period.addVariant(1, (variant) => { - variant.bandwidth = 100; - variant.addVideo(2); - }); - }); - }); - - await setupPlayer(); - let activeVariant = getActiveVariantTrack(); - expect(activeVariant.id).toBe(0); - - // Ask AbrManager to choose the 0th variant from those it is given. - abrManager.chooseIndex = 0; - abrManager.chooseVariant.calls.reset(); - - // This restriction should make it so that the first variant (bandwidth - // 500, id 0) cannot be selected. - player.configure({ - restrictions: {maxBandwidth: 200}, - }); - - // The restriction change should trigger a call to AbrManager. - expect(abrManager.chooseVariant).toHaveBeenCalled(); - - // The first variant is disallowed. - expect(manifest.periods[0].variants[0].id).toBe(0); - expect(manifest.periods[0].variants[0].allowedByApplication) - .toBe(false); - - // AbrManager chose the second variant (id 1). - activeVariant = getActiveVariantTrack(); - expect(activeVariant.id).toBe(1); - }); - - it('updates AbrManager for restriction changes', async () => { - manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(1, (variant) => { - variant.bandwidth = 500; - variant.addVideo(10); - }); - period.addVariant(2, (variant) => { - variant.bandwidth = 100; - variant.addVideo(20); - }); - }); - }); - - await setupPlayer(); abrManager.setVariants.calls.reset(); player.configure({restrictions: {maxBandwidth: 200}}); // AbrManager should have been updated with the restricted tracks. // The first variant is disallowed. - expect(abrManager.setVariants).toHaveBeenCalledTimes(1); + expect(abrManager.setVariants).toHaveBeenCalled(); const variants = abrManager.setVariants.calls.argsFor(0)[0]; expect(variants.length).toBe(1); expect(variants[0].id).toBe(2); @@ -2489,19 +2262,17 @@ describe('Player', () => { it('switches if active key status is "output-restricted"', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(1, (stream) => { - stream.keyId = 'abc'; - }); - }); - period.addVariant(1, (variant) => { - variant.addVideo(2); + manifest.addVariant(0, (variant) => { + variant.addVideo(1, (stream) => { + stream.keyIds = ['abc']; }); }); + manifest.addVariant(1, (variant) => { + variant.addVideo(2); + }); }); - await setupPlayer(); + await player.load(fakeManifestUri, 0, fakeMimeType); let activeVariant = getActiveVariantTrack(); expect(activeVariant.id).toBe(0); @@ -2513,9 +2284,8 @@ describe('Player', () => { expect(abrManager.chooseVariant).toHaveBeenCalled(); // The first variant is disallowed. - expect(manifest.periods[0].variants[0].id).toBe(0); - expect(manifest.periods[0].variants[0].allowedByKeySystem) - .toBe(false); + expect(manifest.variants[0].id).toBe(0); + expect(manifest.variants[0].allowedByKeySystem).toBe(false); // The second variant was chosen. activeVariant = getActiveVariantTrack(); @@ -2524,19 +2294,17 @@ describe('Player', () => { it('switches if active key status is "internal-error"', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(1, (stream) => { - stream.keyId = 'abc'; - }); - }); - period.addVariant(1, (variant) => { - variant.addVideo(2); + manifest.addVariant(0, (variant) => { + variant.addVideo(1, (stream) => { + stream.keyIds = ['abc']; }); }); + manifest.addVariant(1, (variant) => { + variant.addVideo(2); + }); }); - await setupPlayer(); + await player.load(fakeManifestUri, 0, fakeMimeType); let activeVariant = getActiveVariantTrack(); expect(activeVariant.id).toBe(0); @@ -2546,9 +2314,8 @@ describe('Player', () => { abrManager.chooseVariant.calls.reset(); onKeyStatus({'abc': 'internal-error'}); expect(abrManager.chooseVariant).toHaveBeenCalled(); - expect(manifest.periods[0].variants[0].id).toBe(0); - expect(manifest.periods[0].variants[0].allowedByKeySystem) - .toBe(false); + expect(manifest.variants[0].id).toBe(0); + expect(manifest.variants[0].allowedByKeySystem).toBe(false); activeVariant = getActiveVariantTrack(); expect(activeVariant.id).toBe(1); @@ -2556,19 +2323,17 @@ describe('Player', () => { it('doesn\'t switch if the active stream isn\'t restricted', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(1, (stream) => { - stream.keyId = 'abc'; - }); - }); - period.addVariant(1, (variant) => { - variant.addVideo(2); + manifest.addVariant(0, (variant) => { + variant.addVideo(1, (stream) => { + stream.keyIds = ['abc']; }); }); + manifest.addVariant(1, (variant) => { + variant.addVideo(2); + }); }); - await setupPlayer(); + await player.load(fakeManifestUri, 0, fakeMimeType); abrManager.chooseVariant.calls.reset(); let activeVariant = getActiveVariantTrack(); @@ -2583,19 +2348,17 @@ describe('Player', () => { it('removes if key status is "output-restricted"', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(1, (stream) => { - stream.keyId = 'abc'; - }); - }); - period.addVariant(1, (variant) => { - variant.addVideo(2); + manifest.addVariant(0, (variant) => { + variant.addVideo(1, (stream) => { + stream.keyIds = ['abc']; }); }); + manifest.addVariant(1, (variant) => { + variant.addVideo(2); + }); }); - await setupPlayer(); + await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(2); onKeyStatus({'abc': 'output-restricted'}); @@ -2607,19 +2370,17 @@ describe('Player', () => { it('removes if key status is "internal-error"', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(1, (stream) => { - stream.keyId = 'abc'; - }); - }); - period.addVariant(1, (variant) => { - variant.addVideo(2); + manifest.addVariant(0, (variant) => { + variant.addVideo(1, (stream) => { + stream.keyIds = ['abc']; }); }); + manifest.addVariant(1, (variant) => { + variant.addVideo(2); + }); }); - await setupPlayer(); + await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(2); onKeyStatus({'abc': 'internal-error'}); @@ -2631,19 +2392,17 @@ describe('Player', () => { it('removes if we don\'t have the required key', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(1, (stream) => { - stream.keyId = 'abc'; - }); - }); - period.addVariant(2, (variant) => { - variant.addVideo(3); + manifest.addVariant(0, (variant) => { + variant.addVideo(1, (stream) => { + stream.keyIds = ['abc']; }); }); + manifest.addVariant(2, (variant) => { + variant.addVideo(3); + }); }); - await setupPlayer(); + await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(2); // We have some key statuses, but not for the key IDs we know. @@ -2654,53 +2413,46 @@ describe('Player', () => { expect(tracks[0].id).toBe(2); }); - // https://github.com/google/shaka-player/issues/2135 - it('updates key statuses for multi-Period content', async () => { + it('updates key statuses for multi-key content', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(1, (stream) => { - stream.keyId = 'abc'; - }); + manifest.addVariant(0, (variant) => { + variant.addVideo(1, (stream) => { + stream.keyIds = ['abc']; }); }); - manifest.addPeriod(10, (period) => { - period.addVariant(2, (variant) => { - variant.addVideo(3, (stream) => { - stream.keyId = 'abc'; - }); + manifest.addVariant(2, (variant) => { + variant.addVideo(3, (stream) => { + stream.keyIds = ['def']; }); - period.addVariant(4, (variant) => { - variant.addVideo(5, (stream) => { - stream.keyId = 'def'; - }); + }); + manifest.addVariant(4, (variant) => { + variant.addVideo(5, (stream) => { + stream.keyIds = ['abc', 'def']; }); }); }); - await setupPlayer(); + await player.load(fakeManifestUri, 0, fakeMimeType); onKeyStatus({'abc': 'usable'}); - expect(manifest.periods[0].variants[0].allowedByKeySystem).toBe(true); - expect(manifest.periods[1].variants[0].allowedByKeySystem).toBe(true); - expect(manifest.periods[1].variants[1].allowedByKeySystem).toBe(false); + expect(manifest.variants[0].allowedByKeySystem).toBe(true); + expect(manifest.variants[1].allowedByKeySystem).toBe(false); + expect(manifest.variants[2].allowedByKeySystem).toBe(false); }); it('does not restrict if no key statuses are available', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(1, (stream) => { - stream.keyId = 'abc'; - }); - }); - period.addVariant(2, (variant) => { - variant.addVideo(3); + manifest.addVariant(0, (variant) => { + variant.addVideo(1, (stream) => { + stream.keyIds = ['abc']; }); }); + manifest.addVariant(2, (variant) => { + variant.addVideo(3); + }); }); - await setupPlayer(); + await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(2); // This simulates, for example, the lack of key status on Chromecast @@ -2713,19 +2465,17 @@ describe('Player', () => { it('doesn\'t remove when using synthetic key status', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(1, (stream) => { - stream.keyId = 'abc'; - }); - }); - period.addVariant(2, (variant) => { - variant.addVideo(3); + manifest.addVariant(0, (variant) => { + variant.addVideo(1, (stream) => { + stream.keyIds = ['abc']; }); }); + manifest.addVariant(2, (variant) => { + variant.addVideo(3); + }); }); - await setupPlayer(); + await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(2); // A synthetic key status contains a single key status with key '00'. @@ -2737,24 +2487,22 @@ describe('Player', () => { it('removes all encrypted tracks for errors with synthetic key status', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(1, (stream) => { - stream.keyId = 'abc'; - }); - }); - period.addVariant(2, (variant) => { - variant.addVideo(3, (stream) => { - stream.keyId = 'xyz'; - }); + manifest.addVariant(0, (variant) => { + variant.addVideo(1, (stream) => { + stream.keyIds = ['abc']; }); - period.addVariant(4, (variant) => { - variant.addVideo(5); + }); + manifest.addVariant(2, (variant) => { + variant.addVideo(3, (stream) => { + stream.keyIds = ['xyz']; }); }); + manifest.addVariant(4, (variant) => { + variant.addVideo(5); + }); }); - await setupPlayer(); + await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(3); // A synthetic key status contains a single key status with key '00'. @@ -2767,19 +2515,17 @@ describe('Player', () => { it('removes if key system does not support codec', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addDrmInfo('foo.bar'); - variant.addVideo(1, (stream) => { - stream.encrypted = true; - stream.mimeType = 'video/unsupported'; - }); + manifest.addVariant(0, (variant) => { + variant.addDrmInfo('foo.bar'); + variant.addVideo(1, (stream) => { + stream.encrypted = true; + stream.mimeType = 'video/unsupported'; }); - period.addVariant(1, (variant) => { - variant.addDrmInfo('foo.bar'); - variant.addVideo(2, (stream) => { - stream.encrypted = true; - }); + }); + manifest.addVariant(1, (variant) => { + variant.addDrmInfo('foo.bar'); + variant.addVideo(2, (stream) => { + stream.encrypted = true; }); }); }); @@ -2795,7 +2541,7 @@ describe('Player', () => { return variant.video.mimeType != 'video/unsupported'; }); - await setupPlayer(); + await player.load(fakeManifestUri, 0, fakeMimeType); const tracks = player.getVariantTracks(); expect(tracks.length).toBe(1); expect(tracks[0].id).toBe(1); @@ -2803,23 +2549,21 @@ describe('Player', () => { it('removes based on bandwidth', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.bandwidth = 10; - variant.addVideo(1); - }); - period.addVariant(1, (variant) => { - variant.bandwidth = 1500; - variant.addVideo(2); - }); - period.addVariant(2, (variant) => { - variant.bandwidth = 500; - variant.addVideo(3); - }); + manifest.addVariant(0, (variant) => { + variant.bandwidth = 10; + variant.addVideo(1); + }); + manifest.addVariant(1, (variant) => { + variant.bandwidth = 1500; + variant.addVideo(2); + }); + manifest.addVariant(2, (variant) => { + variant.bandwidth = 500; + variant.addVideo(3); }); }); - await setupPlayer(); + await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(3); player.configure({restrictions: {minBandwidth: 100, maxBandwidth: 1000}}); @@ -2831,26 +2575,24 @@ describe('Player', () => { it('removes based on pixels', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(1, (stream) => { - stream.size(900, 900); - }); + manifest.addVariant(0, (variant) => { + variant.addVideo(1, (stream) => { + stream.size(900, 900); }); - period.addVariant(1, (variant) => { - variant.addVideo(2, (stream) => { - stream.size(5, 5); - }); + }); + manifest.addVariant(1, (variant) => { + variant.addVideo(2, (stream) => { + stream.size(5, 5); }); - period.addVariant(2, (variant) => { - variant.addVideo(3, (stream) => { - stream.size(190, 190); - }); + }); + manifest.addVariant(2, (variant) => { + variant.addVideo(3, (stream) => { + stream.size(190, 190); }); }); }); - await setupPlayer(); + await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(3); player.configure({restrictions: {minPixels: 100, maxPixels: 800 * 800}}); @@ -2862,26 +2604,24 @@ describe('Player', () => { it('removes based on width', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(1, (stream) => { - stream.size(5, 5); - }); + manifest.addVariant(0, (variant) => { + variant.addVideo(1, (stream) => { + stream.size(5, 5); }); - period.addVariant(1, (variant) => { - variant.addVideo(2, (stream) => { - stream.size(1500, 200); - }); + }); + manifest.addVariant(1, (variant) => { + variant.addVideo(2, (stream) => { + stream.size(1500, 200); }); - period.addVariant(2, (variant) => { - variant.addVideo(3, (stream) => { - stream.size(190, 190); - }); + }); + manifest.addVariant(2, (variant) => { + variant.addVideo(3, (stream) => { + stream.size(190, 190); }); }); }); - await setupPlayer(); + await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(3); player.configure({restrictions: {minWidth: 100, maxWidth: 1000}}); @@ -2893,26 +2633,26 @@ describe('Player', () => { it('removes based on height', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(1, (stream) => { - stream.size(5, 5); - }); + manifest.addVariant(0, (variant) => { + variant.addVideo(1, (stream) => { + stream.size(5, 5); }); - period.addVariant(1, (variant) => { - variant.addVideo(2, (stream) => { - stream.size(200, 1500); - }); + }); + + manifest.addVariant(1, (variant) => { + variant.addVideo(2, (stream) => { + stream.size(200, 1500); }); - period.addVariant(2, (variant) => { - variant.addVideo(3, (stream) => { - stream.size(190, 190); - }); + }); + + manifest.addVariant(2, (variant) => { + variant.addVideo(3, (stream) => { + stream.size(190, 190); }); }); }); - await setupPlayer(); + await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(3); player.configure({restrictions: {minHeight: 100, maxHeight: 1000}}); @@ -2924,23 +2664,22 @@ describe('Player', () => { it('removes the whole variant if one stream is restricted', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(1, (stream) => { - stream.size(5, 5); - }); - variant.addAudio(2); + manifest.addVariant(0, (variant) => { + variant.addVideo(1, (stream) => { + stream.size(5, 5); }); - period.addVariant(1, (variant) => { - variant.addVideo(3, (stream) => { - stream.size(190, 190); - }); - variant.addAudio(4); + variant.addAudio(2); + }); + + manifest.addVariant(1, (variant) => { + variant.addVideo(3, (stream) => { + stream.size(190, 190); }); + variant.addAudio(4); }); }); - await setupPlayer(); + await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(2); player.configure({restrictions: {minHeight: 100, maxHeight: 1000}}); @@ -2952,26 +2691,26 @@ describe('Player', () => { it('issues error if no streams are playable', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(1, (stream) => { - stream.size(5, 5); - }); + manifest.addVariant(0, (variant) => { + variant.addVideo(1, (stream) => { + stream.size(5, 5); }); - period.addVariant(1, (variant) => { - variant.addVideo(2, (stream) => { - stream.size(200, 300); - }); + }); + + manifest.addVariant(1, (variant) => { + variant.addVideo(2, (stream) => { + stream.size(200, 300); }); - period.addVariant(2, (variant) => { - variant.addVideo(3, (stream) => { - stream.size(190, 190); - }); + }); + + manifest.addVariant(2, (variant) => { + variant.addVideo(3, (stream) => { + stream.size(190, 190); }); }); }); - await setupPlayer(); + await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(3); onError.and.callFake((e) => { @@ -2994,50 +2733,48 @@ describe('Player', () => { it('chooses efficient codecs and removes the rest', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - // More efficient codecs - period.addVariant(0, (variant) => { - variant.bandwidth = 100; - variant.addVideo(0, (stream) => { - stream.codecs = 'good'; - }); + // More efficient codecs + manifest.addVariant(0, (variant) => { + variant.bandwidth = 100; + variant.addVideo(0, (stream) => { + stream.codecs = 'good'; }); - period.addVariant(1, (variant) => { - variant.bandwidth = 200; - variant.addVideo(1, (stream) => { - stream.codecs = 'good'; - }); + }); + manifest.addVariant(1, (variant) => { + variant.bandwidth = 200; + variant.addVideo(1, (stream) => { + stream.codecs = 'good'; }); - period.addVariant(2, (variant) => { - variant.bandwidth = 300; - variant.addVideo(2, (stream) => { - stream.codecs = 'good'; - }); + }); + manifest.addVariant(2, (variant) => { + variant.bandwidth = 300; + variant.addVideo(2, (stream) => { + stream.codecs = 'good'; }); + }); - // Less efficient codecs - period.addVariant(3, (variant) => { - variant.bandwidth = 10000; - variant.addVideo(3, (stream) => { - stream.codecs = 'bad'; - }); + // Less efficient codecs + manifest.addVariant(3, (variant) => { + variant.bandwidth = 10000; + variant.addVideo(3, (stream) => { + stream.codecs = 'bad'; }); - period.addVariant(4, (variant) => { - variant.bandwidth = 20000; - variant.addVideo(4, (stream) => { - stream.codecs = 'bad'; - }); + }); + manifest.addVariant(4, (variant) => { + variant.bandwidth = 20000; + variant.addVideo(4, (stream) => { + stream.codecs = 'bad'; }); - period.addVariant(5, (variant) => { - variant.bandwidth = 30000; - variant.addVideo(5, (stream) => { - stream.codecs = 'bad'; - }); + }); + manifest.addVariant(5, (variant) => { + variant.bandwidth = 30000; + variant.addVideo(5, (stream) => { + stream.codecs = 'bad'; }); }); }); - await setupPlayer(); + await player.load(fakeManifestUri, 0, fakeMimeType); expect(abrManager.setVariants).toHaveBeenCalled(); const variants = abrManager.setVariants.calls.argsFor(0)[0]; // We've already chosen codecs, so only 3 tracks should remain. @@ -3050,22 +2787,20 @@ describe('Player', () => { it('updates AbrManager about restricted variants', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(1, (stream) => { - stream.keyId = 'abc'; - }); - }); - period.addVariant(2, (variant) => { - variant.addVideo(3); + manifest.addVariant(0, (variant) => { + variant.addVideo(1, (stream) => { + stream.keyIds = ['abc']; }); }); + manifest.addVariant(2, (variant) => { + variant.addVideo(3); + }); }); /** @type {!shaka.test.FakeAbrManager} */ const abrManager = new shaka.test.FakeAbrManager(); player.configure('abrFactory', () => abrManager); - await setupPlayer(); + await player.load(fakeManifestUri, 0, fakeMimeType); expect(player.getVariantTracks().length).toBe(2); // We have some key statuses, but not for the key IDs we know. @@ -3080,24 +2815,23 @@ describe('Player', () => { it('chooses codecs after considering 6-channel preference', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - // Surround sound AC-3, preferred by config - period.addVariant(0, (variant) => { - variant.bandwidth = 300; - variant.addAudio(0, (stream) => { - stream.channelsCount = 6; - stream.audioSamplingRate = 48000; - stream.codecs = 'ac-3'; - }); + // Surround sound AC-3, preferred by config + manifest.addVariant(0, (variant) => { + variant.bandwidth = 300; + variant.addAudio(0, (stream) => { + stream.channelsCount = 6; + stream.audioSamplingRate = 48000; + stream.codecs = 'ac-3'; }); - // Stereo AAC, would win out based on bandwidth alone - period.addVariant(1, (variant) => { - variant.bandwidth = 100; - variant.addAudio(1, (stream) => { - stream.channelsCount = 2; - stream.audioSamplingRate = 48000; - stream.codecs = 'mp4a.40.2'; - }); + }); + + // Stereo AAC, would win out based on bandwidth alone + manifest.addVariant(1, (variant) => { + variant.bandwidth = 100; + variant.addAudio(1, (stream) => { + stream.channelsCount = 2; + stream.audioSamplingRate = 48000; + stream.codecs = 'mp4a.40.2'; }); }); }); @@ -3106,7 +2840,7 @@ describe('Player', () => { player.configure({ preferredAudioChannelCount: 6, }); - await setupPlayer(); + await player.load(fakeManifestUri, 0, fakeMimeType); expect(abrManager.setVariants).toHaveBeenCalled(); // We've chosen codecs, so only 1 track should remain. expect(abrManager.variants.length).toBe(1); @@ -3114,15 +2848,6 @@ describe('Player', () => { expect(abrManager.variants[0].audio.channelsCount).toBe(6); expect(abrManager.variants[0].audio.codecs).toBe('ac-3'); }); - - /** - * @return {!Promise} - */ - async function setupPlayer() { - await player.load(fakeManifestUri, 0, fakeMimeType); - // Initialize the fake streams. - streamingEngine.onCanSwitch(); - } }); describe('getPlayheadTimeAsDate()', () => { @@ -3132,10 +2857,8 @@ describe('Player', () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.presentationTimeline = timeline; - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(1); - }); + manifest.addVariant(0, (variant) => { + variant.addVideo(1); }); }); @@ -3154,7 +2877,7 @@ describe('Player', () => { const expected = Util.jasmineError(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.NO_PERIODS)); + shaka.util.Error.Code.NO_VARIANTS)); await expectAsync(player.load(fakeManifestUri, 0, fakeMimeType)) .toBeRejectedWith(expected); }); @@ -3166,31 +2889,30 @@ describe('Player', () => { // type. This test covers https://github.com/google/shaka-player/issues/954 manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.bandwidth = 100; - variant.addVideo(0); - variant.addAudio(9); - }); - period.addVariant(1, (variant) => { - variant.bandwidth = 200; - variant.addVideo(1); - variant.addExistingStream(9); // audio - }); - period.addVariant(2, (variant) => { - variant.bandwidth = 300; - variant.addVideo(2); - variant.addExistingStream(9); // audio - }); + manifest.addVariant(0, (variant) => { + variant.bandwidth = 100; + variant.addVideo(0); + variant.addAudio(9); + }); + + manifest.addVariant(1, (variant) => { + variant.bandwidth = 200; + variant.addVideo(1); + variant.addExistingStream(9); // audio + }); + + manifest.addVariant(2, (variant) => { + variant.bandwidth = 300; + variant.addVideo(2); + variant.addExistingStream(9); // audio }); }); await player.load(fakeManifestUri, 0, fakeMimeType); - streamingEngine.onCanSwitch(); // We've already loaded variants[0]. Switch to [1] and [2]. - abrManager.switchCallback(manifest.periods[0].variants[1]); - abrManager.switchCallback(manifest.periods[0].variants[2]); + abrManager.switchCallback(manifest.variants[1]); + abrManager.switchCallback(manifest.variants[2]); }); describe('isTextTrackVisible', () => { @@ -3211,11 +2933,9 @@ describe('Player', () => { expect(player.isAudioOnly()).toBe(false); manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(0); - variant.addAudio(1); - }); + manifest.addVariant(0, (variant) => { + variant.addVideo(0); + variant.addAudio(1); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); @@ -3223,10 +2943,8 @@ describe('Player', () => { expect(player.isAudioOnly()).toBe(false); manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(0); - }); + manifest.addVariant(0, (variant) => { + variant.addVideo(0); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); @@ -3234,10 +2952,8 @@ describe('Player', () => { expect(player.isAudioOnly()).toBe(false); manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addAudio(1); - }); + manifest.addVariant(0, (variant) => { + variant.addAudio(1); }); }); await player.load(fakeManifestUri, 0, fakeMimeType); @@ -3254,19 +2970,17 @@ describe('Player', () => { it('tolerates bandwidth of NaN, undefined, or 0', async () => { // Regression test for https://github.com/google/shaka-player/issues/938 manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.bandwidth = /** @type {?} */(undefined); - variant.addVideo(0); - }); - period.addVariant(1, (variant) => { - variant.bandwidth = NaN; - variant.addVideo(1); - }); - period.addVariant(2, (variant) => { - variant.bandwidth = 0; - variant.addVideo(2); - }); + manifest.addVariant(0, (variant) => { + variant.bandwidth = /** @type {?} */(undefined); + variant.addVideo(0); + }); + manifest.addVariant(1, (variant) => { + variant.bandwidth = NaN; + variant.addVideo(1); + }); + manifest.addVariant(2, (variant) => { + variant.bandwidth = 0; + variant.addVideo(2); }); }); @@ -3286,10 +3000,8 @@ describe('Player', () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.presentationTimeline = timeline; - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(1); - }); + manifest.addVariant(0, (variant) => { + variant.addVideo(1); }); }); @@ -3328,69 +3040,67 @@ describe('Player', () => { beforeEach(() => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(1, (variant) => { - variant.language = 'fr'; - variant.addVideo(0); - variant.addAudio(1, (stream) => { - stream.language = 'fr'; - }); + manifest.addVariant(1, (variant) => { + variant.language = 'fr'; + variant.addVideo(0); + variant.addAudio(1, (stream) => { + stream.language = 'fr'; }); + }); - period.addVariant(2, (variant) => { - variant.language = 'en'; - variant.addExistingStream(0); // video - variant.addAudio(2, (stream) => { - stream.language = 'en'; - stream.roles = ['main']; - }); + manifest.addVariant(2, (variant) => { + variant.language = 'en'; + variant.addExistingStream(0); // video + variant.addAudio(2, (stream) => { + stream.language = 'en'; + stream.roles = ['main']; }); + }); - period.addVariant(3, (variant) => { - variant.language = 'en'; - variant.addExistingStream(0); // video - variant.addAudio(3, (stream) => { - stream.language = 'en'; - stream.roles = ['commentary']; - }); + manifest.addVariant(3, (variant) => { + variant.language = 'en'; + variant.addExistingStream(0); // video + variant.addAudio(3, (stream) => { + stream.language = 'en'; + stream.roles = ['commentary']; }); + }); - period.addVariant(4, (variant) => { - variant.language = 'de'; - variant.addExistingStream(0); // video - variant.addAudio(4, (stream) => { - stream.language = 'de'; - stream.roles = ['foo', 'bar']; - }); + manifest.addVariant(4, (variant) => { + variant.language = 'de'; + variant.addExistingStream(0); // video + variant.addAudio(4, (stream) => { + stream.language = 'de'; + stream.roles = ['foo', 'bar']; }); + }); - period.addTextStream(5, (stream) => { - stream.language = 'es'; - stream.roles = ['baz', 'qwerty']; - }); - period.addTextStream(6, (stream) => { - stream.language = 'en'; - stream.kind = 'caption'; - stream.roles = ['main', 'caption']; - }); - period.addTextStream(7, (stream) => { - stream.language = 'en'; - stream.kind = 'subtitle'; - stream.roles = ['main', 'subtitle']; - }); + manifest.addTextStream(5, (stream) => { + stream.language = 'es'; + stream.roles = ['baz', 'qwerty']; + }); + + manifest.addTextStream(6, (stream) => { + stream.language = 'en'; + stream.kind = 'caption'; + stream.roles = ['main', 'caption']; + }); + + manifest.addTextStream(7, (stream) => { + stream.language = 'en'; + stream.kind = 'subtitle'; + stream.roles = ['main', 'subtitle']; }); }); videoOnlyManifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(1, (variant) => { - variant.bandwidth = 400; - variant.addVideo(1); - }); - period.addVariant(2, (variant) => { - variant.bandwidth = 800; - variant.addVideo(2); - }); + manifest.addVariant(1, (variant) => { + variant.bandwidth = 400; + variant.addVideo(1); + }); + manifest.addVariant(2, (variant) => { + variant.bandwidth = 800; + variant.addVideo(2); }); }); }); @@ -3414,16 +3124,14 @@ describe('Player', () => { describe('getAudioLanguagesAndRoles', () => { it('ignores video roles', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.language = 'en'; - variant.addVideo(1, (stream) => { - stream.roles = ['video-only-role']; - }); - variant.addAudio(2, (stream) => { - stream.roles = ['audio-only-role']; - stream.language = 'en'; - }); + manifest.addVariant(0, (variant) => { + variant.language = 'en'; + variant.addVideo(1, (stream) => { + stream.roles = ['video-only-role']; + }); + variant.addAudio(2, (stream) => { + stream.roles = ['audio-only-role']; + stream.language = 'en'; }); }); }); @@ -3448,11 +3156,9 @@ describe('Player', () => { it('uses "und" for video-only tracks', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(1, (stream) => { - stream.roles = ['video-only-role']; - }); + manifest.addVariant(0, (variant) => { + variant.addVideo(1, (stream) => { + stream.roles = ['video-only-role']; }); }); }); @@ -3493,39 +3199,14 @@ describe('Player', () => { /** * Gets the currently active text track. - * @return {shaka.extern.Track} + * @return {?shaka.extern.Track} */ function getActiveTextTrack() { const activeTracks = player.getTextTracks().filter((track) => { return track.active; }); - expect(activeTracks.length).toBe(1); - return activeTracks[0]; - } - - /** - * Simulate the transition to a new period using the fake StreamingEngine. - * @param {number} index - */ - function transitionPeriod(index) { - periodIndex = index; - streamingEngine.onChooseStreams(); - } - - /** - * Choose streams for the given period. - * - * @suppress {accessControls} - * @return {shaka.media.StreamingEngine.ChosenStreams} - */ - function onChooseStreams() { - return player.onChooseStreams_(manifest.periods[periodIndex]); - } - - /** @suppress {accessControls} */ - function onCanSwitch() { - player.canSwitch_(); + return activeTracks[0] || null; } /** diff --git a/test/test/assets/db-dump-v3-broken.json b/test/test/assets/db-dump-v4-broken.json similarity index 100% rename from test/test/assets/db-dump-v3-broken.json rename to test/test/assets/db-dump-v4-broken.json diff --git a/test/test/boot.js b/test/test/boot.js index 70f1c24725..5eba0872c1 100644 --- a/test/test/boot.js +++ b/test/test/boot.js @@ -204,6 +204,17 @@ function getClientArg(name) { }); }; + /** + * Unconditionally skip contained tests that would normally be run + * conditionally. Used to temporarily disable tests that use filterDescribe. + * See filterDescribe above. + * + * @param {string} describeName + * @param {function():*} cond + * @param {function()} describeBody + */ + window.xfilterDescribe = (describeName, cond, describeBody) => {}; + beforeAll((done) => { // eslint-disable-line no-restricted-syntax // Configure AMD modules and their dependencies. require.config({ diff --git a/test/test/externs/filters.js b/test/test/externs/filters.js index eeb305718c..f4cf9361f0 100644 --- a/test/test/externs/filters.js +++ b/test/test/externs/filters.js @@ -29,3 +29,11 @@ var quarantinedIt = function(name, callback) {}; * @param {function()} callback */ var filterDescribe = function(name, cond, callback) {}; + + +/** + * @param {string} name + * @param {function():*} cond + * @param {function()} callback + */ +var xfilterDescribe = function(name, cond, callback) {}; diff --git a/test/test/externs/jasmine.js b/test/test/externs/jasmine.js index 6d5a0571d3..cc867c6619 100644 --- a/test/test/externs/jasmine.js +++ b/test/test/externs/jasmine.js @@ -154,14 +154,10 @@ jasmine.Matchers.prototype.toHaveBeenCalledTimes = function(times) {}; jasmine.Matchers.prototype.toMatch = function(value) {}; -/** @param {*} value */ +/** @param {*=} value */ jasmine.Matchers.prototype.toThrow = function(value) {}; -/** @param {*} value */ -jasmine.Matchers.prototype.toThrowError = function(value) {}; - - /** * A custom matcher for DOM Node objects. * @param {!Element} expected diff --git a/test/test/util/dash_parser_util.js b/test/test/util/dash_parser_util.js index b2257dd5f9..62ba074fad 100644 --- a/test/test/util/dash_parser_util.js +++ b/test/test/util/dash_parser_util.js @@ -35,26 +35,15 @@ shaka.test.Dash = class { const playerInterface = { networkingEngine: networkingEngine, - filterNewPeriod: () => {}, - filterAllPeriods: () => {}, + filter: () => {}, onTimelineRegionAdded: fail, // Should not have any EventStream elements. onEvent: fail, onError: fail, }; const manifest = await dashParser.start('dummy://foo', playerInterface); - const stream = manifest.periods[0].variants[0].video; + const stream = manifest.variants[0].video; await stream.createSegmentIndex(); - // Set expected values for append window. - const appendWindowStart = manifest.periods[0].startTime; - const appendWindowEnd = manifest.periods[1] ? - manifest.periods[1].startTime : - manifest.presentationTimeline.getDuration(); - for (const ref of references) { - ref.appendWindowStart = appendWindowStart; - ref.appendWindowEnd = appendWindowEnd; - } - shaka.test.ManifestParser.verifySegmentIndex(stream, references); } @@ -74,8 +63,7 @@ shaka.test.Dash = class { const playerInterface = { networkingEngine: networkingEngine, - filterNewPeriod: () => {}, - filterAllPeriods: () => {}, + filter: () => {}, onTimelineRegionAdded: fail, // Should not have any EventStream elements. onEvent: fail, onError: fail, @@ -125,13 +113,7 @@ shaka.test.Dash = class { * @return {!Promise.} */ static async getFirstVideoSegmentReference(manifest) { - const period = manifest.periods[0]; - expect(period).not.toBe(null); - if (!period) { - return null; - } - - const variant = period.variants[0]; + const variant = manifest.variants[0]; expect(variant).not.toBe(null); if (!variant) { return null; @@ -161,7 +143,7 @@ shaka.test.Dash = class { * @return {!Promise} */ static async callCreateSegmentIndex(manifest) { - const stream = manifest.periods[0].variants[0].video; + const stream = manifest.variants[0].video; await expectAsync(stream.createSegmentIndex()).toBeRejected(); } diff --git a/test/test/util/fake_media_source_engine.js b/test/test/util/fake_media_source_engine.js index 3dd6927b1f..39011c9509 100644 --- a/test/test/util/fake_media_source_engine.js +++ b/test/test/util/fake_media_source_engine.js @@ -272,19 +272,16 @@ shaka.test.FakeMediaSourceEngine = class { throw new Error('unexpected data'); } + // Verify that the segment is aligned. const segmentData = this.segmentData[type]; - const expectedStartTime = - segmentData.segmentPeriodTimes[i] + segmentData.segmentStartTimes[i]; + const appendedTime = segmentData.segmentStartTimes[i] + + this.timestampOffsets_[originalType]; + const expectedStartTime = i * segmentData.segmentDuration; const expectedEndTime = expectedStartTime + segmentData.segmentDuration; + expect(appendedTime).toBe(expectedStartTime); expect(startTime).toBe(expectedStartTime); expect(endTime).toBe(expectedEndTime); - // Verify that the segment is aligned. - const start = this.segmentData[type].segmentStartTimes[i] + - this.timestampOffsets_[originalType]; - const expectedStart = i * this.segmentData[type].segmentDuration; - expect(start).toBe(expectedStart); - this.segments[type][i] = true; return Promise.resolve(); } @@ -410,7 +407,6 @@ shaka.test.FakeMediaSourceEngine = class { * initSegments: !Array., * segments: !Array., * segmentStartTimes: !Array., - * segmentPeriodTimes: !Array., * segmentDuration: number * }} * @@ -422,9 +418,6 @@ shaka.test.FakeMediaSourceEngine = class { * The start time of each media segment as they would appear within a * segment index. These values plus drift simulate the segments' * baseMediaDecodeTime (or equivalent) values. - * @property {!Array.} segmentPeriodTimes - * The start time of the period of the associated segment. These are the same - * segments as in |segmentStartTimes|. * @property {number} segmentDuration * The duration of each media segment. */ diff --git a/test/test/util/manifest_generator.js b/test/test/util/manifest_generator.js index 928ba48e6f..d73f7bda47 100644 --- a/test/test/util/manifest_generator.js +++ b/test/test/util/manifest_generator.js @@ -83,8 +83,10 @@ shaka.test.ManifestGenerator.Manifest = class { /** @private {?} */ this.shaka_ = shaka || window['shaka']; - /** @type {shaka.test.ManifestGenerator.Period} */ - this.currentPeriod_ = null; + /** @type {!Array.} */ + this.variants = []; + /** @type {!Array.} */ + this.textStreams = []; const timeline = new this.shaka_.media.PresentationTimeline(0, 0); timeline.setSegmentAvailabilityDuration(Infinity); @@ -92,8 +94,6 @@ shaka.test.ManifestGenerator.Manifest = class { /** @type {!shaka.media.PresentationTimeline} */ this.presentationTimeline = timeline; - /** @type {!Array.} */ - this.periods = []; /** @type {!Array.} */ this.offlineSessionIds = []; /** @type {number} */ @@ -120,23 +120,6 @@ shaka.test.ManifestGenerator.Manifest = class { /** @type {?} */ (jasmine.any(this.shaka_.media.PresentationTimeline)); } - /** - * Adds a new Period to the manifest. - * - * @param {?number} startTime - * @param {function(!shaka.test.ManifestGenerator.Period)=} func - */ - addPeriod(startTime, func) { - const period = - new shaka.test.ManifestGenerator.Period(this, startTime); - if (func) { - this.currentPeriod_ = period; - func(period); - this.currentPeriod_ = null; - } - this.periods.push(period.build_()); - } - /** * Gets the existing stream with the given ID. * @param {number} id @@ -145,25 +128,19 @@ shaka.test.ManifestGenerator.Manifest = class { */ findExistingStream_(id) { const real = (obj) => shaka.test.ManifestGenerator.realObj_(obj); - let periods = this.periods; - if (this.currentPeriod_) { - periods = periods.concat([this.currentPeriod_]); - } - for (const period of periods) { - for (const maybeVariant of period.variants) { - const variant = real(maybeVariant); - if (variant.video && real(variant.video).id == id) { - return variant.video; - } - if (variant.audio && real(variant.audio).id == id) { - return variant.audio; - } + for (const maybeVariant of this.variants) { + const variant = real(maybeVariant); + if (variant.video && real(variant.video).id == id) { + return variant.video; + } + if (variant.audio && real(variant.audio).id == id) { + return variant.audio; } - for (const maybeText of period.textStreams) { - if (real(maybeText).id == id) { - return maybeText; - } + } + for (const maybeText of this.textStreams) { + if (real(maybeText).id == id) { + return maybeText; } } return null; @@ -178,37 +155,6 @@ shaka.test.ManifestGenerator.Manifest = class { isIdUsed_(id) { return id != null && this.findExistingStream_(id) != null; } -}; - -shaka.test.ManifestGenerator.Period = class { - /** - * @param {!shaka.test.ManifestGenerator.Manifest} manifest - * @param {?number} startTime - */ - constructor(manifest, startTime) { - /** @const {!shaka.test.ManifestGenerator.Manifest} */ - this.manifest_ = manifest; - - /** @type {number} */ - this.startTime = - startTime == null ? /** @type {?} */ (jasmine.any(Number)) : startTime; - /** @type {!Array.} */ - this.variants = []; - /** @type {!Array.} */ - this.textStreams = []; - - /** @type {shaka.extern.Period} */ - const foo = this; - goog.asserts.assert(foo, 'Checking for type compatibility'); - } - - /** - * @return {shaka.extern.Period} - * @private - */ - build_() { - return shaka.test.ManifestGenerator.buildCommon_(this); - } /** * Adds a new variant to the manifest. @@ -218,7 +164,7 @@ shaka.test.ManifestGenerator.Period = class { */ addVariant(id, func) { const variant = new shaka.test.ManifestGenerator.Variant( - this.manifest_, /* isPartial= */ false, id); + this, /* isPartial= */ false, id); if (func) { func(variant); } @@ -234,7 +180,7 @@ shaka.test.ManifestGenerator.Period = class { */ addPartialVariant(func) { const variant = new shaka.test.ManifestGenerator.Variant( - this.manifest_, /* isPartial= */ true); + this, /* isPartial= */ true); if (func) { func(variant); } @@ -243,7 +189,7 @@ shaka.test.ManifestGenerator.Period = class { } /** - * Adds a text stream to the current period. + * Adds a text stream to the manifest. * * @param {number} id * @param {function(!shaka.test.ManifestGenerator.Stream)=} func @@ -251,7 +197,7 @@ shaka.test.ManifestGenerator.Period = class { addTextStream(id, func) { const ContentType = shaka.util.ManifestParserUtils.ContentType; const stream = new shaka.test.ManifestGenerator.Stream( - this.manifest_, /* isPartial= */ false, id, ContentType.TEXT, 'und'); + this, /* isPartial= */ false, id, ContentType.TEXT, 'und'); if (func) { func(stream); } @@ -269,7 +215,7 @@ shaka.test.ManifestGenerator.Period = class { const ContentType = shaka.util.ManifestParserUtils.ContentType; const stream = new shaka.test.ManifestGenerator.Stream( - this.manifest_, /* isPartial= */ true, null, ContentType.TEXT); + this, /* isPartial= */ true, null, ContentType.TEXT); if (func) { func(stream); } @@ -563,8 +509,8 @@ shaka.test.ManifestGenerator.Stream = class { this.kind = undefined; /** @type {boolean} */ this.encrypted = false; - /** @type {?string} */ - this.keyId = null; + /** @type {!Array.} */ + this.keyIds = []; /** @type {string} */ this.language = lang || 'und'; /** @type {?string} */ @@ -609,16 +555,11 @@ shaka.test.ManifestGenerator.Stream = class { useSegmentTemplate(template, segmentDuration, segmentSize = null) { const totalDuration = this.manifest_.presentationTimeline.getDuration(); const segmentCount = totalDuration / segmentDuration; - const currentPeriod = this.manifest_.currentPeriod_; - const periodStart = currentPeriod.startTime; + const duration = this.manifest_.presentationTimeline.getDuration(); this.createSegmentIndex = () => Promise.resolve(); - this.segmentIndex.find = (time) => { - // Note: |time| is relative to the presentation. - const periodTime = time - periodStart; - return Math.floor(periodTime / segmentDuration); - }; + this.segmentIndex.find = (time) => Math.floor(time / segmentDuration); this.segmentIndex.get = (index) => { goog.asserts.assert(!isNaN(index), 'Invalid index requested!'); @@ -629,21 +570,21 @@ shaka.test.ManifestGenerator.Stream = class { const start = index * segmentDuration; const end = Math.min(totalDuration, (index + 1) * segmentDuration); return new this.manifest_.shaka_.media.SegmentReference( - /* startTime= */ periodStart + start, - /* endTime= */ periodStart + end, + /* startTime= */ start, + /* endTime= */ end, getUris, /* startByte= */ 0, /* endByte= */ segmentSize, this.initSegmentReference_, - /* timestampOffset= */ periodStart, - /* appendWindowStart= */ periodStart, - /* appendWindowEnd= */ Infinity); + /* timestampOffset= */ 0, + /* appendWindowStart= */ 0, + /* appendWindowEnd= */ duration); }; } /** * Sets the current stream to use the given text stream. It will serve a - * single media segment at the given URI for the entire Period. + * single media segment at the given URI for the entire presentation. * * @param {string} uri */ diff --git a/test/test/util/offline_utils.js b/test/test/util/offline_utils.js index 0c3ebf9982..b8b97a6ab9 100644 --- a/test/test/util/offline_utils.js +++ b/test/test/util/offline_utils.js @@ -18,7 +18,7 @@ shaka.test.OfflineUtils = class { duration: 90, expiration: Infinity, originalManifestUri: originalUri, - periods: [], + streams: [], sessionIds: [], size: 1024, }; @@ -47,9 +47,13 @@ shaka.test.OfflineUtils = class { height: null, initSegmentKey: null, encrypted: false, - keyId: null, + keyIds: [], segments: [], variantIds: [], + roles: [], + channelsCount: null, + audioSamplingRate: null, + closedCaptions: null, }; } diff --git a/test/test/util/simple_fakes.js b/test/test/util/simple_fakes.js index 1926f5e04f..3f48bfdb5a 100644 --- a/test/test/util/simple_fakes.js +++ b/test/test/util/simple_fakes.js @@ -78,24 +78,12 @@ shaka.test.FakeAbrManager = class { /** @extends {shaka.media.StreamingEngine} */ shaka.test.FakeStreamingEngine = class { - /** - * @param {function():shaka.media.StreamingEngine.ChosenStreams} - * onChooseStreams - * @param {function()} onCanSwitch - */ - constructor(onChooseStreams, onCanSwitch) { + constructor() { const resolve = () => Promise.resolve(); - let activeAudio = null; - let activeVideo = null; + let activeVariant = null; let activeText = null; - /** @type {function()} */ - this.onChooseStreams = onChooseStreams; - - /** @type {function()} */ - this.onCanSwitch = onCanSwitch; - /** @type {!jasmine.Spy} */ this.destroy = jasmine.createSpy('destroy').and.callFake(resolve); @@ -106,26 +94,14 @@ shaka.test.FakeStreamingEngine = class { this.seeked = jasmine.createSpy('seeked'); /** @type {!jasmine.Spy} */ - this.getBufferingPeriod = - jasmine.createSpy('getBufferingPeriod').and.returnValue(null); + this.getCurrentVariant = + jasmine.createSpy('getCurrentVariant').and.callFake( + () => activeVariant); /** @type {!jasmine.Spy} */ - this.getBufferingAudio = - jasmine.createSpy('getBufferingAudio').and.callFake(() => activeAudio); - - /** @type {!jasmine.Spy} */ - this.getBufferingVideo = - jasmine.createSpy('getBufferingVideo').and.callFake(() => activeVideo); - - this.getBufferingText = - jasmine.createSpy('getBufferingText').and.callFake(() => activeText); - - /** @type {!jasmine.Spy} */ - this.loadNewTextStream = - jasmine.createSpy('loadNewTextStream').and.callFake((stream) => { - activeText = stream; - return Promise.resolve(); - }); + this.getCurrentTextStream = + jasmine.createSpy('getCurrentTextStream').and.callFake( + () => activeText); /** @type {!jasmine.Spy} */ this.unloadTextStream = @@ -134,25 +110,12 @@ shaka.test.FakeStreamingEngine = class { }); /** @type {!jasmine.Spy} */ - this.start = jasmine.createSpy('start').and.callFake(async () => { - const chosen = onChooseStreams(); - await Promise.resolve(); - if (chosen.variant && chosen.variant.audio) { - activeAudio = chosen.variant.audio; - } - if (chosen.variant && chosen.variant.video) { - activeVideo = chosen.variant.video; - } - if (chosen.text) { - activeText = chosen.text; - } - }); + this.start = jasmine.createSpy('start'); /** @type {!jasmine.Spy} */ this.switchVariant = jasmine.createSpy('switchVariant').and.callFake((variant) => { - activeAudio = variant.audio || activeAudio; - activeVideo = variant.video || activeVideo; + activeVariant = variant; }); /** @type {!jasmine.Spy} */ diff --git a/test/test/util/stream_generator.js b/test/test/util/stream_generator.js index e01ca46e26..4cecf16a57 100644 --- a/test/test/util/stream_generator.js +++ b/test/test/util/stream_generator.js @@ -40,19 +40,13 @@ shaka.test.IStreamGenerator = class { * Gets one of the stream's segments. * The IStreamGenerator must be initialized. * - * @param {number} position The segment's position within a particular Period - * p. - * @param {number} segmentOffset The number of segments in all Periods that - * came before Period p. + * @param {number} position The segment's position. * @param {number} wallClockTime The wall-clock time in seconds. - * @example getSegment(1, 0) gets the 1st segment in the stream, - * and getSegment(2, 5) gets the 2nd segment in a Period that starts - * at the 6th segment (relative to the very start of the stream). * * @return {ArrayBuffer} The segment if the stream has started, and the * segment exists and is available; otherwise, return null. */ - getSegment(position, segmentOffset, wallClockTime) {} + getSegment(position, wallClockTime) {} }; /** @@ -85,13 +79,12 @@ shaka.test.TSVodStreamGenerator = class { } /** @override */ - getSegment(position, segmentOffset, wallClockTime) { + getSegment(position, wallClockTime) { goog.asserts.assert( this.segment_, 'init() must be called before getSegment().'); // TODO: complete implementation; this should change the timestamps based on - // the given segmentOffset and wallClockTime, so as to simulate a long - // stream. + // the given wallClockTime, so as to simulate a long stream. return this.segment_; } }; @@ -170,7 +163,7 @@ shaka.test.Mp4VodStreamGenerator = class { } /** @override */ - getSegment(position, segmentOffset, wallClockTime) { + getSegment(position, wallClockTime) { goog.asserts.assert( this.segmentTemplate_, 'init() must be called before getSegment().'); @@ -178,11 +171,11 @@ shaka.test.Mp4VodStreamGenerator = class { return null; } - // |position| must be an integer and >= 1. - goog.asserts.assert((position % 1 === 0) && (position >= 1), - 'segment number must be an integer >= 1'); + // |position| must be an integer and >= 0. + goog.asserts.assert((position % 1 === 0) && (position >= 0), + 'segment number must be an integer >= 0'); - const segmentStartTime = (position - 1) * this.segmentDuration_; + const segmentStartTime = position * this.segmentDuration_; return shaka.test.StreamGenerator.setBaseMediaDecodeTime_( this.segmentTemplate_, this.tfdtOffset_, segmentStartTime, @@ -292,7 +285,7 @@ shaka.test.Mp4LiveStreamGenerator = class { } /** @override */ - getSegment(position, segmentOffset, wallClockTime) { + getSegment(position, wallClockTime) { goog.asserts.assert( this.initSegment_, 'init() must be called before getSegment().'); @@ -300,17 +293,16 @@ shaka.test.Mp4LiveStreamGenerator = class { return null; } - // |position| must be an integer and >= 1. - goog.asserts.assert((position % 1 === 0) && (position >= 1), - 'segment number must be an integer >= 1'); + // |position| must be an integer and >= 0. + goog.asserts.assert((position % 1 === 0) && (position >= 0), + 'segment number must be an integer >= 0'); - const segmentStartTime = (position - 1) * this.segmentDuration_; + const segmentStartTime = position * this.segmentDuration_; // Compute the segment's availability start time and end time. // (See section 5.3.9.5.3 of the DASH spec.) const segmentAvailabilityStartTime = this.availabilityStartTime_ + segmentStartTime + - (segmentOffset * this.segmentDuration_) + this.segmentDuration_; const segmentAvailabiltyEndTime = segmentAvailabilityStartTime + this.segmentDuration_ + @@ -337,8 +329,7 @@ shaka.test.Mp4LiveStreamGenerator = class { // 0. const artificialPresentationTimeOffset = this.broadcastStartTime_ - this.availabilityStartTime_; - const mediaTimestamp = segmentStartTime + - artificialPresentationTimeOffset; + const mediaTimestamp = segmentStartTime + artificialPresentationTimeOffset; return shaka.test.StreamGenerator.setBaseMediaDecodeTime_( /** @type {!ArrayBuffer} */ (this.segmentTemplate_), this.tfdtOffset_, diff --git a/test/test/util/streaming_engine_util.js b/test/test/util/streaming_engine_util.js index 932739cbc0..7898aaa4b6 100644 --- a/test/test/util/streaming_engine_util.js +++ b/test/test/util/streaming_engine_util.js @@ -5,9 +5,6 @@ goog.provide('shaka.test.StreamingEngineUtil'); -goog.require('shaka.util.Iterables'); - - shaka.test.StreamingEngineUtil = class { /** * Creates a FakeNetworkingEngine. @@ -17,7 +14,7 @@ shaka.test.StreamingEngineUtil = class { * * A request's URI must follow either the init segment URI pattern: * PERIOD_TYPE_init, e.g., "1_audio_init" or "2_video_init"; or the media - * segment URI pattern: PERIOD_TYPE_POSITION, e.g., "1_text_2" or "2_video_1". + * segment URI pattern: PERIOD_TYPE_POSITION, e.g., "1_text_2" or "2_video_5". * * @param {function(string, number): BufferSource} getInitSegment Init segment * generator: takes a content type and a Period number; returns an init @@ -44,22 +41,22 @@ shaka.test.StreamingEngineUtil = class { const parts = request.uris[0].split('_'); expect(parts.length).toBe(3); - const periodNumber = Number(parts[0]); - expect(periodNumber).not.toBeNaN(); - expect(periodNumber).toBeGreaterThan(0); - expect(Math.floor(periodNumber)).toBe(periodNumber); + const periodIndex = Number(parts[0]); + expect(periodIndex).not.toBeNaN(); + expect(periodIndex).toBeGreaterThan(-1); + expect(Math.floor(periodIndex)).toBe(periodIndex); const contentType = parts[1]; let buffer; if (parts[2] == 'init') { - buffer = getInitSegment(contentType, periodNumber); + buffer = getInitSegment(contentType, periodIndex); } else { const position = Number(parts[2]); expect(position).not.toBeNaN(); - expect(position).toBeGreaterThan(0); + expect(position).toBeGreaterThan(-1); expect(Math.floor(position)).toBe(position); - buffer = getSegment(contentType, periodNumber, position); + buffer = getSegment(contentType, periodIndex, position); } const response = {uri: request.uris[0], data: buffer, headers: {}}; @@ -164,18 +161,18 @@ shaka.test.StreamingEngineUtil = class { } /** - * Creates a fake Manifest. - * - * Each Period within the fake Manifest has one Variant and one - * text stream. + * Creates a fake Manifest simulating one or more DASH periods, containing + * one variant and optionally one text stream. The streams we create are + * based on the keys in segmentDurations. * * Audio, Video, and Text Stream MIME types are set to * "audio/mp4; codecs=mp4a.40.2", "video/mp4; codecs=avc1.42c01e", * and "text/vtt" respectively. * * Each media segment's URI follows the media segment URI pattern: - * PERIOD_TYPE_POSITION, e.g., "1_text_2" or "2_video_1". + * PERIOD_TYPE_POSITION, e.g., "1_text_2" or "2_video_5". * + * @param {!shaka.media.PresentationTimeline} presentationTimeline * @param {!Array.} periodStartTimes The start time of each Period. * @param {number} presentationDuration * @param {!Object.} segmentDurations The duration of each @@ -185,40 +182,85 @@ shaka.test.StreamingEngineUtil = class { * @return {shaka.extern.Manifest} */ static createManifest( - periodStartTimes, presentationDuration, segmentDurations, - initSegmentRanges) { - const boundsCheckPosition = (time, period, pos) => - shaka.test.StreamingEngineUtil.boundsCheckPosition( - periodStartTimes, presentationDuration, segmentDurations, time, - period, pos); - + presentationTimeline, periodStartTimes, presentationDuration, + segmentDurations, initSegmentRanges) { /** * @param {string} type - * @param {number} periodNumber * @param {number} time * @return {?number} A segment position. */ - const find = (type, periodNumber, time) => { - // Note: |time| is relative to the presentation, and |periodNumber| is - // 1-based. - const periodTime = time - periodStartTimes[periodNumber - 1]; + const find = (type, time) => { + if (time >= presentationDuration || time < 0) { + return null; + } - const position = Math.floor(periodTime / segmentDurations[type]) + 1; - return boundsCheckPosition(type, periodNumber, position); + // Note that we don't just directly compute the segment position because + // a period start time could be in the middle of the previous period's + // last segment. + let position = 0; + let i; + for (i = 0; i < periodStartTimes.length; ++i) { + const startTime = periodStartTimes[i]; + const nextStartTime = i < periodStartTimes.length - 1 ? + periodStartTimes[i + 1] : + presentationDuration; + if (nextStartTime > time) { + // This is the period in which we would find the requested time. + break; + } + + // This is an earlier period. Count up the number of segments in it. + const periodDuration = nextStartTime - startTime; + const numSegments = Math.ceil(periodDuration / segmentDurations[type]); + position += numSegments; + } + + goog.asserts.assert(i < periodStartTimes.length, 'Ran out of periods!'); + const periodStartTime = periodStartTimes[i]; + const periodTime = time - periodStartTime; + position += Math.floor(periodTime / segmentDurations[type]); + + return position; }; /** * @param {string} type - * @param {number} periodNumber * @param {number} position * @return {shaka.media.SegmentReference} A SegmentReference. */ - const get = (type, periodNumber, position) => { - if (boundsCheckPosition(type, periodNumber, position) == null) { + const get = (type, position) => { + // Note that we don't just directly compute the segment position because + // a period start time could be in the middle of the previous period's + // last segment. + let periodFirstPosition = 0; + let i; + for (i = 0; i < periodStartTimes.length; ++i) { + const startTime = periodStartTimes[i]; + const nextStartTime = i < periodStartTimes.length - 1 ? + periodStartTimes[i + 1] : + presentationDuration; + + // Count up the number of segments in this period. + const periodDuration = nextStartTime - startTime; + const numSegments = Math.ceil(periodDuration / segmentDurations[type]); + + const nextPeriodFirstPosition = periodFirstPosition + numSegments; + + if (nextPeriodFirstPosition > position) { + // This is the period in which we would find the requested position. + break; + } + + periodFirstPosition = nextPeriodFirstPosition; + } + if (i == periodStartTimes.length) { return null; } - const initSegmentUri = periodNumber + '_' + type + '_init'; + const periodIndex = i; // 0-based + const positionWithinPeriod = position - periodFirstPosition; + + const initSegmentUri = periodIndex + '_' + type + '_init'; // The type can be 'text', 'audio', 'video', or 'trickvideo', // but we pull video init segment metadata from the 'video' part of the @@ -234,116 +276,109 @@ shaka.test.StreamingEngineUtil = class { } const d = segmentDurations[type]; - const getUris = () => [periodNumber + '_' + type + '_' + position]; - const timestampOffset = periodStartTimes[periodNumber - 1]; - const appendWindowStart = periodStartTimes[periodNumber - 1]; - const appendWindowEnd = periodNumber == periodStartTimes.length ? - presentationDuration : periodStartTimes[periodNumber]; + const getUris = () => [periodIndex + '_' + type + '_' + position]; + const periodStart = periodStartTimes[periodIndex]; + const appendWindowStart = periodStartTimes[periodIndex]; + const appendWindowEnd = periodIndex == periodStartTimes.length - 1? + presentationDuration : periodStartTimes[periodIndex + 1]; return new shaka.media.SegmentReference( - /* startTime= */ timestampOffset + (position - 1) * d, - /* endTime= */ timestampOffset + position * d, + /* startTime= */ periodStart + positionWithinPeriod * d, + /* endTime= */ periodStart + (positionWithinPeriod + 1) * d, getUris, /* startByte= */ 0, /* endByte= */ null, initSegmentReference, - timestampOffset, + /* timestampOffset= */ 0, appendWindowStart, appendWindowEnd); }; + /** @type {shaka.extern.Manifest} */ const manifest = { - presentationTimeline: undefined, // Should be set externally. - minBufferTime: undefined, // Should be set externally. - periods: [], + presentationTimeline, + minBufferTime: 2, + offlineSessionIds: [], + variants: [], + textStreams: [], }; + /** @type {shaka.extern.Variant} */ + const variant = { + video: null, + audio: null, + allowedByApplication: true, + allowedByKeySystem: true, + bandwidth: 0, + drmInfos: [], + id: 0, + language: 'und', + primary: false, + }; + + if ('video' in segmentDurations) { + variant.video = /** @type {shaka.extern.Stream} */( + shaka.test.StreamingEngineUtil.createMockStream('video', 0)); + } + + if ('audio' in segmentDurations) { + variant.audio = /** @type {shaka.extern.Stream} */( + shaka.test.StreamingEngineUtil.createMockStream('audio', 1)); + } + + /** @type {?shaka.extern.Stream} */ + let textStream = null; + + if ('text' in segmentDurations) { + textStream = /** @type {shaka.extern.Stream} */( + shaka.test.StreamingEngineUtil.createMockStream('text', 2)); + } + + /** @type {?shaka.extern.Stream} */ + let trickModeVideo = null; + + if ('trickvideo' in segmentDurations) { + trickModeVideo = /** @type {shaka.extern.Stream} */( + shaka.test.StreamingEngineUtil.createMockStream('video', 3)); + } + // Populate the Manifest. - let id = 0; - const enumerate = (it) => shaka.util.Iterables.enumerate(it); - for (const {i, item: startTime} of enumerate(periodStartTimes)) { - const period = { - startTime, - variants: [], - textStreams: [], - }; - - const variant = {}; - let trickModeVideo; - - for (const type in segmentDurations) { - const stream = - shaka.test.StreamingEngineUtil.createMockStream(type, id++); - - const segmentIndex = new shaka.test.FakeSegmentIndex(); - segmentIndex.find.and.callFake( - (time) => find(type, i + 1, time)); - segmentIndex.get.and.callFake((pos) => get(type, i + 1, pos)); - - stream.createSegmentIndex.and.callFake(() => { - stream.segmentIndex = segmentIndex; - return Promise.resolve(); - }); - - const ContentType = shaka.util.ManifestParserUtils.ContentType; - if (type == ContentType.TEXT) { - period.textStreams.push(stream); - } else if (type == ContentType.AUDIO) { - variant.audio = stream; - } else if (type == 'trickvideo') { - trickModeVideo = stream; - } else { - variant.video = stream; - } + for (const type in segmentDurations) { + const ContentType = shaka.util.ManifestParserUtils.ContentType; + let stream; + if (type == ContentType.TEXT) { + stream = textStream; + } else if (type == ContentType.AUDIO) { + stream = variant.audio; + } else if (type == 'trickvideo') { + stream = trickModeVideo; + } else { + stream = variant.video; } - variant.video.trickModeVideo = trickModeVideo; - period.variants.push(variant); - manifest.periods.push(period); - } + const segmentIndex = new shaka.test.FakeSegmentIndex(); + segmentIndex.find.and.callFake((time) => find(type, time)); + segmentIndex.get.and.callFake((pos) => get(type, pos)); - return /** @type {shaka.extern.Manifest} */ (manifest); - } + const createSegmentIndexSpy = jasmine.createSpy('createSegmentIndex'); + createSegmentIndexSpy.and.callFake(() => { + stream.segmentIndex = segmentIndex; + return Promise.resolve(); + }); - /** - * Returns |position| if |type|, |periodNumber|, and |position| correspond - * to a valid segment, as dictated by the provided metadata: - * |periodStartTimes|, |presentationDuration|, and |segmentDurations|. - * - * @param {!Array.} periodStartTimes - * @param {number} presentationDuration - * @param {!Object.} segmentDurations - * @param {string} type - * @param {number} periodNumber - * @param {number} position - * @return {?number} - */ - static boundsCheckPosition( - periodStartTimes, presentationDuration, segmentDurations, - type, periodNumber, position) { - const numSegments = shaka.test.StreamingEngineUtil.getNumSegments( - periodStartTimes, presentationDuration, segmentDurations, - type, periodNumber); - return position >= 1 && position <= numSegments ? position : null; - } + stream.createSegmentIndex = + shaka.test.Util.spyFunc(createSegmentIndexSpy); + } - /** - * @param {!Array.} periodStartTimes - * @param {number} presentationDuration - * @param {!Object.} segmentDurations - * @param {string} type - * @param {number} periodNumber - * @return {number} - */ - static getNumSegments( - periodStartTimes, presentationDuration, segmentDurations, - type, periodNumber) { - const periodIndex = periodNumber - 1; - const nextStartTime = periodIndex < periodStartTimes.length - 1 ? - periodStartTimes[periodIndex + 1] : - presentationDuration; - const periodDuration = nextStartTime - periodStartTimes[periodIndex]; - return Math.ceil(periodDuration / segmentDurations[type]); + if (trickModeVideo) { + variant.video.trickModeVideo = trickModeVideo; + } + if (textStream) { + manifest.textStreams = [textStream]; + } + manifest.variants = [variant]; + + return manifest; } /** diff --git a/test/test/util/test_scheme.js b/test/test/util/test_scheme.js index 8138ed078e..010dc0d48e 100644 --- a/test/test/util/test_scheme.js +++ b/test/test/util/test_scheme.js @@ -66,7 +66,7 @@ shaka.test.TestScheme = class { responseData = generator.getInitSegment(0); } else { const index = Number(segmentParts[3]); - responseData = generator.getSegment(index + 1, 0, 0); + responseData = generator.getSegment(index, 0); } if (!responseData) { return shaka.util.AbortableOperation.failed(malformed); @@ -203,138 +203,97 @@ shaka.test.TestScheme = class { const manifest = windowShaka.test.ManifestGenerator.generate((manifest) => { manifest.presentationTimeline.setDuration(data.duration); - manifest.addPeriod(/* startTime= */ 0, (period) => { - period.addVariant(0, (variant) => { - if (data[ContentType.VIDEO]) { - variant.addVideo(1, (stream) => { - addStreamInfo( - stream, variant, data, ContentType.VIDEO, name); - }); - } - if (data[ContentType.AUDIO]) { - variant.addAudio(2, (stream) => { - addStreamInfo( - stream, variant, data, ContentType.AUDIO, name); - }); - } - }); - if (data.text) { - period.addTextStream(3, (stream) => { - stream.mime = data.text.mimeType; - stream.codecs = data.text.codecs; - stream.textStream(getAbsoluteUri(data)); - - if (data.text.language) { - stream.language = data.text.language; - } + manifest.addVariant(0, (variant) => { + if (data[ContentType.VIDEO]) { + variant.addVideo(1, (stream) => { + addStreamInfo( + stream, variant, data, ContentType.VIDEO, name); + }); + } + if (data[ContentType.AUDIO]) { + variant.addAudio(2, (stream) => { + addStreamInfo( + stream, variant, data, ContentType.AUDIO, name); }); } }); + + if (data.text) { + manifest.addTextStream(3, (stream) => { + stream.mime = data.text.mimeType; + stream.codecs = data.text.codecs; + stream.textStream(getAbsoluteUri(data)); + + if (data.text.language) { + stream.language = data.text.language; + } + }); + } }, shaka); MANIFESTS[name + suffix] = manifest; } - // Custom generators: + // Custom generator for multiple streams with different languages and + // resolutions. + // TODO: Can this be generalized from the DATA array instead? const data = DATA['sintel']; const periodDuration = 10; - - // Multi-period - const numPeriods = 10; - let manifest = windowShaka.test.ManifestGenerator.generate((manifest) => { - manifest.presentationTimeline.setDuration(periodDuration * numPeriods); - - let idCount = 1; - for (const i of windowShaka.util.Iterables.range(numPeriods)) { - manifest.addPeriod( - /* startTime= */ periodDuration * i, - (period) => { - period.addVariant(idCount++, (variant) => { - variant.language = 'en'; - variant.addVideo(idCount++, (stream) => { - addStreamInfo( - stream, variant, data, ContentType.VIDEO, 'sintel'); - }); - variant.addAudio(idCount++, (stream) => { - addStreamInfo( - stream, variant, data, ContentType.AUDIO, 'sintel'); - }); - }); - - period.addVariant(idCount++, (variant) => { - variant.language = 'es'; - variant.addVideo(idCount++, (stream) => { - addStreamInfo( - stream, variant, data, ContentType.VIDEO, 'sintel'); - }); - variant.addAudio(idCount++, (stream) => { - addStreamInfo( - stream, variant, data, ContentType.AUDIO, 'sintel'); - }); - }); - }); - } - }, shaka); - MANIFESTS['sintel_short_periods' + suffix] = manifest; - - - // Multi-stream. Different languages and resolutions. let idCount = 1; - manifest = windowShaka.test.ManifestGenerator.generate((manifest) => { + const manifest = windowShaka.test.ManifestGenerator.generate((manifest) => { manifest.presentationTimeline.setDuration(periodDuration); - manifest.addPeriod(/* startTime= */ 0, (period) => { - // Variant in English, res 426x182 - period.addVariant(idCount++, (variant) => { - variant.language = 'en'; - variant.addVideo(idCount++, (stream) => { - stream.size(426, 182); - addStreamInfo(stream, variant, data, ContentType.VIDEO, 'sintel'); - }); - variant.addAudio(idCount++, (stream) => { - stream.language = 'en'; - addStreamInfo(stream, variant, data, ContentType.AUDIO, 'sintel'); - }); - }); - // Same language, different resolution - period.addVariant(idCount++, (variant) => { - variant.language = 'en'; - variant.addVideo(idCount++, (stream) => { - stream.size(640, 272); - addStreamInfo(stream, variant, data, ContentType.VIDEO, 'sintel'); - }); - variant.addAudio(idCount++, (stream) => { - stream.language = 'en'; - addStreamInfo(stream, variant, data, ContentType.AUDIO, 'sintel'); - }); + // Variant in English, res 426x182 + manifest.addVariant(idCount++, (variant) => { + variant.language = 'en'; + variant.addVideo(idCount++, (stream) => { + stream.size(426, 182); + addStreamInfo(stream, variant, data, ContentType.VIDEO, 'sintel'); }); - - // Same resolution, different language - period.addVariant(idCount++, (variant) => { - variant.language = 'es'; - variant.addVideo(idCount++, (stream) => { - stream.size(640, 272); - addStreamInfo(stream, variant, data, ContentType.VIDEO, 'sintel'); - }); - variant.addAudio(idCount++, (stream) => { - stream.language = 'es'; - addStreamInfo(stream, variant, data, ContentType.AUDIO, 'sintel'); - }); + variant.addAudio(idCount++, (stream) => { + stream.language = 'en'; + addStreamInfo(stream, variant, data, ContentType.AUDIO, 'sintel'); }); + }); - period.addTextStream(idCount++, (stream) => { - stream.language = 'zh'; - stream.mime = data.text.mimeType; - stream.codecs = data.text.codecs; - stream.textStream(getAbsoluteUri(data)); + // Same language, different resolution + manifest.addVariant(idCount++, (variant) => { + variant.language = 'en'; + variant.addVideo(idCount++, (stream) => { + stream.size(640, 272); + addStreamInfo(stream, variant, data, ContentType.VIDEO, 'sintel'); }); + variant.addAudio(idCount++, (stream) => { + stream.language = 'en'; + addStreamInfo(stream, variant, data, ContentType.AUDIO, 'sintel'); + }); + }); - period.addTextStream(idCount++, (stream) => { - stream.language = 'fr'; - stream.mime = data.text.mimeType; - stream.codecs = data.text.codecs; - stream.textStream(getAbsoluteUri(data)); + // Same resolution, different language + manifest.addVariant(idCount++, (variant) => { + variant.language = 'es'; + variant.addVideo(idCount++, (stream) => { + stream.size(640, 272); + addStreamInfo(stream, variant, data, ContentType.VIDEO, 'sintel'); }); + variant.addAudio(idCount++, (stream) => { + stream.language = 'es'; + addStreamInfo(stream, variant, data, ContentType.AUDIO, 'sintel'); + }); + }); + + manifest.addTextStream(idCount++, (stream) => { + stream.language = 'zh'; + stream.mime = data.text.mimeType; + stream.codecs = data.text.codecs; + stream.textStream(getAbsoluteUri(data)); + }); + + manifest.addTextStream(idCount++, (stream) => { + stream.language = 'fr'; + stream.mime = data.text.mimeType; + stream.codecs = data.text.codecs; + stream.textStream(getAbsoluteUri(data)); }); }, shaka); MANIFESTS['sintel_multi_lingual_multi_res' + suffix] = manifest; @@ -409,35 +368,6 @@ shaka.test.TestScheme.DATA = { duration: 300, }, - // Like 'sintel' above, but with a non-zero period start time. - // This helps expose edge cases around startup and live streams. - 'sintel_start_at_3': { - periodStart: 3, - video: { - initSegmentUri: '/base/test/test/assets/sintel-video-init.mp4', - mdhdOffset: 0x1ba, - segmentUri: '/base/test/test/assets/sintel-video-segment.mp4', - tfdtOffset: 0x38, - segmentDuration: 10, - mimeType: 'video/mp4', - codecs: 'avc1.42c01e', - }, - audio: { - initSegmentUri: '/base/test/test/assets/sintel-audio-init.mp4', - mdhdOffset: 0x1b6, - segmentUri: '/base/test/test/assets/sintel-audio-segment.mp4', - tfdtOffset: 0x3c, - segmentDuration: 10.005, - mimeType: 'audio/mp4', - codecs: 'mp4a.40.2', - }, - text: { - uri: '/base/test/test/assets/text-clip.vtt', - mimeType: 'text/vtt', - }, - duration: 30, - }, - // Like 'sintel' above, but with languages and delayed setup. // These extra features help expose some edge cases. 'sintel_realistic': { @@ -692,10 +622,7 @@ shaka.test.TestScheme.ManifestParser = class { // This makes sure the filtering functions are covered implicitly by // tests. This covers regression // https://github.com/google/shaka-player/issues/988 - playerInterface.filterAllPeriods(manifest.periods); - for (const period of manifest.periods) { - playerInterface.filterNewPeriod(period); - } + playerInterface.filter(manifest); return Promise.resolve(manifest); } diff --git a/test/test/util/waiter.js b/test/test/util/waiter.js index 806f1cce12..279f66917f 100644 --- a/test/test/util/waiter.js +++ b/test/test/util/waiter.js @@ -203,6 +203,7 @@ shaka.test.Waiter = class { shaka.media.TimeRangesUtils.getBufferedInfo(mediaElement.buffered); shaka.log.error(message, 'current time', mediaElement.currentTime, + 'duration', mediaElement.duration, 'ready state', mediaElement.readyState, 'playback rate', mediaElement.playbackRate, 'paused', mediaElement.paused, diff --git a/test/ui/ui_integration.js b/test/ui/ui_integration.js index 5c78f63dbb..96387f5bcd 100644 --- a/test/ui/ui_integration.js +++ b/test/ui/ui_integration.js @@ -81,6 +81,9 @@ describe('UI', () => { eventManager.listen(player, 'error', Util.spyFunc(onErrorSpy)); eventManager.listen(controls, 'error', Util.spyFunc(onErrorSpy)); + // These tests expect text to be streaming upfront, so always stream text. + player.configure('streaming.alwaysStreamText', true); + await player.load('test:sintel_multi_lingual_multi_res_compiled'); // For this event, we ignore a timeout, since we sometimes miss this event // on Tizen. But expect that the video is ready anyway. @@ -355,6 +358,13 @@ describe('UI', () => { updateResolutionButtonsAndMap(); oldResolutionTrack = findTrackWithHeight(tracks, oldResolution); + + const selectedResolution = + getSelectedTrack(player.getVariantTracks()).height; + if (selectedResolution != oldResolution) { + player.selectVariantTrack(oldResolutionTrack); + await waiter.waitForEvent(player, 'variantchanged'); + } }); @@ -367,10 +377,6 @@ describe('UI', () => { it('changing resolution via UI has effect on the player', async () => { - player.selectVariantTrack(oldResolutionTrack); - - // Wait for the change to take effect - await waiter.waitForEvent(player, 'variantchanged'); // Update the tracks tracks = player.getVariantTracks(); expect(getSelectedTrack(tracks).height).toBe(oldResolution); @@ -387,11 +393,6 @@ describe('UI', () => { it('changing resolution via API has effect on the UI', async () => { - // Start with the old resolution - player.selectVariantTrack(oldResolutionTrack); - - // Wait for the change to take effect - await waiter.waitForEvent(player, 'variantchanged'); updateResolutionButtonsAndMap(); expect(getSelectedTrack(tracks).height).toBe(oldResolution); diff --git a/test/ui/ui_unit.js b/test/ui/ui_unit.js index 6e10419880..c8ce300e6a 100644 --- a/test/ui/ui_unit.js +++ b/test/ui/ui_unit.js @@ -283,10 +283,8 @@ describe('UI', () => { // Load fake content that contains only audio. const manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(/* startTime= */ 0, (period) => { - period.addVariant(/* id= */ 0, (variant) => { - variant.addAudio(/* id= */ 1); - }); + manifest.addVariant(/* id= */ 0, (variant) => { + variant.addAudio(/* id= */ 1); }); }); shaka.media.ManifestParser.registerParserByMime( @@ -433,14 +431,12 @@ describe('UI', () => { it('clears the buffer when changing resolutions', async () => { // Load fake content that has more than one quality level. const manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addVideo(1, (stream) => { - stream.size(320, 240); - }); - variant.addVideo(2, (stream) => { - stream.size(640, 480); - }); + manifest.addVariant(0, (variant) => { + variant.addVideo(1, (stream) => { + stream.size(320, 240); + }); + variant.addVideo(2, (stream) => { + stream.size(640, 480); }); }); }); @@ -471,61 +467,59 @@ describe('UI', () => { // languages/channel-counts to test the current resolution list is // filtered. const manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.primary = true; - variant.language = 'en'; - variant.addVideo(1, (stream) => { - stream.size(320, 240); - }); - variant.addAudio(3, (stream) => { - stream.channelsCount = 2; - }); + manifest.addVariant(0, (variant) => { + variant.primary = true; + variant.language = 'en'; + variant.addVideo(1, (stream) => { + stream.size(320, 240); + }); + variant.addAudio(3, (stream) => { + stream.channelsCount = 2; + }); + }); + manifest.addVariant(4, (variant) => { + variant.language = 'en'; + variant.addVideo(5, (stream) => { + stream.size(640, 480); + }); + variant.addAudio(6, (stream) => { + stream.channelsCount = 2; + }); + }); + manifest.addVariant(7, (variant) => { // Duplicate with 4 + variant.language = 'en'; + variant.addVideo(8, (stream) => { + stream.size(640, 480); + }); + variant.addAudio(9, (stream) => { + stream.channelsCount = 2; + }); + }); + manifest.addVariant(10, (variant) => { + variant.language = 'en'; + variant.addVideo(11, (stream) => { + stream.size(1280, 720); }); - period.addVariant(4, (variant) => { - variant.language = 'en'; - variant.addVideo(5, (stream) => { - stream.size(640, 480); - }); - variant.addAudio(6, (stream) => { - stream.channelsCount = 2; - }); + variant.addAudio(12, (stream) => { + stream.channelsCount = 1; }); - period.addVariant(7, (variant) => { // Duplicate with 4 - variant.language = 'en'; - variant.addVideo(8, (stream) => { - stream.size(640, 480); - }); - variant.addAudio(9, (stream) => { - stream.channelsCount = 2; - }); + }); + manifest.addVariant(13, (variant) => { + variant.language = 'es'; + variant.addVideo(14, (stream) => { + stream.size(960, 540); }); - period.addVariant(10, (variant) => { - variant.language = 'en'; - variant.addVideo(11, (stream) => { - stream.size(1280, 720); - }); - variant.addAudio(12, (stream) => { - stream.channelsCount = 1; - }); + variant.addAudio(15, (stream) => { + stream.channelsCount = 2; }); - period.addVariant(13, (variant) => { - variant.language = 'es'; - variant.addVideo(14, (stream) => { - stream.size(960, 540); - }); - variant.addAudio(15, (stream) => { - stream.channelsCount = 2; - }); + }); + manifest.addVariant(16, (variant) => { + variant.language = 'fr'; + variant.addVideo(17, (stream) => { + stream.size(256, 144); }); - period.addVariant(16, (variant) => { - variant.language = 'fr'; - variant.addVideo(17, (stream) => { - stream.size(256, 144); - }); - variant.addAudio(18, (stream) => { - stream.channelsCount = 2; - }); + variant.addAudio(18, (stream) => { + stream.channelsCount = 2; }); }); }); diff --git a/test/util/stream_utils_unit.js b/test/util/stream_utils_unit.js index 63e853aba6..1d534011a4 100644 --- a/test/util/stream_utils_unit.js +++ b/test/util/stream_utils_unit.js @@ -14,94 +14,86 @@ describe('StreamUtils', () => { describe('filterStreamsByLanguageAndRole', () => { it('chooses text streams in user\'s preferred language', () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addTextStream(1, (stream) => { - stream.language = 'en'; - }); - period.addTextStream(2, (stream) => { - stream.language = 'es'; - }); - period.addTextStream(3, (stream) => { - stream.language = 'en'; - }); + manifest.addTextStream(1, (stream) => { + stream.language = 'en'; + }); + manifest.addTextStream(2, (stream) => { + stream.language = 'es'; + }); + manifest.addTextStream(3, (stream) => { + stream.language = 'en'; }); }); const chosen = filterStreamsByLanguageAndRole( - manifest.periods[0].textStreams, + manifest.textStreams, 'en', ''); expect(chosen.length).toBe(2); - expect(chosen[0]).toBe(manifest.periods[0].textStreams[0]); - expect(chosen[1]).toBe(manifest.periods[0].textStreams[2]); + expect(chosen[0]).toBe(manifest.textStreams[0]); + expect(chosen[1]).toBe(manifest.textStreams[2]); }); it('chooses primary text streams', () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addTextStream(1); - period.addTextStream(2, (stream) => { - stream.primary = true; - }); - period.addTextStream(3, (stream) => { - stream.primary = true; - }); + manifest.addTextStream(1); + manifest.addTextStream(2, (stream) => { + stream.primary = true; + }); + manifest.addTextStream(3, (stream) => { + stream.primary = true; }); }); const chosen = filterStreamsByLanguageAndRole( - manifest.periods[0].textStreams, + manifest.textStreams, 'en', ''); expect(chosen.length).toBe(2); - expect(chosen[0]).toBe(manifest.periods[0].textStreams[1]); - expect(chosen[1]).toBe(manifest.periods[0].textStreams[2]); + expect(chosen[0]).toBe(manifest.textStreams[1]); + expect(chosen[1]).toBe(manifest.textStreams[2]); }); it('chooses text streams in preferred language and role', () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addTextStream(1, (stream) => { - stream.language = 'en'; - stream.roles = ['main', 'commentary']; - }); - period.addTextStream(2, (stream) => { - stream.language = 'es'; - }); - period.addTextStream(3, (stream) => { - stream.language = 'en'; - stream.roles = ['caption']; - }); + manifest.addTextStream(1, (stream) => { + stream.language = 'en'; + stream.roles = ['main', 'commentary']; + }); + manifest.addTextStream(2, (stream) => { + stream.language = 'es'; + }); + manifest.addTextStream(3, (stream) => { + stream.language = 'en'; + stream.roles = ['caption']; }); }); const chosen = filterStreamsByLanguageAndRole( - manifest.periods[0].textStreams, + manifest.textStreams, 'en', 'main'); expect(chosen.length).toBe(1); - expect(chosen[0]).toBe(manifest.periods[0].textStreams[0]); + expect(chosen[0]).toBe(manifest.textStreams[0]); }); it('prefers no-role streams if there is no preferred role', () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addTextStream(0, (stream) => { - stream.language = 'en'; - stream.roles = ['commentary']; - }); - period.addTextStream(1, (stream) => { - stream.language = 'en'; - }); - period.addTextStream(2, (stream) => { - stream.language = 'en'; - stream.roles = ['secondary']; - }); + manifest.addTextStream(0, (stream) => { + stream.language = 'en'; + stream.roles = ['commentary']; + }); + manifest.addTextStream(1, (stream) => { + stream.language = 'en'; + }); + manifest.addTextStream(2, (stream) => { + stream.language = 'en'; + stream.roles = ['secondary']; }); }); const chosen = filterStreamsByLanguageAndRole( - manifest.periods[0].textStreams, + manifest.textStreams, 'en', ''); expect(chosen.length).toBe(1); @@ -110,23 +102,21 @@ describe('StreamUtils', () => { it('ignores no-role streams if there is a preferred role', () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addTextStream(0, (stream) => { - stream.language = 'en'; - stream.roles = ['commentary']; - }); - period.addTextStream(1, (stream) => { - stream.language = 'en'; - }); - period.addTextStream(2, (stream) => { - stream.language = 'en'; - stream.roles = ['secondary']; - }); + manifest.addTextStream(0, (stream) => { + stream.language = 'en'; + stream.roles = ['commentary']; + }); + manifest.addTextStream(1, (stream) => { + stream.language = 'en'; + }); + manifest.addTextStream(2, (stream) => { + stream.language = 'en'; + stream.roles = ['secondary']; }); }); const chosen = filterStreamsByLanguageAndRole( - manifest.periods[0].textStreams, + manifest.textStreams, 'en', 'main'); // A role that is not present. expect(chosen.length).toBe(1); @@ -136,36 +126,34 @@ describe('StreamUtils', () => { it('chooses only one role, even if none is preferred', () => { // Regression test for https://github.com/google/shaka-player/issues/949 manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addTextStream(0, (stream) => { - stream.language = 'en'; - stream.roles = ['commentary']; - }); - period.addTextStream(1, (stream) => { - stream.language = 'en'; - stream.roles = ['commentary']; - }); - period.addTextStream(2, (stream) => { - stream.language = 'en'; - stream.roles = ['secondary']; - }); - period.addTextStream(3, (stream) => { - stream.language = 'en'; - stream.roles = ['secondary']; - }); - period.addTextStream(4, (stream) => { - stream.language = 'en'; - stream.roles = ['main']; - }); - period.addTextStream(5, (stream) => { - stream.language = 'en'; - stream.roles = ['main']; - }); + manifest.addTextStream(0, (stream) => { + stream.language = 'en'; + stream.roles = ['commentary']; + }); + manifest.addTextStream(1, (stream) => { + stream.language = 'en'; + stream.roles = ['commentary']; + }); + manifest.addTextStream(2, (stream) => { + stream.language = 'en'; + stream.roles = ['secondary']; + }); + manifest.addTextStream(3, (stream) => { + stream.language = 'en'; + stream.roles = ['secondary']; + }); + manifest.addTextStream(4, (stream) => { + stream.language = 'en'; + stream.roles = ['main']; + }); + manifest.addTextStream(5, (stream) => { + stream.language = 'en'; + stream.roles = ['main']; }); }); const chosen = filterStreamsByLanguageAndRole( - manifest.periods[0].textStreams, + manifest.textStreams, 'en', ''); // Which role is chosen is an implementation detail. @@ -177,42 +165,40 @@ describe('StreamUtils', () => { it('chooses only one role, even if all are primary', () => { // Regression test for https://github.com/google/shaka-player/issues/949 manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addTextStream(0, (stream) => { - stream.language = 'en'; - stream.primary = true; - stream.roles = ['commentary']; - }); - period.addTextStream(1, (stream) => { - stream.language = 'en'; - stream.primary = true; - stream.roles = ['commentary']; - }); - period.addTextStream(2, (stream) => { - stream.language = 'en'; - stream.primary = true; - stream.roles = ['secondary']; - }); - period.addTextStream(3, (stream) => { - stream.language = 'en'; - stream.primary = true; - stream.roles = ['secondary']; - }); - period.addTextStream(4, (stream) => { - stream.language = 'en'; - stream.primary = true; - stream.roles = ['main']; - }); - period.addTextStream(5, (stream) => { - stream.language = 'en'; - stream.primary = true; - stream.roles = ['main']; - }); + manifest.addTextStream(0, (stream) => { + stream.language = 'en'; + stream.primary = true; + stream.roles = ['commentary']; + }); + manifest.addTextStream(1, (stream) => { + stream.language = 'en'; + stream.primary = true; + stream.roles = ['commentary']; + }); + manifest.addTextStream(2, (stream) => { + stream.language = 'en'; + stream.primary = true; + stream.roles = ['secondary']; + }); + manifest.addTextStream(3, (stream) => { + stream.language = 'en'; + stream.primary = true; + stream.roles = ['secondary']; + }); + manifest.addTextStream(4, (stream) => { + stream.language = 'en'; + stream.primary = true; + stream.roles = ['main']; + }); + manifest.addTextStream(5, (stream) => { + stream.language = 'en'; + stream.primary = true; + stream.roles = ['main']; }); }); const chosen = filterStreamsByLanguageAndRole( - manifest.periods[0].textStreams, + manifest.textStreams, 'zh', ''); // Which role is chosen is an implementation detail. @@ -224,28 +210,26 @@ describe('StreamUtils', () => { it('chooses only one language, even if all are primary', () => { // Regression test for https://github.com/google/shaka-player/issues/918 manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addTextStream(0, (stream) => { - stream.language = 'en'; - stream.primary = true; - }); - period.addTextStream(1, (stream) => { - stream.language = 'en'; - stream.primary = true; - }); - period.addTextStream(2, (stream) => { - stream.language = 'es'; - stream.primary = true; - }); - period.addTextStream(3, (stream) => { - stream.language = 'es'; - stream.primary = true; - }); + manifest.addTextStream(0, (stream) => { + stream.language = 'en'; + stream.primary = true; + }); + manifest.addTextStream(1, (stream) => { + stream.language = 'en'; + stream.primary = true; + }); + manifest.addTextStream(2, (stream) => { + stream.language = 'es'; + stream.primary = true; + }); + manifest.addTextStream(3, (stream) => { + stream.language = 'es'; + stream.primary = true; }); }); const chosen = filterStreamsByLanguageAndRole( - manifest.periods[0].textStreams, + manifest.textStreams, 'zh', ''); // Which language is chosen is an implementation detail. @@ -257,40 +241,38 @@ describe('StreamUtils', () => { it('chooses a role from among primary streams without language match', () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addTextStream(0, (stream) => { - stream.language = 'en'; - stream.primary = true; - stream.roles = ['commentary']; - }); - period.addTextStream(1, (stream) => { - stream.language = 'en'; - stream.primary = true; - stream.roles = ['commentary']; - }); - period.addTextStream(2, (stream) => { - stream.language = 'en'; - stream.roles = ['secondary']; - }); - period.addTextStream(3, (stream) => { - stream.language = 'en'; - stream.roles = ['secondary']; - }); - period.addTextStream(4, (stream) => { - stream.language = 'en'; - stream.primary = true; - stream.roles = ['main']; - }); - period.addTextStream(5, (stream) => { - stream.language = 'en'; - stream.primary = true; - stream.roles = ['main']; - }); + manifest.addTextStream(0, (stream) => { + stream.language = 'en'; + stream.primary = true; + stream.roles = ['commentary']; + }); + manifest.addTextStream(1, (stream) => { + stream.language = 'en'; + stream.primary = true; + stream.roles = ['commentary']; + }); + manifest.addTextStream(2, (stream) => { + stream.language = 'en'; + stream.roles = ['secondary']; + }); + manifest.addTextStream(3, (stream) => { + stream.language = 'en'; + stream.roles = ['secondary']; + }); + manifest.addTextStream(4, (stream) => { + stream.language = 'en'; + stream.primary = true; + stream.roles = ['main']; + }); + manifest.addTextStream(5, (stream) => { + stream.language = 'en'; + stream.primary = true; + stream.roles = ['main']; }); }); const chosen = filterStreamsByLanguageAndRole( - manifest.periods[0].textStreams, + manifest.textStreams, 'zh', ''); // Which role is chosen is an implementation detail. @@ -307,40 +289,38 @@ describe('StreamUtils', () => { it('chooses a role from best language match, in spite of primary', () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addTextStream(0, (stream) => { - stream.language = 'en'; - stream.primary = true; - stream.roles = ['commentary']; - }); - period.addTextStream(1, (stream) => { - stream.language = 'en'; - stream.primary = true; - stream.roles = ['commentary']; - }); - period.addTextStream(2, (stream) => { - stream.language = 'zh'; - stream.roles = ['secondary']; - }); - period.addTextStream(3, (stream) => { - stream.language = 'zh'; - stream.roles = ['secondary']; - }); - period.addTextStream(4, (stream) => { - stream.language = 'en'; - stream.primary = true; - stream.roles = ['main']; - }); - period.addTextStream(5, (stream) => { - stream.language = 'en'; - stream.primary = true; - stream.roles = ['main']; - }); + manifest.addTextStream(0, (stream) => { + stream.language = 'en'; + stream.primary = true; + stream.roles = ['commentary']; + }); + manifest.addTextStream(1, (stream) => { + stream.language = 'en'; + stream.primary = true; + stream.roles = ['commentary']; + }); + manifest.addTextStream(2, (stream) => { + stream.language = 'zh'; + stream.roles = ['secondary']; + }); + manifest.addTextStream(3, (stream) => { + stream.language = 'zh'; + stream.roles = ['secondary']; + }); + manifest.addTextStream(4, (stream) => { + stream.language = 'en'; + stream.primary = true; + stream.roles = ['main']; + }); + manifest.addTextStream(5, (stream) => { + stream.language = 'en'; + stream.primary = true; + stream.roles = ['main']; }); }); const chosen = filterStreamsByLanguageAndRole( - manifest.periods[0].textStreams, + manifest.textStreams, 'zh', ''); expect(chosen.length).toBe(2); @@ -354,92 +334,84 @@ describe('StreamUtils', () => { describe('filterVariantsByAudioChannelCount', () => { it('chooses variants with preferred audio channels count', () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addAudio(0, (stream) => { - stream.channelsCount = 2; - }); + manifest.addVariant(0, (variant) => { + variant.addAudio(0, (stream) => { + stream.channelsCount = 2; }); - period.addVariant(1, (variant) => { - variant.addAudio(1, (stream) => { - stream.channelsCount = 6; - }); + }); + manifest.addVariant(1, (variant) => { + variant.addAudio(1, (stream) => { + stream.channelsCount = 6; }); - period.addVariant(2, (variant) => { - variant.addAudio(2, (stream) => { - stream.channelsCount = 2; - }); + }); + manifest.addVariant(2, (variant) => { + variant.addAudio(2, (stream) => { + stream.channelsCount = 2; }); }); }); - const chosen = filterVariantsByAudioChannelCount( - manifest.periods[0].variants, 2); + const chosen = filterVariantsByAudioChannelCount(manifest.variants, 2); expect(chosen.length).toBe(2); - expect(chosen[0]).toBe(manifest.periods[0].variants[0]); - expect(chosen[1]).toBe(manifest.periods[0].variants[2]); + expect(chosen[0]).toBe(manifest.variants[0]); + expect(chosen[1]).toBe(manifest.variants[2]); }); it('chooses variants with largest audio channel count less than config' + ' when no exact audio channel count match is possible', () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addAudio(0, (stream) => { - stream.channelsCount = 2; - }); + manifest.addVariant(0, (variant) => { + variant.addAudio(0, (stream) => { + stream.channelsCount = 2; }); - period.addVariant(1, (variant) => { - variant.addAudio(1, (stream) => { - stream.channelsCount = 8; - }); + }); + manifest.addVariant(1, (variant) => { + variant.addAudio(1, (stream) => { + stream.channelsCount = 8; }); - period.addVariant(2, (variant) => { - variant.addAudio(2, (stream) => { - stream.channelsCount = 2; - }); + }); + manifest.addVariant(2, (variant) => { + variant.addAudio(2, (stream) => { + stream.channelsCount = 2; }); }); }); const chosen = filterVariantsByAudioChannelCount( - manifest.periods[0].variants, 6); + manifest.variants, 6); expect(chosen.length).toBe(2); - expect(chosen[0]).toBe(manifest.periods[0].variants[0]); - expect(chosen[1]).toBe(manifest.periods[0].variants[2]); + expect(chosen[0]).toBe(manifest.variants[0]); + expect(chosen[1]).toBe(manifest.variants[2]); }); it('chooses variants with fewest audio channels when none fit in the ' + 'config', () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addVariant(0, (variant) => { - variant.addAudio(0, (stream) => { - stream.channelsCount = 6; - }); + manifest.addVariant(0, (variant) => { + variant.addAudio(0, (stream) => { + stream.channelsCount = 6; }); - period.addVariant(1, (variant) => { - variant.addAudio(1, (stream) => { - stream.channelsCount = 8; - }); + }); + manifest.addVariant(1, (variant) => { + variant.addAudio(1, (stream) => { + stream.channelsCount = 8; }); - period.addVariant(2, (variant) => { - variant.addAudio(2, (stream) => { - stream.channelsCount = 6; - }); + }); + manifest.addVariant(2, (variant) => { + variant.addAudio(2, (stream) => { + stream.channelsCount = 6; }); }); }); - const chosen = filterVariantsByAudioChannelCount( - manifest.periods[0].variants, 2); + const chosen = filterVariantsByAudioChannelCount(manifest.variants, 2); expect(chosen.length).toBe(2); - expect(chosen[0]).toBe(manifest.periods[0].variants[0]); - expect(chosen[1]).toBe(manifest.periods[0].variants[2]); + expect(chosen[0]).toBe(manifest.variants[0]); + expect(chosen[1]).toBe(manifest.variants[2]); }); }); - describe('filterNewPeriod', () => { + describe('filterManifest', () => { let fakeDrmEngine; beforeAll(() => { @@ -448,34 +420,29 @@ describe('StreamUtils', () => { it('filters text streams with the full MIME type', () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { - manifest.addPeriod(0, (period) => { - period.addTextStream(1, (stream) => { - stream.mimeType = 'text/vtt'; - }); - period.addTextStream(2, (stream) => { - stream.mime('application/mp4', 'wvtt'); - }); - period.addTextStream(3, (stream) => { - stream.mimeType = 'text/bogus'; - }); - period.addTextStream(4, (stream) => { - stream.mime('application/mp4', 'bogus'); - }); + manifest.addTextStream(1, (stream) => { + stream.mimeType = 'text/vtt'; + }); + manifest.addTextStream(2, (stream) => { + stream.mime('application/mp4', 'wvtt'); + }); + manifest.addTextStream(3, (stream) => { + stream.mimeType = 'text/bogus'; + }); + manifest.addTextStream(4, (stream) => { + stream.mime('application/mp4', 'bogus'); }); }); - const noAudio = null; - const noVideo = null; - shaka.util.StreamUtils.filterNewPeriod( - fakeDrmEngine, noAudio, noVideo, manifest.periods[0]); + const noVariant = null; + shaka.util.StreamUtils.filterManifest(fakeDrmEngine, noVariant, manifest); // Covers a regression in which we would remove streams with codecs. // The last two streams should be removed because their full MIME types // are bogus. - expect(manifest.periods[0].textStreams.length).toBe(2); - const textStreams = manifest.periods[0].textStreams; - expect(textStreams[0].id).toBe(1); - expect(textStreams[1].id).toBe(2); + expect(manifest.textStreams.length).toBe(2); + expect(manifest.textStreams[0].id).toBe(1); + expect(manifest.textStreams[1].id).toBe(2); }); }); }); diff --git a/ui/text_selection.js b/ui/text_selection.js index 3111bda61e..d6ee41bb58 100644 --- a/ui/text_selection.js +++ b/ui/text_selection.js @@ -132,8 +132,7 @@ shaka.ui.TextSelection = class extends shaka.ui.SettingsMenu { const offButton = shaka.util.Dom.createHTMLElement('button'); offButton.classList.add('shaka-turn-captions-off-button'); this.eventManager.listen(offButton, 'click', () => { - const p = this.player.setTextTrackVisibility(false); - p.catch(() => {}); // TODO(#1993): Handle possible errors. + this.player.setTextTrackVisibility(false); this.updateTextLanguages_(); });