d4da70c7 by Garrett Committed by Jon-Carlos Rivera

Cue Tags (#791)

* Add text track cues for #ext-x-cue-out, #ext-x-cue-in, and #ext-x-cue-cont segment tags
* Change tags track to keep its reference and add documentation
1 parent 6b06b35a
...@@ -24,6 +24,7 @@ Play back HLS with video.js, even where it's not natively supported. ...@@ -24,6 +24,7 @@ Play back HLS with video.js, even where it's not natively supported.
24 - [Source](#source) 24 - [Source](#source)
25 - [List](#list) 25 - [List](#list)
26 - [withCredentials](#withcredentials) 26 - [withCredentials](#withcredentials)
27 - [useCueTags](#usecuetags)
27 - [Runtime Properties](#runtime-properties) 28 - [Runtime Properties](#runtime-properties)
28 - [hls.playlists.master](#hlsplaylistsmaster) 29 - [hls.playlists.master](#hlsplaylistsmaster)
29 - [hls.playlists.media](#hlsplaylistsmedia) 30 - [hls.playlists.media](#hlsplaylistsmedia)
...@@ -212,6 +213,43 @@ is set to `true`. ...@@ -212,6 +213,43 @@ is set to `true`.
212 See html5rocks's [article](http://www.html5rocks.com/en/tutorials/cors/) 213 See html5rocks's [article](http://www.html5rocks.com/en/tutorials/cors/)
213 for more info. 214 for more info.
214 215
216 ##### useCueTags
217 * Type: `boolean`
218 * can be used as an initialization option
219
220 When the `useCueTags` property is set to `true,` a text track is created with
221 label 'hls-segment-metadata' and kind 'metadata'. The track is then added to
222 `player.textTracks()`. Whenever a segment associated with a cue tag is playing,
223 the cue tags will be listed as a properties inside of a stringified JSON object
224 under its active cue's `text` property. The properties that are currently
225 supported are cueOut, cueOutCont, and cueIn. Changes in active cue may be
226 tracked by following the Video.js cue points API for text tracks. For example:
227
228 ```javascript
229 lettextTracks = player.textTracks();
230 letcuesTrack;
231
232 for (let i = 0; i < textTracks.length; i++) {
233   if (textTracks[i].label === 'hls-segment-metadata') {
234     cuesTrack = textTracks[i];
235   }
236 }
237
238 cuesTrack.addEventListener('cuechange', function() {
239 letactiveCues = cuesTrack.activeCues;
240
241   for (let i = 0; i < activeCues.length; i++) {
242 let activeCue = activeCues[i];
243 let cueData = JSON.parse(activeCue.text);
244
245     console.log('Cue runs from ' + activeCue.startTime +
246 ' to ' + activeCue.endTime +
247 ' with cue tag contents ' +
248 (cueData.cueOut || cueData.cueOutCont || cueData.cueIn));
249   }
250 });
251 ```
252
215 ### Runtime Properties 253 ### Runtime Properties
216 Runtime properties are attached to the tech object when HLS is in 254 Runtime properties are attached to the tech object when HLS is in
217 use. You can get a reference to the HLS source handler like this: 255 use. You can get a reference to the HLS source handler like this:
......
...@@ -6,6 +6,7 @@ import SegmentLoader from './segment-loader'; ...@@ -6,6 +6,7 @@ import SegmentLoader from './segment-loader';
6 import Ranges from './ranges'; 6 import Ranges from './ranges';
7 import videojs from 'video.js'; 7 import videojs from 'video.js';
8 import HlsAudioTrack from './hls-audio-track'; 8 import HlsAudioTrack from './hls-audio-track';
9 import window from 'global/window';
9 10
10 // 5 minute blacklist 11 // 5 minute blacklist
11 const BLACKLIST_DURATION = 5 * 60 * 1000; 12 const BLACKLIST_DURATION = 5 * 60 * 1000;
...@@ -48,7 +49,8 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -48,7 +49,8 @@ export default class MasterPlaylistController extends videojs.EventTarget {
48 mode, 49 mode,
49 tech, 50 tech,
50 bandwidth, 51 bandwidth,
51 externHls 52 externHls,
53 useCueTags
52 }) { 54 }) {
53 super(); 55 super();
54 56
...@@ -58,6 +60,13 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -58,6 +60,13 @@ export default class MasterPlaylistController extends videojs.EventTarget {
58 this.tech_ = tech; 60 this.tech_ = tech;
59 this.hls_ = tech.hls; 61 this.hls_ = tech.hls;
60 this.mode_ = mode; 62 this.mode_ = mode;
63 this.useCueTags_ = useCueTags;
64 if (this.useCueTags_) {
65 this.cueTagsTrack_ = this.tech_.addTextTrack('metadata', 'hls-segment-metadata');
66 this.cueTagsTrack_.inBandMetadataTrackDispatchType = '';
67 this.tech_.textTracks().addTrack_(this.cueTagsTrack_);
68 }
69
61 this.audioTracks_ = []; 70 this.audioTracks_ = [];
62 this.requestOptions_ = { 71 this.requestOptions_ = {
63 withCredentials: this.withCredentials, 72 withCredentials: this.withCredentials,
...@@ -124,6 +133,8 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -124,6 +133,8 @@ export default class MasterPlaylistController extends videojs.EventTarget {
124 return; 133 return;
125 } 134 }
126 135
136 this.updateCues_(updatedPlaylist);
137
127 // TODO: Create a new event on the PlaylistLoader that signals 138 // TODO: Create a new event on the PlaylistLoader that signals
128 // that the segments have changed in some way and use that to 139 // that the segments have changed in some way and use that to
129 // update the SegmentLoader instead of doing it twice here and 140 // update the SegmentLoader instead of doing it twice here and
...@@ -819,4 +830,44 @@ export default class MasterPlaylistController extends videojs.EventTarget { ...@@ -819,4 +830,44 @@ export default class MasterPlaylistController extends videojs.EventTarget {
819 } 830 }
820 }); 831 });
821 } 832 }
833
834 updateCues_(media) {
835 if (!this.useCueTags_ || !media.segments) {
836 return;
837 }
838
839 while (this.cueTagsTrack_.cues.length) {
840 this.cueTagsTrack_.removeCue(this.cueTagsTrack_.cues[0]);
841 }
842
843 let mediaTime = 0;
844
845 for (let i = 0; i < media.segments.length; i++) {
846 let segment = media.segments[i];
847
848 if ('cueOut' in segment || 'cueOutCont' in segment || 'cueIn' in segment) {
849 let cueJson = {};
850
851 if ('cueOut' in segment) {
852 cueJson.cueOut = segment.cueOut;
853 }
854 if ('cueOutCont' in segment) {
855 cueJson.cueOutCont = segment.cueOutCont;
856 }
857 if ('cueIn' in segment) {
858 cueJson.cueIn = segment.cueIn;
859 }
860
861 // Use a short duration for the cue point, as it should trigger for a segment
862 // transition (in this case, defined as the beginning of the segment that the tag
863 // precedes), but keep it for a minimum of 0.5 seconds to remain usable (won't
864 // lose it as an active cue by the time a user retrieves the active cues).
865 this.cueTagsTrack_.addCue(new window.VTTCue(mediaTime,
866 mediaTime + 0.5,
867 JSON.stringify(cueJson)));
868 }
869
870 mediaTime += segment.duration;
871 }
872 }
822 } 873 }
......
...@@ -13,6 +13,7 @@ import MasterPlaylistController from '../src/master-playlist-controller'; ...@@ -13,6 +13,7 @@ import MasterPlaylistController from '../src/master-playlist-controller';
13 import { Hls } from '../src/videojs-contrib-hls'; 13 import { Hls } from '../src/videojs-contrib-hls';
14 /* eslint-enable no-unused-vars */ 14 /* eslint-enable no-unused-vars */
15 import Playlist from '../src/playlist'; 15 import Playlist from '../src/playlist';
16 import window from 'global/window';
16 17
17 QUnit.module('MasterPlaylistController', { 18 QUnit.module('MasterPlaylistController', {
18 beforeEach() { 19 beforeEach() {
...@@ -617,3 +618,184 @@ function() { ...@@ -617,3 +618,184 @@ function() {
617 618
618 Playlist.seekable = origSeekable; 619 Playlist.seekable = origSeekable;
619 }); 620 });
621
622 QUnit.test('calls to update cues on new media', function() {
623 let callCount = 0;
624
625 this.masterPlaylistController.updateCues_ = (media) => callCount++;
626
627 // master
628 standardXHRResponse(this.requests.shift());
629
630 QUnit.equal(callCount, 0, 'no call to update cues on master');
631
632 // media
633 standardXHRResponse(this.requests.shift());
634
635 QUnit.equal(callCount, 1, 'calls to update cues on first media');
636
637 this.masterPlaylistController.masterPlaylistLoader_.trigger('loadedplaylist');
638
639 QUnit.equal(callCount, 2, 'calls to update cues on subsequent media');
640 });
641
642 QUnit.test('calls to update cues on media when no master', function() {
643 this.requests.length = 0;
644 this.player.src({
645 src: 'manifest/media.m3u8',
646 type: 'application/vnd.apple.mpegurl'
647 });
648 this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
649
650 let callCount = 0;
651
652 this.masterPlaylistController.updateCues_ = (media) => callCount++;
653
654 // media
655 standardXHRResponse(this.requests.shift());
656
657 QUnit.equal(callCount, 1, 'calls to update cues on first media');
658
659 this.masterPlaylistController.masterPlaylistLoader_.trigger('loadedplaylist');
660
661 QUnit.equal(callCount, 2, 'calls to update cues on subsequent media');
662 });
663
664 QUnit.test('respects useCueTags option', function() {
665 this.masterPlaylistController.updateCues_({
666 segments: [{
667 duration: 10,
668 tags: ['test']
669 }]
670 });
671
672 QUnit.ok(!this.masterPlaylistController.cueTagsTrack_,
673 'does not create cueTagsTrack_ if useCueTags is falsy');
674 QUnit.equal(this.player.textTracks().length,
675 0,
676 'does not create a text track if useCueTags is falsy');
677
678 this.player.dispose();
679
680 let origHlsOptions = videojs.options.hls;
681
682 videojs.options.hls = {
683 useCueTags: true
684 };
685
686 this.player = createPlayer();
687 this.player.src({
688 src: 'manifest/media.m3u8',
689 type: 'application/vnd.apple.mpegurl'
690 });
691 this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
692
693 QUnit.ok(this.masterPlaylistController.cueTagsTrack_,
694 'creates cueTagsTrack_ if useCueTags is truthy');
695 QUnit.equal(this.masterPlaylistController.cueTagsTrack_.label,
696 'hls-segment-metadata',
697 'cueTagsTrack_ has label of hls-segment-metadata');
698 QUnit.equal(this.player.textTracks()[0], this.masterPlaylistController.cueTagsTrack_,
699 'adds cueTagsTrack as a text track if useCueTags is truthy');
700
701 this.masterPlaylistController.updateCues_({
702 segments: [{
703 duration: 10,
704 cueOut: 'test'
705 }]
706 });
707
708 let cue = this.masterPlaylistController.cueTagsTrack_.cues[0];
709
710 QUnit.equal(cue.startTime,
711 0,
712 'adds cue with correct start time if useCueTags is truthy');
713 QUnit.equal(cue.endTime,
714 0.5,
715 'adds cue with correct end time if useCueTags is truthy');
716 QUnit.equal(cue.text,
717 JSON.stringify({ cueOut: 'test' }),
718 'adds cue with correct text if useCueTags is truthy');
719
720 videojs.options.hls = origHlsOptions;
721 });
722
723 QUnit.test('update tag cues', function() {
724 let origHlsOptions = videojs.options.hls;
725
726 videojs.options.hls = {
727 useCueTags: true
728 };
729
730 this.player = createPlayer();
731 this.player.src({
732 src: 'manifest/master.m3u8',
733 type: 'application/vnd.apple.mpegurl'
734 });
735 this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
736
737 let cueTagsTrack = this.masterPlaylistController.cueTagsTrack_;
738 let testCue = new window.VTTCue(0, 10, 'test');
739
740 cueTagsTrack.addCue(testCue);
741
742 this.masterPlaylistController.updateCues_({});
743
744 QUnit.equal(cueTagsTrack.cues.length,
745 1,
746 'does not change cues if media does not have segment property');
747 QUnit.equal(cueTagsTrack.cues[0],
748 testCue,
749 'does not change cues if media does not have segment property');
750
751 this.masterPlaylistController.updateCues_({
752 segments: []
753 });
754
755 QUnit.equal(cueTagsTrack.cues.length,
756 0,
757 'removes cues even if no segments in playlist');
758
759 this.masterPlaylistController.updateCues_({
760 segments: [{
761 duration: 5.1,
762 cueOut: '11.5'
763 }, {
764 duration: 6.4,
765 cueOutCont: '5.1/11.5'
766 }, {
767 duration: 6,
768 cueIn: ''
769 }]
770 });
771
772 QUnit.equal(cueTagsTrack.cues.length, 3, 'adds a cue for each segment');
773
774 QUnit.equal(cueTagsTrack.cues[0].startTime, 0, 'cue starts at 0');
775 QUnit.equal(cueTagsTrack.cues[0].endTime, 0.5, 'cue ends at start time plus duration');
776 QUnit.equal(JSON.parse(cueTagsTrack.cues[0].text).cueOut, '11.5', 'cueOut matches');
777 QUnit.ok(!('cueOutCont' in JSON.parse(cueTagsTrack.cues[0].text)),
778 'cueOutCont not in cue');
779 QUnit.ok(!('cueIn' in JSON.parse(cueTagsTrack.cues[0].text)), 'cueIn not in cue');
780 QUnit.equal(cueTagsTrack.cues[1].startTime, 5.1, 'cue starts at 5.1');
781 QUnit.equal(cueTagsTrack.cues[1].endTime, 5.6, 'cue ends at start time plus duration');
782 QUnit.equal(JSON.parse(cueTagsTrack.cues[1].text).cueOutCont,
783 '5.1/11.5',
784 'cueOutCont matches');
785 QUnit.ok(!('cueOut' in JSON.parse(cueTagsTrack.cues[1].text)), 'cueOut not in cue');
786 QUnit.ok(!('cueIn' in JSON.parse(cueTagsTrack.cues[1].text)), 'cueIn not in cue');
787 QUnit.equal(cueTagsTrack.cues[2].startTime, 11.5, 'cue starts at 11.5');
788 QUnit.equal(cueTagsTrack.cues[2].endTime, 12, 'cue ends at start time plus duration');
789 QUnit.equal(JSON.parse(cueTagsTrack.cues[2].text).cueIn, '', 'cueIn matches');
790 QUnit.ok(!('cueOut' in JSON.parse(cueTagsTrack.cues[2].text)), 'cueOut not in cue');
791 QUnit.ok(!('cueOutCont' in JSON.parse(cueTagsTrack.cues[2].text)),
792 'cueOutCont not in cue');
793
794 this.masterPlaylistController.updateCues_({
795 segments: []
796 });
797
798 QUnit.equal(cueTagsTrack.cues.length, 0, 'removes old cues on update');
799
800 videojs.options.hls = origHlsOptions;
801 });
......
...@@ -2275,6 +2275,34 @@ QUnit.test('Allows overriding the global beforeRequest function', function() { ...@@ -2275,6 +2275,34 @@ QUnit.test('Allows overriding the global beforeRequest function', function() {
2275 QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, 'one segment request'); 2275 QUnit.equal(this.player.tech_.hls.stats.mediaRequests, 1, 'one segment request');
2276 }); 2276 });
2277 2277
2278 QUnit.test('passes useCueTags hls option to master playlist controller', function() {
2279 this.player.src({
2280 src: 'master.m3u8',
2281 type: 'application/vnd.apple.mpegurl'
2282 });
2283
2284 QUnit.ok(!this.player.tech_.hls.masterPlaylistController_.useCueTags_,
2285 'useCueTags is falsy by default');
2286
2287 let origHlsOptions = videojs.options.hls;
2288
2289 videojs.options.hls = {
2290 useCueTags: true
2291 };
2292
2293 this.player.dispose();
2294 this.player = createPlayer();
2295 this.player.src({
2296 src: 'http://example.com/media.m3u8',
2297 type: 'application/vnd.apple.mpegurl'
2298 });
2299
2300 QUnit.ok(this.player.tech_.hls.masterPlaylistController_.useCueTags_,
2301 'useCueTags passed to master playlist controller');
2302
2303 videojs.options.hls = origHlsOptions;
2304 });
2305
2278 QUnit.module('HLS Integration', { 2306 QUnit.module('HLS Integration', {
2279 beforeEach() { 2307 beforeEach() {
2280 this.env = useFakeEnvironment(); 2308 this.env = useFakeEnvironment();
......