UNPKG

29.8 kBJavaScriptView Raw
1import videojs from 'video.js';
2import PlaylistLoader from './playlist-loader';
3import DashPlaylistLoader from './dash-playlist-loader';
4import noop from './util/noop';
5import {isAudioOnly, playlistMatch} from './playlist.js';
6import logger from './util/logger';
7
8/**
9 * Convert the properties of an HLS track into an audioTrackKind.
10 *
11 * @private
12 */
13const audioTrackKind_ = (properties) => {
14 let kind = properties.default ? 'main' : 'alternative';
15
16 if (properties.characteristics &&
17 properties.characteristics.indexOf('public.accessibility.describes-video') >= 0) {
18 kind = 'main-desc';
19 }
20
21 return kind;
22};
23
24/**
25 * Pause provided segment loader and playlist loader if active
26 *
27 * @param {SegmentLoader} segmentLoader
28 * SegmentLoader to pause
29 * @param {Object} mediaType
30 * Active media type
31 * @function stopLoaders
32 */
33export const stopLoaders = (segmentLoader, mediaType) => {
34 segmentLoader.abort();
35 segmentLoader.pause();
36
37 if (mediaType && mediaType.activePlaylistLoader) {
38 mediaType.activePlaylistLoader.pause();
39 mediaType.activePlaylistLoader = null;
40 }
41};
42
43/**
44 * Start loading provided segment loader and playlist loader
45 *
46 * @param {PlaylistLoader} playlistLoader
47 * PlaylistLoader to start loading
48 * @param {Object} mediaType
49 * Active media type
50 * @function startLoaders
51 */
52export const startLoaders = (playlistLoader, mediaType) => {
53 // Segment loader will be started after `loadedmetadata` or `loadedplaylist` from the
54 // playlist loader
55 mediaType.activePlaylistLoader = playlistLoader;
56 playlistLoader.load();
57};
58
59/**
60 * Returns a function to be called when the media group changes. It performs a
61 * non-destructive (preserve the buffer) resync of the SegmentLoader. This is because a
62 * change of group is merely a rendition switch of the same content at another encoding,
63 * rather than a change of content, such as switching audio from English to Spanish.
64 *
65 * @param {string} type
66 * MediaGroup type
67 * @param {Object} settings
68 * Object containing required information for media groups
69 * @return {Function}
70 * Handler for a non-destructive resync of SegmentLoader when the active media
71 * group changes.
72 * @function onGroupChanged
73 */
74export const onGroupChanged = (type, settings) => () => {
75 const {
76 segmentLoaders: {
77 [type]: segmentLoader,
78 main: mainSegmentLoader
79 },
80 mediaTypes: { [type]: mediaType }
81 } = settings;
82 const activeTrack = mediaType.activeTrack();
83 const activeGroup = mediaType.getActiveGroup();
84 const previousActiveLoader = mediaType.activePlaylistLoader;
85 const lastGroup = mediaType.lastGroup_;
86
87 // the group did not change do nothing
88 if (activeGroup && lastGroup && activeGroup.id === lastGroup.id) {
89 return;
90 }
91
92 mediaType.lastGroup_ = activeGroup;
93 mediaType.lastTrack_ = activeTrack;
94
95 stopLoaders(segmentLoader, mediaType);
96
97 if (!activeGroup || activeGroup.isMasterPlaylist) {
98 // there is no group active or active group is a main playlist and won't change
99 return;
100 }
101
102 if (!activeGroup.playlistLoader) {
103 if (previousActiveLoader) {
104 // The previous group had a playlist loader but the new active group does not
105 // this means we are switching from demuxed to muxed audio. In this case we want to
106 // do a destructive reset of the main segment loader and not restart the audio
107 // loaders.
108 mainSegmentLoader.resetEverything();
109 }
110 return;
111 }
112
113 // Non-destructive resync
114 segmentLoader.resyncLoader();
115
116 startLoaders(activeGroup.playlistLoader, mediaType);
117};
118
119export const onGroupChanging = (type, settings) => () => {
120 const {
121 segmentLoaders: {
122 [type]: segmentLoader
123 },
124 mediaTypes: { [type]: mediaType }
125 } = settings;
126
127 mediaType.lastGroup_ = null;
128
129 segmentLoader.abort();
130 segmentLoader.pause();
131};
132
133/**
134 * Returns a function to be called when the media track changes. It performs a
135 * destructive reset of the SegmentLoader to ensure we start loading as close to
136 * currentTime as possible.
137 *
138 * @param {string} type
139 * MediaGroup type
140 * @param {Object} settings
141 * Object containing required information for media groups
142 * @return {Function}
143 * Handler for a destructive reset of SegmentLoader when the active media
144 * track changes.
145 * @function onTrackChanged
146 */
147export const onTrackChanged = (type, settings) => () => {
148 const {
149 masterPlaylistLoader,
150 segmentLoaders: {
151 [type]: segmentLoader,
152 main: mainSegmentLoader
153 },
154 mediaTypes: { [type]: mediaType }
155 } = settings;
156 const activeTrack = mediaType.activeTrack();
157 const activeGroup = mediaType.getActiveGroup();
158 const previousActiveLoader = mediaType.activePlaylistLoader;
159 const lastTrack = mediaType.lastTrack_;
160
161 // track did not change, do nothing
162 if (lastTrack && activeTrack && lastTrack.id === activeTrack.id) {
163 return;
164 }
165
166 mediaType.lastGroup_ = activeGroup;
167 mediaType.lastTrack_ = activeTrack;
168
169 stopLoaders(segmentLoader, mediaType);
170
171 if (!activeGroup) {
172 // there is no group active so we do not want to restart loaders
173 return;
174 }
175
176 if (activeGroup.isMasterPlaylist) {
177 // track did not change, do nothing
178 if (!activeTrack || !lastTrack || activeTrack.id === lastTrack.id) {
179 return;
180 }
181
182 const mpc = settings.vhs.masterPlaylistController_;
183 const newPlaylist = mpc.selectPlaylist();
184
185 // media will not change do nothing
186 if (mpc.media() === newPlaylist) {
187 return;
188 }
189
190 mediaType.logger_(`track change. Switching master audio from ${lastTrack.id} to ${activeTrack.id}`);
191 masterPlaylistLoader.pause();
192 mainSegmentLoader.resetEverything();
193 mpc.fastQualityChange_(newPlaylist);
194
195 return;
196 }
197
198 if (type === 'AUDIO') {
199 if (!activeGroup.playlistLoader) {
200 // when switching from demuxed audio/video to muxed audio/video (noted by no
201 // playlist loader for the audio group), we want to do a destructive reset of the
202 // main segment loader and not restart the audio loaders
203 mainSegmentLoader.setAudio(true);
204 // don't have to worry about disabling the audio of the audio segment loader since
205 // it should be stopped
206 mainSegmentLoader.resetEverything();
207 return;
208 }
209
210 // although the segment loader is an audio segment loader, call the setAudio
211 // function to ensure it is prepared to re-append the init segment (or handle other
212 // config changes)
213 segmentLoader.setAudio(true);
214 mainSegmentLoader.setAudio(false);
215 }
216
217 if (previousActiveLoader === activeGroup.playlistLoader) {
218 // Nothing has actually changed. This can happen because track change events can fire
219 // multiple times for a "single" change. One for enabling the new active track, and
220 // one for disabling the track that was active
221 startLoaders(activeGroup.playlistLoader, mediaType);
222 return;
223 }
224
225 if (segmentLoader.track) {
226 // For WebVTT, set the new text track in the segmentloader
227 segmentLoader.track(activeTrack);
228 }
229
230 // destructive reset
231 segmentLoader.resetEverything();
232
233 startLoaders(activeGroup.playlistLoader, mediaType);
234};
235
236export const onError = {
237 /**
238 * Returns a function to be called when a SegmentLoader or PlaylistLoader encounters
239 * an error.
240 *
241 * @param {string} type
242 * MediaGroup type
243 * @param {Object} settings
244 * Object containing required information for media groups
245 * @return {Function}
246 * Error handler. Logs warning (or error if the playlist is blacklisted) to
247 * console and switches back to default audio track.
248 * @function onError.AUDIO
249 */
250 AUDIO: (type, settings) => () => {
251 const {
252 segmentLoaders: { [type]: segmentLoader},
253 mediaTypes: { [type]: mediaType },
254 blacklistCurrentPlaylist
255 } = settings;
256
257 stopLoaders(segmentLoader, mediaType);
258
259 // switch back to default audio track
260 const activeTrack = mediaType.activeTrack();
261 const activeGroup = mediaType.activeGroup();
262 const id = (activeGroup.filter(group => group.default)[0] || activeGroup[0]).id;
263 const defaultTrack = mediaType.tracks[id];
264
265 if (activeTrack === defaultTrack) {
266 // Default track encountered an error. All we can do now is blacklist the current
267 // rendition and hope another will switch audio groups
268 blacklistCurrentPlaylist({
269 message: 'Problem encountered loading the default audio track.'
270 });
271 return;
272 }
273
274 videojs.log.warn('Problem encountered loading the alternate audio track.' +
275 'Switching back to default.');
276
277 for (const trackId in mediaType.tracks) {
278 mediaType.tracks[trackId].enabled = mediaType.tracks[trackId] === defaultTrack;
279 }
280
281 mediaType.onTrackChanged();
282 },
283 /**
284 * Returns a function to be called when a SegmentLoader or PlaylistLoader encounters
285 * an error.
286 *
287 * @param {string} type
288 * MediaGroup type
289 * @param {Object} settings
290 * Object containing required information for media groups
291 * @return {Function}
292 * Error handler. Logs warning to console and disables the active subtitle track
293 * @function onError.SUBTITLES
294 */
295 SUBTITLES: (type, settings) => () => {
296 const {
297 segmentLoaders: { [type]: segmentLoader},
298 mediaTypes: { [type]: mediaType }
299 } = settings;
300
301 videojs.log.warn('Problem encountered loading the subtitle track.' +
302 'Disabling subtitle track.');
303
304 stopLoaders(segmentLoader, mediaType);
305
306 const track = mediaType.activeTrack();
307
308 if (track) {
309 track.mode = 'disabled';
310 }
311
312 mediaType.onTrackChanged();
313 }
314};
315
316export const setupListeners = {
317 /**
318 * Setup event listeners for audio playlist loader
319 *
320 * @param {string} type
321 * MediaGroup type
322 * @param {PlaylistLoader|null} playlistLoader
323 * PlaylistLoader to register listeners on
324 * @param {Object} settings
325 * Object containing required information for media groups
326 * @function setupListeners.AUDIO
327 */
328 AUDIO: (type, playlistLoader, settings) => {
329 if (!playlistLoader) {
330 // no playlist loader means audio will be muxed with the video
331 return;
332 }
333
334 const {
335 tech,
336 requestOptions,
337 segmentLoaders: { [type]: segmentLoader }
338 } = settings;
339
340 playlistLoader.on('loadedmetadata', () => {
341 const media = playlistLoader.media();
342
343 segmentLoader.playlist(media, requestOptions);
344
345 // if the video is already playing, or if this isn't a live video and preload
346 // permits, start downloading segments
347 if (!tech.paused() || (media.endList && tech.preload() !== 'none')) {
348 segmentLoader.load();
349 }
350 });
351
352 playlistLoader.on('loadedplaylist', () => {
353 segmentLoader.playlist(playlistLoader.media(), requestOptions);
354
355 // If the player isn't paused, ensure that the segment loader is running
356 if (!tech.paused()) {
357 segmentLoader.load();
358 }
359 });
360
361 playlistLoader.on('error', onError[type](type, settings));
362 },
363 /**
364 * Setup event listeners for subtitle playlist loader
365 *
366 * @param {string} type
367 * MediaGroup type
368 * @param {PlaylistLoader|null} playlistLoader
369 * PlaylistLoader to register listeners on
370 * @param {Object} settings
371 * Object containing required information for media groups
372 * @function setupListeners.SUBTITLES
373 */
374 SUBTITLES: (type, playlistLoader, settings) => {
375 const {
376 tech,
377 requestOptions,
378 segmentLoaders: { [type]: segmentLoader },
379 mediaTypes: { [type]: mediaType }
380 } = settings;
381
382 playlistLoader.on('loadedmetadata', () => {
383 const media = playlistLoader.media();
384
385 segmentLoader.playlist(media, requestOptions);
386 segmentLoader.track(mediaType.activeTrack());
387
388 // if the video is already playing, or if this isn't a live video and preload
389 // permits, start downloading segments
390 if (!tech.paused() || (media.endList && tech.preload() !== 'none')) {
391 segmentLoader.load();
392 }
393 });
394
395 playlistLoader.on('loadedplaylist', () => {
396 segmentLoader.playlist(playlistLoader.media(), requestOptions);
397
398 // If the player isn't paused, ensure that the segment loader is running
399 if (!tech.paused()) {
400 segmentLoader.load();
401 }
402 });
403
404 playlistLoader.on('error', onError[type](type, settings));
405 }
406};
407
408export const initialize = {
409 /**
410 * Setup PlaylistLoaders and AudioTracks for the audio groups
411 *
412 * @param {string} type
413 * MediaGroup type
414 * @param {Object} settings
415 * Object containing required information for media groups
416 * @function initialize.AUDIO
417 */
418 'AUDIO': (type, settings) => {
419 const {
420 vhs,
421 sourceType,
422 segmentLoaders: { [type]: segmentLoader },
423 requestOptions,
424 master: {mediaGroups},
425 mediaTypes: {
426 [type]: {
427 groups,
428 tracks,
429 logger_
430 }
431 },
432 masterPlaylistLoader
433 } = settings;
434
435 const audioOnlyMaster = isAudioOnly(masterPlaylistLoader.master);
436
437 // force a default if we have none
438 if (!mediaGroups[type] ||
439 Object.keys(mediaGroups[type]).length === 0) {
440 mediaGroups[type] = { main: { default: { default: true } } };
441 if (audioOnlyMaster) {
442 mediaGroups[type].main.default.playlists = masterPlaylistLoader.master.playlists;
443 }
444 }
445
446 for (const groupId in mediaGroups[type]) {
447 if (!groups[groupId]) {
448 groups[groupId] = [];
449 }
450 for (const variantLabel in mediaGroups[type][groupId]) {
451 let properties = mediaGroups[type][groupId][variantLabel];
452
453 let playlistLoader;
454
455 if (audioOnlyMaster) {
456 logger_(`AUDIO group '${groupId}' label '${variantLabel}' is a master playlist`);
457 properties.isMasterPlaylist = true;
458 playlistLoader = null;
459
460 // if vhs-json was provided as the source, and the media playlist was resolved,
461 // use the resolved media playlist object
462 } else if (sourceType === 'vhs-json' && properties.playlists) {
463 playlistLoader = new PlaylistLoader(
464 properties.playlists[0],
465 vhs,
466 requestOptions
467 );
468 } else if (properties.resolvedUri) {
469 playlistLoader = new PlaylistLoader(
470 properties.resolvedUri,
471 vhs,
472 requestOptions
473 );
474 // TODO: dash isn't the only type with properties.playlists
475 // should we even have properties.playlists in this check.
476 } else if (properties.playlists && sourceType === 'dash') {
477 playlistLoader = new DashPlaylistLoader(
478 properties.playlists[0],
479 vhs,
480 requestOptions,
481 masterPlaylistLoader
482 );
483 } else {
484 // no resolvedUri means the audio is muxed with the video when using this
485 // audio track
486 playlistLoader = null;
487 }
488
489 properties = videojs.mergeOptions(
490 { id: variantLabel, playlistLoader },
491 properties
492 );
493
494 setupListeners[type](type, properties.playlistLoader, settings);
495
496 groups[groupId].push(properties);
497
498 if (typeof tracks[variantLabel] === 'undefined') {
499 const track = new videojs.AudioTrack({
500 id: variantLabel,
501 kind: audioTrackKind_(properties),
502 enabled: false,
503 language: properties.language,
504 default: properties.default,
505 label: variantLabel
506 });
507
508 tracks[variantLabel] = track;
509 }
510 }
511 }
512
513 // setup single error event handler for the segment loader
514 segmentLoader.on('error', onError[type](type, settings));
515 },
516 /**
517 * Setup PlaylistLoaders and TextTracks for the subtitle groups
518 *
519 * @param {string} type
520 * MediaGroup type
521 * @param {Object} settings
522 * Object containing required information for media groups
523 * @function initialize.SUBTITLES
524 */
525 'SUBTITLES': (type, settings) => {
526 const {
527 tech,
528 vhs,
529 sourceType,
530 segmentLoaders: { [type]: segmentLoader },
531 requestOptions,
532 master: { mediaGroups },
533 mediaTypes: {
534 [type]: {
535 groups,
536 tracks
537 }
538 },
539 masterPlaylistLoader
540 } = settings;
541
542 for (const groupId in mediaGroups[type]) {
543 if (!groups[groupId]) {
544 groups[groupId] = [];
545 }
546
547 for (const variantLabel in mediaGroups[type][groupId]) {
548 if (mediaGroups[type][groupId][variantLabel].forced) {
549 // Subtitle playlists with the forced attribute are not selectable in Safari.
550 // According to Apple's HLS Authoring Specification:
551 // If content has forced subtitles and regular subtitles in a given language,
552 // the regular subtitles track in that language MUST contain both the forced
553 // subtitles and the regular subtitles for that language.
554 // Because of this requirement and that Safari does not add forced subtitles,
555 // forced subtitles are skipped here to maintain consistent experience across
556 // all platforms
557 continue;
558 }
559
560 let properties = mediaGroups[type][groupId][variantLabel];
561
562 let playlistLoader;
563
564 if (sourceType === 'hls') {
565 playlistLoader =
566 new PlaylistLoader(properties.resolvedUri, vhs, requestOptions);
567 } else if (sourceType === 'dash') {
568 const playlists = properties.playlists.filter((p) => p.excludeUntil !== Infinity);
569
570 if (!playlists.length) {
571 return;
572 }
573 playlistLoader = new DashPlaylistLoader(
574 properties.playlists[0],
575 vhs,
576 requestOptions,
577 masterPlaylistLoader
578 );
579 } else if (sourceType === 'vhs-json') {
580 playlistLoader = new PlaylistLoader(
581 // if the vhs-json object included the media playlist, use the media playlist
582 // as provided, otherwise use the resolved URI to load the playlist
583 properties.playlists ? properties.playlists[0] : properties.resolvedUri,
584 vhs,
585 requestOptions
586 );
587 }
588
589 properties = videojs.mergeOptions({
590 id: variantLabel,
591 playlistLoader
592 }, properties);
593
594 setupListeners[type](type, properties.playlistLoader, settings);
595
596 groups[groupId].push(properties);
597
598 if (typeof tracks[variantLabel] === 'undefined') {
599 const track = tech.addRemoteTextTrack({
600 id: variantLabel,
601 kind: 'subtitles',
602 default: properties.default && properties.autoselect,
603 language: properties.language,
604 label: variantLabel
605 }, false).track;
606
607 tracks[variantLabel] = track;
608 }
609 }
610 }
611
612 // setup single error event handler for the segment loader
613 segmentLoader.on('error', onError[type](type, settings));
614 },
615 /**
616 * Setup TextTracks for the closed-caption groups
617 *
618 * @param {String} type
619 * MediaGroup type
620 * @param {Object} settings
621 * Object containing required information for media groups
622 * @function initialize['CLOSED-CAPTIONS']
623 */
624 'CLOSED-CAPTIONS': (type, settings) => {
625 const {
626 tech,
627 master: { mediaGroups },
628 mediaTypes: {
629 [type]: {
630 groups,
631 tracks
632 }
633 }
634 } = settings;
635
636 for (const groupId in mediaGroups[type]) {
637 if (!groups[groupId]) {
638 groups[groupId] = [];
639 }
640
641 for (const variantLabel in mediaGroups[type][groupId]) {
642 const properties = mediaGroups[type][groupId][variantLabel];
643
644 // Look for either 608 (CCn) or 708 (SERVICEn) caption services
645 if (!/^(?:CC|SERVICE)/.test(properties.instreamId)) {
646 continue;
647 }
648
649 const captionServices = tech.options_.vhs && tech.options_.vhs.captionServices || {};
650
651 let newProps = {
652 label: variantLabel,
653 language: properties.language,
654 instreamId: properties.instreamId,
655 default: properties.default && properties.autoselect
656 };
657
658 if (captionServices[newProps.instreamId]) {
659 newProps = videojs.mergeOptions(newProps, captionServices[newProps.instreamId]);
660 }
661
662 if (newProps.default === undefined) {
663 delete newProps.default;
664 }
665
666 // No PlaylistLoader is required for Closed-Captions because the captions are
667 // embedded within the video stream
668 groups[groupId].push(videojs.mergeOptions({ id: variantLabel }, properties));
669
670 if (typeof tracks[variantLabel] === 'undefined') {
671 const track = tech.addRemoteTextTrack({
672 id: newProps.instreamId,
673 kind: 'captions',
674 default: newProps.default,
675 language: newProps.language,
676 label: newProps.label
677 }, false).track;
678
679 tracks[variantLabel] = track;
680 }
681 }
682 }
683 }
684};
685
686const groupMatch = (list, media) => {
687 for (let i = 0; i < list.length; i++) {
688 if (playlistMatch(media, list[i])) {
689 return true;
690 }
691
692 if (list[i].playlists && groupMatch(list[i].playlists, media)) {
693 return true;
694 }
695 }
696
697 return false;
698};
699
700/**
701 * Returns a function used to get the active group of the provided type
702 *
703 * @param {string} type
704 * MediaGroup type
705 * @param {Object} settings
706 * Object containing required information for media groups
707 * @return {Function}
708 * Function that returns the active media group for the provided type. Takes an
709 * optional parameter {TextTrack} track. If no track is provided, a list of all
710 * variants in the group, otherwise the variant corresponding to the provided
711 * track is returned.
712 * @function activeGroup
713 */
714export const activeGroup = (type, settings) => (track) => {
715 const {
716 masterPlaylistLoader,
717 mediaTypes: { [type]: { groups } }
718 } = settings;
719
720 const media = masterPlaylistLoader.media();
721
722 if (!media) {
723 return null;
724 }
725
726 let variants = null;
727
728 // set to variants to main media active group
729 if (media.attributes[type]) {
730 variants = groups[media.attributes[type]];
731 }
732
733 const groupKeys = Object.keys(groups);
734
735 if (!variants) {
736 // find the masterPlaylistLoader media
737 // that is in a media group if we are dealing
738 // with audio only
739 if (type === 'AUDIO' && groupKeys.length > 1 && isAudioOnly(settings.master)) {
740 for (let i = 0; i < groupKeys.length; i++) {
741 const groupPropertyList = groups[groupKeys[i]];
742
743 if (groupMatch(groupPropertyList, media)) {
744 variants = groupPropertyList;
745 break;
746 }
747 }
748 // use the main group if it exists
749 } else if (groups.main) {
750 variants = groups.main;
751 // only one group, use that one
752 } else if (groupKeys.length === 1) {
753 variants = groups[groupKeys[0]];
754 }
755 }
756
757 if (typeof track === 'undefined') {
758 return variants;
759 }
760
761 if (track === null || !variants) {
762 // An active track was specified so a corresponding group is expected. track === null
763 // means no track is currently active so there is no corresponding group
764 return null;
765 }
766
767 return variants.filter((props) => props.id === track.id)[0] || null;
768};
769
770export const activeTrack = {
771 /**
772 * Returns a function used to get the active track of type provided
773 *
774 * @param {string} type
775 * MediaGroup type
776 * @param {Object} settings
777 * Object containing required information for media groups
778 * @return {Function}
779 * Function that returns the active media track for the provided type. Returns
780 * null if no track is active
781 * @function activeTrack.AUDIO
782 */
783 AUDIO: (type, settings) => () => {
784 const { mediaTypes: { [type]: { tracks } } } = settings;
785
786 for (const id in tracks) {
787 if (tracks[id].enabled) {
788 return tracks[id];
789 }
790 }
791
792 return null;
793 },
794 /**
795 * Returns a function used to get the active track of type provided
796 *
797 * @param {string} type
798 * MediaGroup type
799 * @param {Object} settings
800 * Object containing required information for media groups
801 * @return {Function}
802 * Function that returns the active media track for the provided type. Returns
803 * null if no track is active
804 * @function activeTrack.SUBTITLES
805 */
806 SUBTITLES: (type, settings) => () => {
807 const { mediaTypes: { [type]: { tracks } } } = settings;
808
809 for (const id in tracks) {
810 if (tracks[id].mode === 'showing' || tracks[id].mode === 'hidden') {
811 return tracks[id];
812 }
813 }
814
815 return null;
816 }
817};
818
819export const getActiveGroup = (type, {mediaTypes}) => () => {
820 const activeTrack_ = mediaTypes[type].activeTrack();
821
822 if (!activeTrack_) {
823 return null;
824 }
825
826 return mediaTypes[type].activeGroup(activeTrack_);
827};
828
829/**
830 * Setup PlaylistLoaders and Tracks for media groups (Audio, Subtitles,
831 * Closed-Captions) specified in the master manifest.
832 *
833 * @param {Object} settings
834 * Object containing required information for setting up the media groups
835 * @param {Tech} settings.tech
836 * The tech of the player
837 * @param {Object} settings.requestOptions
838 * XHR request options used by the segment loaders
839 * @param {PlaylistLoader} settings.masterPlaylistLoader
840 * PlaylistLoader for the master source
841 * @param {VhsHandler} settings.vhs
842 * VHS SourceHandler
843 * @param {Object} settings.master
844 * The parsed master manifest
845 * @param {Object} settings.mediaTypes
846 * Object to store the loaders, tracks, and utility methods for each media type
847 * @param {Function} settings.blacklistCurrentPlaylist
848 * Blacklists the current rendition and forces a rendition switch.
849 * @function setupMediaGroups
850 */
851export const setupMediaGroups = (settings) => {
852 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach((type) => {
853 initialize[type](type, settings);
854 });
855
856 const {
857 mediaTypes,
858 masterPlaylistLoader,
859 tech,
860 vhs,
861 segmentLoaders: {
862 ['AUDIO']: audioSegmentLoader,
863 main: mainSegmentLoader
864 }
865 } = settings;
866
867 // setup active group and track getters and change event handlers
868 ['AUDIO', 'SUBTITLES'].forEach((type) => {
869 mediaTypes[type].activeGroup = activeGroup(type, settings);
870 mediaTypes[type].activeTrack = activeTrack[type](type, settings);
871 mediaTypes[type].onGroupChanged = onGroupChanged(type, settings);
872 mediaTypes[type].onGroupChanging = onGroupChanging(type, settings);
873 mediaTypes[type].onTrackChanged = onTrackChanged(type, settings);
874 mediaTypes[type].getActiveGroup = getActiveGroup(type, settings);
875 });
876
877 // DO NOT enable the default subtitle or caption track.
878 // DO enable the default audio track
879 const audioGroup = mediaTypes.AUDIO.activeGroup();
880
881 if (audioGroup) {
882 const groupId = (audioGroup.filter(group => group.default)[0] || audioGroup[0]).id;
883
884 mediaTypes.AUDIO.tracks[groupId].enabled = true;
885 mediaTypes.AUDIO.onGroupChanged();
886 mediaTypes.AUDIO.onTrackChanged();
887
888 const activeAudioGroup = mediaTypes.AUDIO.getActiveGroup();
889
890 // a similar check for handling setAudio on each loader is run again each time the
891 // track is changed, but needs to be handled here since the track may not be considered
892 // changed on the first call to onTrackChanged
893 if (!activeAudioGroup.playlistLoader) {
894 // either audio is muxed with video or the stream is audio only
895 mainSegmentLoader.setAudio(true);
896 } else {
897 // audio is demuxed
898 mainSegmentLoader.setAudio(false);
899 audioSegmentLoader.setAudio(true);
900 }
901 }
902
903 masterPlaylistLoader.on('mediachange', () => {
904 ['AUDIO', 'SUBTITLES'].forEach(type => mediaTypes[type].onGroupChanged());
905 });
906
907 masterPlaylistLoader.on('mediachanging', () => {
908 ['AUDIO', 'SUBTITLES'].forEach(type => mediaTypes[type].onGroupChanging());
909 });
910
911 // custom audio track change event handler for usage event
912 const onAudioTrackChanged = () => {
913 mediaTypes.AUDIO.onTrackChanged();
914 tech.trigger({ type: 'usage', name: 'vhs-audio-change' });
915 tech.trigger({ type: 'usage', name: 'hls-audio-change' });
916 };
917
918 tech.audioTracks().addEventListener('change', onAudioTrackChanged);
919 tech.remoteTextTracks().addEventListener(
920 'change',
921 mediaTypes.SUBTITLES.onTrackChanged
922 );
923
924 vhs.on('dispose', () => {
925 tech.audioTracks().removeEventListener('change', onAudioTrackChanged);
926 tech.remoteTextTracks().removeEventListener(
927 'change',
928 mediaTypes.SUBTITLES.onTrackChanged
929 );
930 });
931
932 // clear existing audio tracks and add the ones we just created
933 tech.clearTracks('audio');
934
935 for (const id in mediaTypes.AUDIO.tracks) {
936 tech.audioTracks().addTrack(mediaTypes.AUDIO.tracks[id]);
937 }
938};
939
940/**
941 * Creates skeleton object used to store the loaders, tracks, and utility methods for each
942 * media type
943 *
944 * @return {Object}
945 * Object to store the loaders, tracks, and utility methods for each media type
946 * @function createMediaTypes
947 */
948export const createMediaTypes = () => {
949 const mediaTypes = {};
950
951 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach((type) => {
952 mediaTypes[type] = {
953 groups: {},
954 tracks: {},
955 activePlaylistLoader: null,
956 activeGroup: noop,
957 activeTrack: noop,
958 getActiveGroup: noop,
959 onGroupChanged: noop,
960 onTrackChanged: noop,
961 lastTrack_: null,
962 logger_: logger(`MediaGroups[${type}]`)
963 };
964 });
965
966 return mediaTypes;
967};