From b58a7fcbe36dbee7a38624d70075f2cd474001a4 Mon Sep 17 00:00:00 2001 From: David LaPalomento Date: Mon, 13 Jul 2015 15:03:07 -0400 Subject: [PATCH] duration should work when video or audio is missing If audio or video data is missing for an HLS stream, duration calculations should just use the info that is available. Fixes #341. --- src/playlist.js | 44 +++++++++++++++++++-------- src/videojs-hls.js | 12 +++++--- test/playlist_test.js | 24 +++++++++++++++ test/segment-parser.js | 11 +++++++ test/videojs-hls_test.js | 64 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 139 insertions(+), 16 deletions(-) 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({