diff --git a/lib/abr/simple_abr_manager.js b/lib/abr/simple_abr_manager.js index a0a763c85..b6299f94b 100644 --- a/lib/abr/simple_abr_manager.js +++ b/lib/abr/simple_abr_manager.js @@ -20,6 +20,7 @@ goog.provide('shaka.abr.SimpleAbrManager'); goog.require('goog.asserts'); goog.require('shaka.abr.EwmaBandwidthEstimator'); goog.require('shaka.log'); +goog.require('shaka.util.Iterables'); goog.require('shaka.util.StreamUtils'); @@ -131,22 +132,20 @@ shaka.abr.SimpleAbrManager = class { // Start by assuming that we will use the first Stream. let chosen = sortedVariants[0] || null; - for (let i = 0; i < sortedVariants.length; ++i) { - const variant = sortedVariants[i]; - const nextVariant = sortedVariants[i + 1] || {bandwidth: Infinity}; - - const minBandwidth = variant.bandwidth / - this.config_.bandwidthDowngradeTarget; - const maxBandwidth = nextVariant.bandwidth / - this.config_.bandwidthUpgradeTarget; + const enumerate = (it) => shaka.util.Iterables.enumerate(it); + for (const {item, next} of enumerate(sortedVariants)) { + const minBandwidth = + item.bandwidth / this.config_.bandwidthDowngradeTarget; + const maxBandwidth = (next || {bandwidth: Infinity}).bandwidth / + this.config_.bandwidthUpgradeTarget; shaka.log.v2('Bandwidth ranges:', - (variant.bandwidth / 1e6).toFixed(3), + (item.bandwidth / 1e6).toFixed(3), (minBandwidth / 1e6).toFixed(3), (maxBandwidth / 1e6).toFixed(3)); if (currentBandwidth >= minBandwidth && currentBandwidth <= maxBandwidth) { - chosen = variant; + chosen = item; } } diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 65a549efb..42e44bc36 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -33,6 +33,7 @@ goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.text.TextEngine'); goog.require('shaka.util.Error'); goog.require('shaka.util.Functional'); +goog.require('shaka.util.Iterables'); goog.require('shaka.util.LanguageUtils'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.MimeUtils'); @@ -430,21 +431,20 @@ shaka.dash.DashParser = class { const periodNodes = XmlUtils.findChildren(mpd, 'Period'); // This uses a for-loop rather than a for-of loop because this needs to look // ahead to the next element. - for (let i = 0; i < periodNodes.length; i++) { - const elem = periodNodes[i]; + const enumerate = (it) => shaka.util.Iterables.enumerate(it); + for (const {i, item: elem, next} of enumerate(periodNodes)) { const start = /** @type {number} */ ( XmlUtils.parseAttr(elem, 'start', XmlUtils.parseDuration, prevEnd)); const givenDuration = XmlUtils.parseAttr(elem, 'duration', XmlUtils.parseDuration); let periodDuration = null; - if (i != periodNodes.length - 1) { + if (next) { // "The difference between the start time of a Period and the start time // of the following Period is the duration of the media content // represented by this Period." - const nextPeriod = periodNodes[i + 1]; const nextStart = - XmlUtils.parseAttr(nextPeriod, 'start', XmlUtils.parseDuration); + XmlUtils.parseAttr(next, 'start', XmlUtils.parseDuration); if (nextStart != null) { periodDuration = nextStart - start; } @@ -473,7 +473,7 @@ shaka.dash.DashParser = class { start: start, duration: periodDuration, node: elem, - isLastPeriod: periodDuration == null || i == periodNodes.length - 1, + isLastPeriod: periodDuration == null || !next, }; const period = this.parsePeriod_(context, baseUris, info); periods.push(period); @@ -497,12 +497,12 @@ shaka.dash.DashParser = class { } if (periodDuration == null) { - if (i != periodNodes.length - 1) { + if (next) { // If the duration is still null and we aren't at the end, then we // will skip any remaining periods. shaka.log.warning( 'Skipping Period', i + 1, 'and any subsequent Periods:', 'Period', - i + 1, 'does not have a valid start time.', periods[i + 1]); + i + 1, 'does not have a valid start time.', next); } // The duration is unknown, so the end is unknown. diff --git a/lib/dash/mpd_utils.js b/lib/dash/mpd_utils.js index c147c3218..dcbadf180 100644 --- a/lib/dash/mpd_utils.js +++ b/lib/dash/mpd_utils.js @@ -23,6 +23,7 @@ goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.util.AbortableOperation'); goog.require('shaka.util.Error'); goog.require('shaka.util.Functional'); +goog.require('shaka.util.Iterables'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.XmlUtils'); @@ -149,8 +150,8 @@ shaka.dash.MpdUtils = class { const timeline = []; let lastEndTime = 0; - for (let i = 0; i < timePoints.length; ++i) { - const timePoint = timePoints[i]; + const enumerate = (it) => shaka.util.Iterables.enumerate(it); + for (const {item: timePoint, next} of enumerate(timePoints)) { let t = XmlUtils.parseAttr(timePoint, 't', XmlUtils.parseNonNegativeInt); const d = XmlUtils.parseAttr(timePoint, 'd', XmlUtils.parseNonNegativeInt); @@ -172,10 +173,9 @@ shaka.dash.MpdUtils = class { let repeat = r || 0; if (repeat < 0) { - if (i + 1 < timePoints.length) { - const nextTimePoint = timePoints[i + 1]; - const nextStartTime = XmlUtils.parseAttr( - nextTimePoint, 't', XmlUtils.parseNonNegativeInt); + if (next) { + const nextStartTime = + XmlUtils.parseAttr(next, 't', XmlUtils.parseNonNegativeInt); if (nextStartTime == null) { shaka.log.warning( 'An "S" element cannot have a negative repeat', diff --git a/lib/dash/segment_template.js b/lib/dash/segment_template.js index 90944ea1d..7438ddcd6 100644 --- a/lib/dash/segment_template.js +++ b/lib/dash/segment_template.js @@ -25,6 +25,7 @@ goog.require('shaka.media.InitSegmentReference'); goog.require('shaka.media.SegmentIndex'); goog.require('shaka.media.SegmentReference'); goog.require('shaka.util.Error'); +goog.require('shaka.util.Iterables'); goog.require('shaka.util.ManifestParserUtils'); @@ -353,11 +354,8 @@ shaka.dash.SegmentTemplate = class { /** @type {!Array.} */ const references = []; - for (let i = 0; i < info.timeline.length; i++) { - const start = info.timeline[i].start; - const unscaledStart = info.timeline[i].unscaledStart; - const end = info.timeline[i].end; - + const enum_ = (it) => shaka.util.Iterables.enumerate(it); + for (const {i, item: {start, unscaledStart, end}} of enum_(info.timeline)) { // Note: i = k - 1, where k indicates the k'th segment listed in the MPD. // (See section 5.3.9.5.3 of the DASH spec.) const segmentReplacement = i + info.startNumber; diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index b4d0d2ce1..492731699 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -40,6 +40,7 @@ goog.require('shaka.util.ArrayUtils'); goog.require('shaka.util.DataViewReader'); goog.require('shaka.util.Error'); goog.require('shaka.util.Functional'); +goog.require('shaka.util.Iterables'); goog.require('shaka.util.LanguageUtils'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.MimeUtils'); @@ -1509,18 +1510,14 @@ shaka.hls.HlsParser = class { initSegmentRef, firstSegmentRef, mimeType, codecs); shaka.log.debug('First segment', firstSegmentUri.split('/').pop(), 'starts at', firstStartTime); - for (let i = 0; i < hlsSegments.length; ++i) { - const hlsSegment = hlsSegments[i]; + const enumerate = (it) => shaka.util.Iterables.enumerate(it); + for (const {i, item} of enumerate(hlsSegments)) { const previousReference = references[references.length - 1]; const startTime = (i == 0) ? firstStartTime : previousReference.endTime; const position = startPosition + i; const reference = this.createSegmentReference_( - playlist, - previousReference, - hlsSegment, - position, - startTime); + playlist, previousReference, item, position, startTime); references.push(reference); } diff --git a/lib/hls/manifest_text_parser.js b/lib/hls/manifest_text_parser.js index ff773026b..712c95b3a 100644 --- a/lib/hls/manifest_text_parser.js +++ b/lib/hls/manifest_text_parser.js @@ -88,16 +88,18 @@ shaka.hls.ManifestTextParser = class { /** {Array.} */ const tags = []; - // Using for loop and instead of for-of, since we start from 1 inside the - // loop. - for (let i = 1; i < lines.length;) { + // Wether to skip the next element; initialize to "true" to skip the first + // element. + let skip = true; + const enumerate = (it) => shaka.util.Iterables.enumerate(it); + for (const {i, item: line, next} of enumerate(lines)) { // Skip comments - if (shaka.hls.Utils.isComment(lines[i])) { - i += 1; + if (shaka.hls.Utils.isComment(line) || skip) { + skip = false; continue; } - const tag = this.parseTag_(lines[i]); + const tag = this.parseTag_(line); if (SEGMENT_TAGS.includes(tag.name)) { if (playlistType != shaka.hls.PlaylistType.MEDIA) { // Only media playlists should contain segment tags @@ -115,14 +117,13 @@ shaka.hls.ManifestTextParser = class { } tags.push(tag); - i += 1; // An EXT-X-STREAM-INF tag is followed by a URI of a media playlist. // Add the URI to the tag object. if (tag.name == 'EXT-X-STREAM-INF') { - const tagUri = new shaka.hls.Attribute('URI', lines[i]); + const tagUri = new shaka.hls.Attribute('URI', next); tag.addAttribute(tagUri); - i += 1; + skip = true; } } diff --git a/lib/util/iterables.js b/lib/util/iterables.js index c33dcb1c8..824a3dbd9 100644 --- a/lib/util/iterables.js +++ b/lib/util/iterables.js @@ -86,4 +86,38 @@ shaka.util.Iterables = class { } return out; } + + /** + * Iterates over an iterable object and includes additional info about each + * item: + * - The zero-based index of the element. + * - The next item in the list, if it exists. + * - The previous item in the list, if it exists. + * + * @param {!Iterable.} iterable + * @return {!Iterable.< + * {i: number, item: T, prev: (T|undefined), next: (T|undefined)}>} + * @template T + */ + static* enumerate(iterable) { + // Since we want the "next" item, we need to skip the first item and return + // elements one in the past. So as we iterate, we are getting the "next" + // element and yielding the one from the previous iteration. + let i = -1; + let prev = undefined; + let item = undefined; + for (const next of iterable) { + if (i >= 0) { + yield {i, item, prev, next}; + } + i++; + prev = item; + item = next; + } + if (i != -1) { + // If it's still -1, there were no items. Otherwise we need to yield + // the last item. + yield {i, prev, item, next: undefined}; + } + } }; diff --git a/lib/util/string_utils.js b/lib/util/string_utils.js index a360b1693..e0af52f25 100644 --- a/lib/util/string_utils.js +++ b/lib/util/string_utils.js @@ -20,6 +20,7 @@ goog.provide('shaka.util.StringUtils'); goog.require('goog.asserts'); goog.require('shaka.log'); goog.require('shaka.util.Error'); +goog.require('shaka.util.Iterables'); /** @@ -181,8 +182,9 @@ shaka.util.StringUtils = class { const utf8 = unescape(encoded); const result = new Uint8Array(utf8.length); - for (let i = 0; i < utf8.length; ++i) { - result[i] = utf8.charCodeAt(i); + const enumerate = (it) => shaka.util.Iterables.enumerate(it); + for (const {i, item} of enumerate(utf8)) { + result[i] = item.charCodeAt(0); } return result.buffer; } @@ -199,8 +201,9 @@ shaka.util.StringUtils = class { static toUTF16(str, littleEndian) { const result = new Uint8Array(str.length * 2); const view = new DataView(result.buffer); - for (let i = 0; i < str.length; ++i) { - const value = str.charCodeAt(i); + const enumerate = (it) => shaka.util.Iterables.enumerate(it); + for (const {i, item} of enumerate(str)) { + const value = item.charCodeAt(0); view.setUint16(/* position= */ i * 2, value, littleEndian); } return result.buffer; diff --git a/lib/util/uint8array_utils.js b/lib/util/uint8array_utils.js index e99624900..2fd52f20e 100644 --- a/lib/util/uint8array_utils.js +++ b/lib/util/uint8array_utils.js @@ -17,6 +17,7 @@ goog.provide('shaka.util.Uint8ArrayUtils'); +goog.require('shaka.util.Iterables'); goog.require('shaka.util.StringUtils'); @@ -65,8 +66,9 @@ shaka.util.Uint8ArrayUtils = class { // byte. const bytes = window.atob(str.replace(/-/g, '+').replace(/_/g, '/')); const result = new Uint8Array(bytes.length); - for (let i = 0; i < bytes.length; ++i) { - result[i] = bytes.charCodeAt(i); + const enumerate = (it) => shaka.util.Iterables.enumerate(it); + for (const {i, item} of enumerate(bytes)) { + result[i] = item.charCodeAt(0); } return result; } diff --git a/test/player_unit.js b/test/player_unit.js index d97bc5363..80c5dda22 100644 --- a/test/player_unit.js +++ b/test/player_unit.js @@ -1759,8 +1759,8 @@ describe('Player', () => { async function runTest(languages, preference, expectedIndex) { const generator = new shaka.test.ManifestGenerator().addPeriod(0); - for (let i = 0; i < languages.length; i++) { - const lang = languages[i]; + const enumerate = (it) => shaka.util.Iterables.enumerate(it); + for (const {i, item: lang} of enumerate(languages)) { if (lang.charAt(0) == '*') { generator .addVariant(i) diff --git a/test/test/util/streaming_engine_util.js b/test/test/util/streaming_engine_util.js index b6e639c62..6ce0c69e5 100644 --- a/test/test/util/streaming_engine_util.js +++ b/test/test/util/streaming_engine_util.js @@ -17,6 +17,8 @@ goog.provide('shaka.test.StreamingEngineUtil'); +goog.require('shaka.util.Iterables'); + shaka.test.StreamingEngineUtil = class { /** @@ -236,9 +238,10 @@ shaka.test.StreamingEngineUtil = class { // Populate the Manifest. let id = 0; - for (let i = 0; i < periodStartTimes.length; ++i) { + const enumerate = (it) => shaka.util.Iterables.enumerate(it); + for (const {i, item} of enumerate(periodStartTimes)) { const period = { - startTime: periodStartTimes[i], + startTime: item, variants: [], textStreams: [], }; diff --git a/test/util/iterables_unit.js b/test/util/iterables_unit.js index 34f9db838..c2d7a19e9 100644 --- a/test/util/iterables_unit.js +++ b/test/util/iterables_unit.js @@ -84,4 +84,66 @@ describe('Iterables', () => { expect(filter(input, (x) => x < 0)).toEqual([]); }); }); + + describe('enumerate', () => { + function enumerate(it) { + return Array.from(Iterables.enumerate(it)); + } + + it('works with no items', () => { + expect(enumerate([])).toEqual([]); + }); + + it('works with one item', () => { + expect(enumerate([999])) + .toEqual([{i: 0, item: 999, prev: undefined, next: undefined}]); + }); + + it('works with special values', () => { + expect(enumerate([[]])) + .toEqual([{i: 0, item: [], prev: undefined, next: undefined}]); + expect(enumerate([0])) + .toEqual([{i: 0, item: 0, prev: undefined, next: undefined}]); + expect(enumerate([null])) + .toEqual([{i: 0, item: null, prev: undefined, next: undefined}]); + expect(enumerate([undefined])) + .toEqual([{i: 0, item: undefined, prev: undefined, next: undefined}]); + }); + + it('works with two items', () => { + expect(enumerate([888, 999])) + .toEqual([ + {i: 0, item: 888, prev: undefined, next: 999}, + {i: 1, item: 999, prev: 888, next: undefined}, + ]); + }); + + it('works with three items', () => { + expect(enumerate([777, 888, 999])) + .toEqual([ + {i: 0, item: 777, prev: undefined, next: 888}, + {i: 1, item: 888, prev: 777, next: 999}, + {i: 2, item: 999, prev: 888, next: undefined}, + ]); + }); + + it('keeps references', () => { + const expected = [ + {a: 'x'}, + {b: 'y'}, + {c: 'z'}, + ]; + const actual = enumerate(expected); + + expect(actual[0].item).toBe(expected[0]); + expect(actual[0].next).toBe(expected[1]); + + expect(actual[1].prev).toBe(expected[0]); + expect(actual[1].item).toBe(expected[1]); + expect(actual[1].next).toBe(expected[2]); + + expect(actual[2].prev).toBe(expected[1]); + expect(actual[2].item).toBe(expected[2]); + }); + }); });