UNPKG

videojs-contrib-hls

Version:

Play back HLS with video.js, even where it's not natively supported

1,039 lines (894 loc) 31.9 kB
/** * @file master-playlist-controller.js */ import PlaylistLoader from './playlist-loader'; import SegmentLoader from './segment-loader'; import Ranges from './ranges'; import videojs from 'video.js'; import AdCueTags from './ad-cue-tags'; // 5 minute blacklist const BLACKLIST_DURATION = 5 * 60 * 1000; let Hls; /** * determine if an object a is differnt from * and object b. both only having one dimensional * properties * * @param {Object} a object one * @param {Object} b object two * @return {Boolean} if the object has changed or not */ const objectChanged = function(a, b) { if (typeof a !== typeof b) { return true; } // if we have a different number of elements // something has changed if (Object.keys(a).length !== Object.keys(b).length) { return true; } for (let prop in a) { if (a[prop] !== b[prop]) { return true; } } return false; }; /** * Parses a codec string to retrieve the number of codecs specified, * the video codec and object type indicator, and the audio profile. * * @private */ const parseCodecs = function(codecs) { let result = { codecCount: 0, videoCodec: null, videoObjectTypeIndicator: null, audioProfile: null }; let parsed; result.codecCount = codecs.split(',').length; result.codecCount = result.codecCount || 2; // parse the video codec parsed = (/(^|\s|,)+(avc1)([^ ,]*)/i).exec(codecs); if (parsed) { result.videoCodec = parsed[2]; result.videoObjectTypeIndicator = parsed[3]; } // parse the last field of the audio codec result.audioProfile = (/(^|\s|,)+mp4a.[0-9A-Fa-f]+\.([0-9A-Fa-f]+)/i).exec(codecs); result.audioProfile = result.audioProfile && result.audioProfile[2]; return result; }; /** * Calculates the MIME type strings for a working configuration of * SourceBuffers to play variant streams in a master playlist. If * there is no possible working configuration, an empty array will be * returned. * * @param master {Object} the m3u8 object for the master playlist * @param media {Object} the m3u8 object for the variant playlist * @return {Array} the MIME type strings. If the array has more than * one entry, the first element should be applied to the video * SourceBuffer and the second to the audio SourceBuffer. * * @private */ export const mimeTypesForPlaylist_ = function(master, media) { let container = 'mp2t'; let codecs = { videoCodec: 'avc1', videoObjectTypeIndicator: '.4d400d', audioProfile: '2' }; let audioGroup = []; let mediaAttributes; let previousGroup = null; if (!media) { // not enough information, return an error return []; } // An initialization segment means the media playlists is an iframe // playlist or is using the mp4 container. We don't currently // support iframe playlists, so assume this is signalling mp4 // fragments. // the existence check for segments can be removed once // https://github.com/videojs/m3u8-parser/issues/8 is closed if (media.segments && media.segments.length && media.segments[0].map) { container = 'mp4'; } // if the codecs were explicitly specified, use them instead of the // defaults mediaAttributes = media.attributes || {}; if (mediaAttributes.CODECS) { let parsedCodecs = parseCodecs(mediaAttributes.CODECS); Object.keys(parsedCodecs).forEach((key) => { codecs[key] = parsedCodecs[key] || codecs[key]; }); } if (master.mediaGroups.AUDIO) { audioGroup = master.mediaGroups.AUDIO[mediaAttributes.AUDIO]; } // if audio could be muxed or unmuxed, use mime types appropriate // for both scenarios for (let groupId in audioGroup) { if (previousGroup && (!!audioGroup[groupId].uri !== !!previousGroup.uri)) { // one source buffer with muxed video and audio and another for // the alternate audio return [ 'video/' + container + '; codecs="' + codecs.videoCodec + codecs.videoObjectTypeIndicator + ', mp4a.40.' + codecs.audioProfile + '"', 'audio/' + container + '; codecs="mp4a.40.' + codecs.audioProfile + '"' ]; } previousGroup = audioGroup[groupId]; } // if all video and audio is unmuxed, use two single-codec mime // types if (previousGroup && previousGroup.uri) { return [ 'video/' + container + '; codecs="' + codecs.videoCodec + codecs.videoObjectTypeIndicator + '"', 'audio/' + container + '; codecs="mp4a.40.' + codecs.audioProfile + '"' ]; } // all video and audio are muxed, use a dual-codec mime type return [ 'video/' + container + '; codecs="' + codecs.videoCodec + codecs.videoObjectTypeIndicator + ', mp4a.40.' + codecs.audioProfile + '"' ]; }; /** * the master playlist controller controller all interactons * between playlists and segmentloaders. At this time this mainly * involves a master playlist and a series of audio playlists * if they are available * * @class MasterPlaylistController * @extends videojs.EventTarget */ export class MasterPlaylistController extends videojs.EventTarget { constructor(options) { super(); let { url, withCredentials, mode, tech, bandwidth, externHls, useCueTags } = options; if (!url) { throw new Error('A non-empty playlist URL is required'); } Hls = externHls; this.withCredentials = withCredentials; this.tech_ = tech; this.hls_ = tech.hls; this.mode_ = mode; this.useCueTags_ = useCueTags; if (this.useCueTags_) { this.cueTagsTrack_ = this.tech_.addTextTrack('metadata', 'ad-cues'); this.cueTagsTrack_.inBandMetadataTrackDispatchType = ''; this.tech_.textTracks().addTrack_(this.cueTagsTrack_); } this.audioTracks_ = []; this.requestOptions_ = { withCredentials: this.withCredentials, timeout: null }; this.audioGroups_ = {}; this.mediaSource = new videojs.MediaSource({ mode }); this.audioinfo_ = null; this.mediaSource.on('audioinfo', this.handleAudioinfoUpdate_.bind(this)); // load the media source into the player this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen_.bind(this)); let segmentLoaderOptions = { hls: this.hls_, mediaSource: this.mediaSource, currentTime: this.tech_.currentTime.bind(this.tech_), seekable: () => this.seekable(), seeking: () => this.tech_.seeking(), setCurrentTime: (a) => this.tech_.setCurrentTime(a), hasPlayed: () => this.tech_.played().length !== 0, bandwidth }; // setup playlist loaders this.masterPlaylistLoader_ = new PlaylistLoader(url, this.hls_, this.withCredentials); this.setupMasterPlaylistLoaderListeners_(); this.audioPlaylistLoader_ = null; // setup segment loaders // combined audio/video or just video when alternate audio track is selected this.mainSegmentLoader_ = new SegmentLoader(segmentLoaderOptions); // alternate audio track this.audioSegmentLoader_ = new SegmentLoader(segmentLoaderOptions); this.setupSegmentLoaderListeners_(); this.masterPlaylistLoader_.start(); } /** * Register event handlers on the master playlist loader. A helper * function for construction time. * * @private */ setupMasterPlaylistLoaderListeners_() { this.masterPlaylistLoader_.on('loadedmetadata', () => { let media = this.masterPlaylistLoader_.media(); let requestTimeout = (this.masterPlaylistLoader_.targetDuration * 1.5) * 1000; this.requestOptions_.timeout = requestTimeout; // if this isn't a live video and preload permits, start // downloading segments if (media.endList && this.tech_.preload() !== 'none') { this.mainSegmentLoader_.playlist(media, this.requestOptions_); this.mainSegmentLoader_.expired(this.masterPlaylistLoader_.expired_); this.mainSegmentLoader_.load(); } try { this.setupSourceBuffers_(); } catch (e) { videojs.log.warn('Failed to create SourceBuffers', e); return this.mediaSource.endOfStream('decode'); } this.setupFirstPlay(); this.fillAudioTracks_(); this.setupAudio(); this.trigger('audioupdate'); this.trigger('selectedinitialmedia'); }); this.masterPlaylistLoader_.on('loadedplaylist', () => { let updatedPlaylist = this.masterPlaylistLoader_.media(); let seekable; if (!updatedPlaylist) { // select the initial variant this.initialMedia_ = this.selectPlaylist(); this.masterPlaylistLoader_.media(this.initialMedia_); return; } if (this.useCueTags_) { this.updateAdCues_(updatedPlaylist, this.masterPlaylistLoader_.expired_); } // TODO: Create a new event on the PlaylistLoader that signals // that the segments have changed in some way and use that to // update the SegmentLoader instead of doing it twice here and // on `mediachange` this.mainSegmentLoader_.playlist(updatedPlaylist, this.requestOptions_); this.mainSegmentLoader_.expired(this.masterPlaylistLoader_.expired_); this.updateDuration(); // update seekable seekable = this.seekable(); if (!updatedPlaylist.endList && seekable.length !== 0) { this.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0)); } }); this.masterPlaylistLoader_.on('error', () => { this.blacklistCurrentPlaylist(this.masterPlaylistLoader_.error); }); this.masterPlaylistLoader_.on('mediachanging', () => { this.mainSegmentLoader_.abort(); this.mainSegmentLoader_.pause(); }); this.masterPlaylistLoader_.on('mediachange', () => { let media = this.masterPlaylistLoader_.media(); let requestTimeout = (this.masterPlaylistLoader_.targetDuration * 1.5) * 1000; let activeAudioGroup; let activeTrack; // If we don't have any more available playlists, we don't want to // timeout the request. if (this.masterPlaylistLoader_.isLowestEnabledRendition_()) { this.requestOptions_.timeout = 0; } else { this.requestOptions_.timeout = requestTimeout; } // TODO: Create a new event on the PlaylistLoader that signals // that the segments have changed in some way and use that to // update the SegmentLoader instead of doing it twice here and // on `loadedplaylist` this.mainSegmentLoader_.playlist(media, this.requestOptions_); this.mainSegmentLoader_.expired(this.masterPlaylistLoader_.expired_); this.mainSegmentLoader_.load(); // if the audio group has changed, a new audio track has to be // enabled activeAudioGroup = this.activeAudioGroup(); activeTrack = activeAudioGroup.filter((track) => track.enabled)[0]; if (!activeTrack) { this.setupAudio(); this.trigger('audioupdate'); } this.tech_.trigger({ type: 'mediachange', bubbles: true }); }); } /** * Register event handlers on the segment loaders. A helper function * for construction time. * * @private */ setupSegmentLoaderListeners_() { this.mainSegmentLoader_.on('progress', () => { // figure out what stream the next segment should be downloaded from // with the updated bandwidth information this.masterPlaylistLoader_.media(this.selectPlaylist()); this.trigger('progress'); }); this.mainSegmentLoader_.on('error', () => { this.blacklistCurrentPlaylist(this.mainSegmentLoader_.error()); }); this.audioSegmentLoader_.on('error', () => { videojs.log.warn('Problem encountered with the current alternate audio track' + '. Switching back to default.'); this.audioSegmentLoader_.abort(); this.audioPlaylistLoader_ = null; this.setupAudio(); }); } handleAudioinfoUpdate_(event) { if (Hls.supportsAudioInfoChange_() || !this.audioInfo_ || !objectChanged(this.audioInfo_, event.info)) { this.audioInfo_ = event.info; return; } let error = 'had different audio properties (channels, sample rate, etc.) ' + 'or changed in some other way. This behavior is currently ' + 'unsupported in Firefox 48 and below due to an issue: \n\n' + 'https://bugzilla.mozilla.org/show_bug.cgi?id=1247138\n\n'; let enabledIndex = this.activeAudioGroup() .map((track) => track.enabled) .indexOf(true); let enabledTrack = this.activeAudioGroup()[enabledIndex]; let defaultTrack = this.activeAudioGroup().filter((track) => { return track.properties_ && track.properties_.default; })[0]; // they did not switch audiotracks // blacklist the current playlist if (!this.audioPlaylistLoader_) { error = `The rendition that we tried to switch to ${error}` + 'Unfortunately that means we will have to blacklist ' + 'the current playlist and switch to another. Sorry!'; this.blacklistCurrentPlaylist(); } else { error = `The audio track '${enabledTrack.label}' that we tried to ` + `switch to ${error} Unfortunately this means we will have to ` + `return you to the main track '${defaultTrack.label}'. Sorry!`; defaultTrack.enabled = true; this.activeAudioGroup().splice(enabledIndex, 1); this.trigger('audioupdate'); } videojs.log.warn(error); this.setupAudio(); } /** * get the total number of media requests from the `audiosegmentloader_` * and the `mainSegmentLoader_` * * @private */ mediaRequests_() { return this.audioSegmentLoader_.mediaRequests + this.mainSegmentLoader_.mediaRequests; } /** * get the total time that media requests have spent trnasfering * from the `audiosegmentloader_` and the `mainSegmentLoader_` * * @private */ mediaTransferDuration_() { return this.audioSegmentLoader_.mediaTransferDuration + this.mainSegmentLoader_.mediaTransferDuration; } /** * get the total number of bytes transfered during media requests * from the `audiosegmentloader_` and the `mainSegmentLoader_` * * @private */ mediaBytesTransferred_() { return this.audioSegmentLoader_.mediaBytesTransferred + this.mainSegmentLoader_.mediaBytesTransferred; } /** * fill our internal list of HlsAudioTracks with data from * the master playlist or use a default * * @private */ fillAudioTracks_() { let master = this.master(); let mediaGroups = master.mediaGroups || {}; // force a default if we have none or we are not // in html5 mode (the only mode to support more than one // audio track) if (!mediaGroups || !mediaGroups.AUDIO || Object.keys(mediaGroups.AUDIO).length === 0 || this.mode_ !== 'html5') { // "main" audio group, track name "default" mediaGroups.AUDIO = { main: { default: { default: true }}}; } for (let mediaGroup in mediaGroups.AUDIO) { if (!this.audioGroups_[mediaGroup]) { this.audioGroups_[mediaGroup] = []; } for (let label in mediaGroups.AUDIO[mediaGroup]) { let properties = mediaGroups.AUDIO[mediaGroup][label]; let track = new videojs.AudioTrack({ id: label, kind: properties.default ? 'main' : 'alternative', enabled: false, language: properties.language, label }); track.properties_ = properties; this.audioGroups_[mediaGroup].push(track); } } // enable the default active track (this.activeAudioGroup().filter((audioTrack) => { return audioTrack.properties_.default; })[0] || this.activeAudioGroup()[0]).enabled = true; } /** * Call load on our SegmentLoaders */ load() { this.mainSegmentLoader_.load(); if (this.audioPlaylistLoader_) { this.audioSegmentLoader_.load(); } } /** * Returns the audio group for the currently active primary * media playlist. */ activeAudioGroup() { let videoPlaylist = this.masterPlaylistLoader_.media(); let result; if (videoPlaylist.attributes && videoPlaylist.attributes.AUDIO) { result = this.audioGroups_[videoPlaylist.attributes.AUDIO]; } return result || this.audioGroups_.main; } /** * Determine the correct audio rendition based on the active * AudioTrack and initialize a PlaylistLoader and SegmentLoader if * necessary. This method is called once automatically before * playback begins to enable the default audio track and should be * invoked again if the track is changed. */ setupAudio() { // determine whether seperate loaders are required for the audio // rendition let audioGroup = this.activeAudioGroup(); let track = audioGroup.filter((audioTrack) => { return audioTrack.enabled; })[0]; if (!track) { track = audioGroup.filter((audioTrack) => { return audioTrack.properties_.default; })[0] || audioGroup[0]; track.enabled = true; } // stop playlist and segment loading for audio if (this.audioPlaylistLoader_) { this.audioPlaylistLoader_.dispose(); this.audioPlaylistLoader_ = null; } this.audioSegmentLoader_.pause(); this.audioSegmentLoader_.clearBuffer(); if (!track.properties_.resolvedUri) { return; } // startup playlist and segment loaders for the enabled audio // track this.audioPlaylistLoader_ = new PlaylistLoader(track.properties_.resolvedUri, this.hls_, this.withCredentials); this.audioPlaylistLoader_.start(); this.audioPlaylistLoader_.on('loadedmetadata', () => { let audioPlaylist = this.audioPlaylistLoader_.media(); this.audioSegmentLoader_.playlist(audioPlaylist, this.requestOptions_); // if the video is already playing, or if this isn't a live video and preload // permits, start downloading segments if (!this.tech_.paused() || (audioPlaylist.endList && this.tech_.preload() !== 'none')) { this.audioSegmentLoader_.load(); } if (!audioPlaylist.endList) { // trigger the playlist loader to start "expired time"-tracking this.audioPlaylistLoader_.trigger('firstplay'); } }); this.audioPlaylistLoader_.on('loadedplaylist', () => { let updatedPlaylist; if (this.audioPlaylistLoader_) { updatedPlaylist = this.audioPlaylistLoader_.media(); } if (!updatedPlaylist) { // only one playlist to select this.audioPlaylistLoader_.media( this.audioPlaylistLoader_.playlists.master.playlists[0]); return; } this.audioSegmentLoader_.playlist(updatedPlaylist, this.requestOptions_); }); this.audioPlaylistLoader_.on('error', () => { videojs.log.warn('Problem encountered loading the alternate audio track' + '. Switching back to default.'); this.audioSegmentLoader_.abort(); this.setupAudio(); }); } /** * Re-tune playback quality level for the current player * conditions. This method may perform destructive actions, like * removing already buffered content, to readjust the currently * active playlist quickly. * * @private */ fastQualityChange_() { let media = this.selectPlaylist(); if (media !== this.masterPlaylistLoader_.media()) { this.masterPlaylistLoader_.media(media); this.mainSegmentLoader_.sourceUpdater_.remove(this.tech_.currentTime() + 5, Infinity); } } /** * Begin playback. */ play() { if (this.setupFirstPlay()) { return; } if (this.tech_.ended()) { this.tech_.setCurrentTime(0); } this.load(); // if the viewer has paused and we fell out of the live window, // seek forward to the earliest available position if (this.tech_.duration() === Infinity) { if (this.tech_.currentTime() < this.tech_.seekable().start(0)) { return this.tech_.setCurrentTime(this.tech_.seekable().start(0)); } } } /** * Seek to the latest media position if this is a live video and the * player and video are loaded and initialized. */ setupFirstPlay() { let seekable; let media = this.masterPlaylistLoader_.media(); // check that everything is ready to begin buffering // 1) the active media playlist is available if (media && // 2) the video is a live stream !media.endList && // 3) the player is not paused !this.tech_.paused() && // 4) the player has not started playing !this.hasPlayed_) { // trigger the playlist loader to start "expired time"-tracking this.masterPlaylistLoader_.trigger('firstplay'); this.hasPlayed_ = true; // seek to the latest media position for live videos seekable = this.seekable(); if (seekable.length) { this.tech_.setCurrentTime(seekable.end(0)); } // now that we seeked to the current time, load the segment this.load(); return true; } return false; } /** * handle the sourceopen event on the MediaSource * * @private */ handleSourceOpen_() { // Only attempt to create the source buffer if none already exist. // handleSourceOpen is also called when we are "re-opening" a source buffer // after `endOfStream` has been called (in response to a seek for instance) try { this.setupSourceBuffers_(); } catch (e) { videojs.log.warn('Failed to create Source Buffers', e); return this.mediaSource.endOfStream('decode'); } // if autoplay is enabled, begin playback. This is duplicative of // code in video.js but is required because play() must be invoked // *after* the media source has opened. if (this.tech_.autoplay()) { this.tech_.play(); } this.trigger('sourceopen'); } /** * Blacklists a playlist when an error occurs for a set amount of time * making it unavailable for selection by the rendition selection algorithm * and then forces a new playlist (rendition) selection. * * @param {Object=} error an optional error that may include the playlist * to blacklist */ blacklistCurrentPlaylist(error = {}) { let currentPlaylist; let nextPlaylist; // If the `error` was generated by the playlist loader, it will contain // the playlist we were trying to load (but failed) and that should be // blacklisted instead of the currently selected playlist which is likely // out-of-date in this scenario currentPlaylist = error.playlist || this.masterPlaylistLoader_.media(); // If there is no current playlist, then an error occurred while we were // trying to load the master OR while we were disposing of the tech if (!currentPlaylist) { this.error = error; return this.mediaSource.endOfStream('network'); } // Blacklist this playlist currentPlaylist.excludeUntil = Date.now() + BLACKLIST_DURATION; // Select a new playlist nextPlaylist = this.selectPlaylist(); if (nextPlaylist) { videojs.log.warn('Problem encountered with the current ' + 'HLS playlist. Switching to another playlist.'); return this.masterPlaylistLoader_.media(nextPlaylist); } videojs.log.warn('Problem encountered with the current ' + 'HLS playlist. No suitable alternatives found.'); // We have no more playlists we can select so we must fail this.error = error; return this.mediaSource.endOfStream('network'); } /** * Pause all segment loaders */ pauseLoading() { this.mainSegmentLoader_.pause(); if (this.audioPlaylistLoader_) { this.audioSegmentLoader_.pause(); } } /** * set the current time on all segment loaders * * @param {TimeRange} currentTime the current time to set * @return {TimeRange} the current time */ setCurrentTime(currentTime) { let buffered = Ranges.findRange(this.tech_.buffered(), currentTime); if (!(this.masterPlaylistLoader_ && this.masterPlaylistLoader_.media())) { // return immediately if the metadata is not ready yet return 0; } // it's clearly an edge-case but don't thrown an error if asked to // seek within an empty playlist if (!this.masterPlaylistLoader_.media().segments) { return 0; } // if the seek location is already buffered, continue buffering as // usual if (buffered && buffered.length) { return currentTime; } // cancel outstanding requests so we begin buffering at the new // location this.mainSegmentLoader_.abort(); if (this.audioPlaylistLoader_) { this.audioSegmentLoader_.abort(); } if (!this.tech_.paused()) { this.mainSegmentLoader_.load(); if (this.audioPlaylistLoader_) { this.audioSegmentLoader_.load(); } } } /** * get the current duration * * @return {TimeRange} the duration */ duration() { if (!this.masterPlaylistLoader_) { return 0; } if (this.mediaSource) { return this.mediaSource.duration; } return Hls.Playlist.duration(this.masterPlaylistLoader_.media()); } /** * check the seekable range * * @return {TimeRange} the seekable range */ seekable() { let media; let mainSeekable; let audioSeekable; if (!this.masterPlaylistLoader_) { return videojs.createTimeRanges(); } media = this.masterPlaylistLoader_.media(); if (!media) { return videojs.createTimeRanges(); } mainSeekable = Hls.Playlist.seekable(media, this.masterPlaylistLoader_.expired_); if (mainSeekable.length === 0) { return mainSeekable; } if (this.audioPlaylistLoader_) { audioSeekable = Hls.Playlist.seekable(this.audioPlaylistLoader_.media(), this.audioPlaylistLoader_.expired_); if (audioSeekable.length === 0) { return audioSeekable; } } if (!audioSeekable) { // seekable has been calculated based on buffering video data so it // can be returned directly return mainSeekable; } return videojs.createTimeRanges([[ (audioSeekable.start(0) > mainSeekable.start(0)) ? audioSeekable.start(0) : mainSeekable.start(0), (audioSeekable.end(0) < mainSeekable.end(0)) ? audioSeekable.end(0) : mainSeekable.end(0) ]]); } /** * Update the player duration */ updateDuration() { let oldDuration = this.mediaSource.duration; let newDuration = Hls.Playlist.duration(this.masterPlaylistLoader_.media()); let buffered = this.tech_.buffered(); let setDuration = () => { this.mediaSource.duration = newDuration; this.tech_.trigger('durationchange'); this.mediaSource.removeEventListener('sourceopen', setDuration); }; if (buffered.length > 0) { newDuration = Math.max(newDuration, buffered.end(buffered.length - 1)); } // if the duration has changed, invalidate the cached value if (oldDuration !== newDuration) { // update the duration if (this.mediaSource.readyState !== 'open') { this.mediaSource.addEventListener('sourceopen', setDuration); } else { setDuration(); } } } /** * dispose of the MasterPlaylistController and everything * that it controls */ dispose() { this.masterPlaylistLoader_.dispose(); this.mainSegmentLoader_.dispose(); this.audioSegmentLoader_.dispose(); } /** * return the master playlist object if we have one * * @return {Object} the master playlist object that we parsed */ master() { return this.masterPlaylistLoader_.master; } /** * return the currently selected playlist * * @return {Object} the currently selected playlist object that we parsed */ media() { // playlist loader will not return media if it has not been fully loaded return this.masterPlaylistLoader_.media() || this.initialMedia_; } /** * setup our internal source buffers on our segment Loaders * * @private */ setupSourceBuffers_() { let media = this.masterPlaylistLoader_.media(); let mimeTypes; // wait until a media playlist is available and the Media Source is // attached if (!media || this.mediaSource.readyState !== 'open') { return; } mimeTypes = mimeTypesForPlaylist_(this.masterPlaylistLoader_.master, media); if (mimeTypes.length < 1) { this.error = 'No compatible SourceBuffer configuration for the variant stream:' + media.resolvedUri; return this.mediaSource.endOfStream('decode'); } this.mainSegmentLoader_.mimeType(mimeTypes[0]); if (mimeTypes[1]) { this.audioSegmentLoader_.mimeType(mimeTypes[1]); } // exclude any incompatible variant streams from future playlist // selection this.excludeIncompatibleVariants_(media); } /** * Blacklist playlists that are known to be codec or * stream-incompatible with the SourceBuffer configuration. For * instance, Media Source Extensions would cause the video element to * stall waiting for video data if you switched from a variant with * video and audio to an audio-only one. * * @param {Object} media a media playlist compatible with the current * set of SourceBuffers. Variants in the current master playlist that * do not appear to have compatible codec or stream configurations * will be excluded from the default playlist selection algorithm * indefinitely. * @private */ excludeIncompatibleVariants_(media) { let master = this.masterPlaylistLoader_.master; let codecCount = 2; let videoCodec = null; let audioProfile = null; let codecs; if (media.attributes && media.attributes.CODECS) { codecs = parseCodecs(media.attributes.CODECS); videoCodec = codecs.videoCodec; audioProfile = codecs.audioProfile; codecCount = codecs.codecCount; } master.playlists.forEach(function(variant) { let variantCodecs = { codecCount: 2, videoCodec: null, audioProfile: null }; if (variant.attributes && variant.attributes.CODECS) { let codecString = variant.attributes.CODECS; variantCodecs = parseCodecs(codecString); if (!MediaSource.isTypeSupported('video/mp4; codecs="' + codecString + '"')) { variant.excludeUntil = Infinity; } } // if the streams differ in the presence or absence of audio or // video, they are incompatible if (variantCodecs.codecCount !== codecCount) { variant.excludeUntil = Infinity; } // if h.264 is specified on the current playlist, some flavor of // it must be specified on all compatible variants if (variantCodecs.videoCodec !== videoCodec) { variant.excludeUntil = Infinity; } // HE-AAC ("mp4a.40.5") is incompatible with all other versions of // AAC audio in Chrome 46. Don't mix the two. if ((variantCodecs.audioProfile === '5' && audioProfile !== '5') || (audioProfile === '5' && variantCodecs.audioProfile !== '5')) { variant.excludeUntil = Infinity; } }); } updateAdCues_(media, offset = 0) { AdCueTags.updateAdCues(media, this.cueTagsTrack_, offset); } }