diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 554168e7ba..ba0d884eaf 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -409,10 +409,12 @@ shaka.hls.HlsParser = class { this.playerInterface_.newDrmInfo(stream); } - const segments = this.createSegments_( + const {segments, bandwidth} = this.createSegments_( streamInfo.verbatimMediaPlaylistUri, playlist, stream.type, stream.mimeType, mediaSequenceToStartTime, mediaVariables); + stream.bandwidth = bandwidth; + stream.segmentIndex.mergeAndEvict( segments, this.presentationTimeline_.getSegmentAvailabilityStart()); if (segments.length) { @@ -707,7 +709,7 @@ shaka.hls.HlsParser = class { primary: true, audio: type == 'audio' ? streamInfo.stream : null, video: type == 'video' ? streamInfo.stream : null, - bandwidth: 0, + bandwidth: streamInfo.stream.bandwidth || 0, allowedByApplication: true, allowedByKeySystem: true, decodingInfos: [], @@ -2067,6 +2069,7 @@ shaka.hls.HlsParser = class { stream.drmInfos = realStream.drmInfos; stream.keyIds = realStream.keyIds; stream.mimeType = realStream.mimeType; + stream.bandwidth = realStream.bandwidth; // Since we lazy-loaded this content, the player may need to create new // sessions for the DRM info in this stream. @@ -2331,8 +2334,9 @@ shaka.hls.HlsParser = class { const mediaSequenceToStartTime = this.isLive_() ? this.mediaSequenceToStartTimeByType_.get(type) : new Map(); - const segments = this.createSegments_(verbatimMediaPlaylistUri, playlist, - type, mimeType, mediaSequenceToStartTime, mediaVariables); + const {segments, bandwidth} = this.createSegments_( + verbatimMediaPlaylistUri, playlist, type, mimeType, + mediaSequenceToStartTime, mediaVariables); // This new calculation is necessary for Low Latency streams. if (this.isLive_()) { @@ -2366,6 +2370,7 @@ shaka.hls.HlsParser = class { stream.drmInfos = drmInfos; stream.keyIds = keyIds; stream.mimeType = mimeType; + stream.bandwidth = bandwidth; return { stream, @@ -3173,7 +3178,9 @@ shaka.hls.HlsParser = class { } /** - * Parses shaka.hls.Segment objects into shaka.media.SegmentReferences. + * Parses shaka.hls.Segment objects into shaka.media.SegmentReferences and + * get the bandwidth necessary for this segments If it's defined in the + * playlist. * * @param {string} verbatimMediaPlaylistUri * @param {!shaka.hls.Playlist} playlist @@ -3181,7 +3188,8 @@ shaka.hls.HlsParser = class { * @param {string} mimeType * @param {!Map.} mediaSequenceToStartTime * @param {!Map.} variables - * @return {!Array.} + * @return {{segments: !Array., + * bandwidth: (number|undefined)}} * @private */ createSegments_(verbatimMediaPlaylistUri, playlist, type, mimeType, @@ -3219,6 +3227,9 @@ shaka.hls.HlsParser = class { let previousReference = null; let lastDiscontinuityStartTime = firstStartTime; + /** @type {!Array.<{bitrate: number, duration: number}>} */ + const bitrates = []; + for (let i = 0; i < hlsSegments.length; i++) { const item = hlsSegments[i]; const startTime = @@ -3273,6 +3284,21 @@ shaka.hls.HlsParser = class { hlsAes128Key); if (reference) { + const bitrate = shaka.hls.Utils.getFirstTagWithNameAsNumber( + item.tags, 'EXT-X-BITRATE'); + if (bitrate) { + bitrates.push({ + bitrate, + duration: reference.endTime - reference.startTime, + }); + } else if (bitrates.length) { + // It applies to every segment between it and the next EXT-X-BITRATE, + // so we use the latest bitrate value + const prevBitrate = bitrates.pop(); + prevBitrate.duration += reference.endTime - reference.startTime; + bitrates.push(prevBitrate); + } + previousReference = reference; reference.discontinuitySequence = discontinuitySequence; @@ -3287,6 +3313,16 @@ shaka.hls.HlsParser = class { } } + let bandwidth = undefined; + if (bitrates.length) { + const duration = bitrates.reduce((sum, value) => { + return sum + value.duration; + }, 0); + bandwidth = Math.round(bitrates.reduce((sum, value) => { + return sum + value.bitrate * value.duration; + }, 0) / duration * 1000); + } + // If some segments have sync times, but not all, extrapolate the sync // times of the ones with none. const someSyncTime = references.some((ref) => ref.syncTime != null); @@ -3372,7 +3408,10 @@ shaka.hls.HlsParser = class { } } - return references; + return { + segments: references, + bandwidth, + }; } /** diff --git a/test/hls/hls_parser_unit.js b/test/hls/hls_parser_unit.js index d28c0347d9..1478d7e6a1 100644 --- a/test/hls/hls_parser_unit.js +++ b/test/hls/hls_parser_unit.js @@ -4582,6 +4582,47 @@ describe('HlsParser', () => { expect(actualManifest.presentationTimeline.getDuration()).toBe(5); }); + it('parses #EXT-X-BITRATE', async () => { + const media = [ + '#EXTM3U\n', + '#EXT-X-PLAYLIST-TYPE:VOD\n', + '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', + '#EXTINF:5,\n', + '#EXT-X-BYTERANGE:121090@616\n', + '#EXT-X-BITRATE:385\n', + 'main.mp4\n', + '#EXTINF:5,\n', + '#EXT-X-BYTERANGE:121090@616\n', + 'main.mp4\n', + '#EXTINF:5,\n', + '#EXT-X-BYTERANGE:121090@616\n', + 'main.mp4\n', + '#EXTINF:5,\n', + '#EXT-X-BYTERANGE:121090@616\n', + '#EXT-X-BITRATE:340\n', + 'main.mp4\n', + '#EXT-X-BYTERANGE:121090@616\n', + '#EXTINF:5,\n', + '#EXT-X-BITRATE:300\n', + 'main.mp4', + ].join(''); + + const manifest = shaka.test.ManifestGenerator.generate((manifest) => { + manifest.sequenceMode = sequenceMode; + manifest.type = shaka.media.ManifestParser.HLS; + manifest.anyTimeline(); + manifest.addPartialVariant((variant) => { + variant.bandwidth = 359000; + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.mime('video/mp4', 'avc1.42C01E'); + stream.bandwidth = 359000; + }); + }); + }); + + await testHlsParser(media, '', manifest); + }); + it('honors hls.mediaPlaylistFullMimeType', async () => { const media = [ '#EXTM3U\n',