diff --git a/src/playlist.js b/src/playlist.js index d5ec7767e..41a9433d2 100644 --- a/src/playlist.js +++ b/src/playlist.js @@ -5,7 +5,23 @@ 'use strict'; var DEFAULT_TARGET_DURATION = 10; - var accumulateDuration, ascendingNumeric, duration, intervalDuration, rangeDuration, seekable; + var accumulateDuration, ascendingNumeric, duration, intervalDuration, optionalMin, optionalMax, rangeDuration, seekable; + + // Math.min that will return the alternative input if one of its + // parameters in undefined + optionalMin = function(left, right) { + left = isFinite(left) ? left : Infinity; + right = isFinite(right) ? right : Infinity; + return Math.min(left, right); + }; + + // Math.max that will return the alternative input if one of its + // parameters in undefined + optionalMax = function(left, right) { + left = isFinite(left) ? left: -Infinity; + right = isFinite(right) ? right: -Infinity; + return Math.max(left, right); + }; // Array.sort comparator to sort numbers in ascending order ascendingNumeric = function(left, right) { @@ -91,7 +107,8 @@ // available PTS information for (left = range.start; left < range.end; left++) { segment = playlist.segments[left]; - if (segment.minVideoPts !== undefined) { + if (segment.minVideoPts !== undefined || + segment.minAudioPts !== undefined) { break; } result += segment.duration || targetDuration; @@ -100,10 +117,12 @@ // see if there's enough information to include the trailing time if (includeTrailingTime) { segment = playlist.segments[range.end]; - if (segment && segment.minVideoPts !== undefined) { + if (segment && + (segment.minVideoPts !== undefined || + segment.minAudioPts !== undefined)) { result += 0.001 * - (Math.min(segment.minVideoPts, segment.minAudioPts) - - Math.min(playlist.segments[left].minVideoPts, + (optionalMin(segment.minVideoPts, segment.minAudioPts) - + optionalMin(playlist.segments[left].minVideoPts, playlist.segments[left].minAudioPts)); return result; } @@ -112,7 +131,8 @@ // do the same thing while finding the latest segment for (right = range.end - 1; right >= left; right--) { segment = playlist.segments[right]; - if (segment.maxVideoPts !== undefined) { + if (segment.maxVideoPts !== undefined || + segment.maxAudioPts !== undefined) { break; } result += segment.duration || targetDuration; @@ -121,9 +141,9 @@ // add in the PTS interval in seconds between them if (right >= left) { result += 0.001 * - (Math.max(playlist.segments[right].maxVideoPts, + (optionalMax(playlist.segments[right].maxVideoPts, playlist.segments[right].maxAudioPts) - - Math.min(playlist.segments[left].minVideoPts, + optionalMin(playlist.segments[left].minVideoPts, playlist.segments[left].minAudioPts)); } @@ -158,7 +178,7 @@ targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; // estimate expired segment duration using the target duration - expiredSegmentCount = Math.max(playlist.mediaSequence - startSequence, 0); + expiredSegmentCount = optionalMax(playlist.mediaSequence - startSequence, 0); result += expiredSegmentCount * targetDuration; // accumulate the segment durations into the result @@ -257,9 +277,9 @@ // from the result. for (i = playlist.segments.length - 1; i >= 0 && liveBuffer > 0; i--) { segment = playlist.segments[i]; - pending = Math.min(duration(playlist, - playlist.mediaSequence + i, - playlist.mediaSequence + i + 1), + pending = optionalMin(duration(playlist, + playlist.mediaSequence + i, + playlist.mediaSequence + i + 1), liveBuffer); liveBuffer -= pending; end -= pending; diff --git a/src/videojs-hls.js b/src/videojs-hls.js index af56d27ad..e24eb0fcf 100644 --- a/src/videojs-hls.js +++ b/src/videojs-hls.js @@ -899,10 +899,14 @@ videojs.Hls.prototype.drainBuffer = function(event) { if (this.segmentParser_.tagsAvailable()) { // record PTS information for the segment so we can calculate // accurate durations and seek reliably - segment.minVideoPts = this.segmentParser_.stats.minVideoPts(); - segment.maxVideoPts = this.segmentParser_.stats.maxVideoPts(); - segment.minAudioPts = this.segmentParser_.stats.minAudioPts(); - segment.maxAudioPts = this.segmentParser_.stats.maxAudioPts(); + if (this.segmentParser_.stats.h264Tags()) { + segment.minVideoPts = this.segmentParser_.stats.minVideoPts(); + segment.maxVideoPts = this.segmentParser_.stats.maxVideoPts(); + } + if (this.segmentParser_.stats.aacTags()) { + segment.minAudioPts = this.segmentParser_.stats.minAudioPts(); + segment.maxAudioPts = this.segmentParser_.stats.maxAudioPts(); + } } while (this.segmentParser_.tagsAvailable()) { diff --git a/test/playlist_test.js b/test/playlist_test.js index cf1c775c1..5c89abba9 100644 --- a/test/playlist_test.js +++ b/test/playlist_test.js @@ -259,6 +259,30 @@ equal(duration, 30.1, 'used the PTS-based interval'); }); + test('works for media without audio', function() { + equal(Playlist.duration({ + mediaSequence: 0, + endList: true, + segments: [{ + minVideoPts: 0, + maxVideoPts: 9 * 1000, + uri: 'no-audio.ts' + }] + }), 9, 'used video PTS values'); + }); + + test('works for media without video', function() { + equal(Playlist.duration({ + mediaSequence: 0, + endList: true, + segments: [{ + minAudioPts: 0, + maxAudioPts: 9 * 1000, + uri: 'no-video.ts' + }] + }), 9, 'used video PTS values'); + }); + test('uses the largest continuous available PTS ranges', function() { var playlist = { mediaSequence: 0, diff --git a/test/segment-parser.js b/test/segment-parser.js index 739100cd2..ab16fdd45 100644 --- a/test/segment-parser.js +++ b/test/segment-parser.js @@ -284,6 +284,17 @@ equal(packets.length, 1, 'parsed non-payload metadata packet'); }); + test('returns undefined for PTS stats when a track is missing', function() { + parser.parseSegmentBinaryData(new Uint8Array(makePacket({ + programs: { + 0x01: [0x01] + } + }))); + + strictEqual(parser.stats.h264Tags(), 0, 'no video tags yet'); + strictEqual(parser.stats.aacTags(), 0, 'no audio tags yet'); + }); + test('parses the first bipbop segment', function() { parser.parseSegmentBinaryData(window.bcSegment); diff --git a/test/videojs-hls_test.js b/test/videojs-hls_test.js index 6b6c3b90d..ea93090d1 100644 --- a/test/videojs-hls_test.js +++ b/test/videojs-hls_test.js @@ -121,12 +121,18 @@ var ]); this.stats = { + h264Tags: function() { + return tags.length; + }, minVideoPts: function() { return tags[0].pts; }, maxVideoPts: function() { return tags[tags.length - 1].pts; }, + aacTags: function() { + return tags.length; + }, minAudioPts: function() { return tags[0].pts; }, @@ -1044,6 +1050,64 @@ test('records the min and max PTS values for a segment', function() { equal(player.hls.playlists.media().segments[0].maxAudioPts, 10, 'recorded max audio pts'); }); +test('records PTS values for video-only segments', function() { + var tags = []; + videojs.Hls.SegmentParser = mockSegmentParser(tags); + player.src({ + src: 'manifest/media.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + openMediaSource(player); + standardXHRResponse(requests.pop()); // media.m3u8 + + player.hls.segmentParser_.stats.aacTags = function() { + return 0; + }; + player.hls.segmentParser_.stats.minAudioPts = function() { + throw new Error('No audio tags'); + }; + player.hls.segmentParser_.stats.maxAudioPts = function() { + throw new Error('No audio tags'); + }; + tags.push({ pts: 0, bytes: new Uint8Array(1) }); + tags.push({ pts: 10, bytes: new Uint8Array(1) }); + standardXHRResponse(requests.pop()); // segment 0 + + equal(player.hls.playlists.media().segments[0].minVideoPts, 0, 'recorded min video pts'); + equal(player.hls.playlists.media().segments[0].maxVideoPts, 10, 'recorded max video pts'); + strictEqual(player.hls.playlists.media().segments[0].minAudioPts, undefined, 'min audio pts is undefined'); + strictEqual(player.hls.playlists.media().segments[0].maxAudioPts, undefined, 'max audio pts is undefined'); +}); + +test('records PTS values for audio-only segments', function() { + var tags = []; + videojs.Hls.SegmentParser = mockSegmentParser(tags); + player.src({ + src: 'manifest/media.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + openMediaSource(player); + standardXHRResponse(requests.pop()); // media.m3u8 + + player.hls.segmentParser_.stats.h264Tags = function() { + return 0; + }; + player.hls.segmentParser_.stats.minVideoPts = function() { + throw new Error('No video tags'); + }; + player.hls.segmentParser_.stats.maxVideoPts = function() { + throw new Error('No video tags'); + }; + tags.push({ pts: 0, bytes: new Uint8Array(1) }); + tags.push({ pts: 10, bytes: new Uint8Array(1) }); + standardXHRResponse(requests.pop()); // segment 0 + + equal(player.hls.playlists.media().segments[0].minAudioPts, 0, 'recorded min audio pts'); + equal(player.hls.playlists.media().segments[0].maxAudioPts, 10, 'recorded max audio pts'); + strictEqual(player.hls.playlists.media().segments[0].minVideoPts, undefined, 'min video pts is undefined'); + strictEqual(player.hls.playlists.media().segments[0].maxVideoPts, undefined, 'max video pts is undefined'); +}); + test('waits to download new segments until the media playlist is stable', function() { var media; player.src({