Files
shaka-player/lib/hls/hls_parser.js
T
Aaron Vaage 1b90e0f4c1 Integrate Deprecation Tool With Library
This integrates the deprecation tool with our library and updates
all the deprecated warnings to use our new tool.

As some features needed to be removed in 2.5, this change removes them
since their deprecation checks would make us fail.

Change-Id: I28310bbe9df6839719f4763a96026d5968bdcea6
2018-12-12 00:26:50 +00:00

2274 lines
74 KiB
JavaScript

/**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.hls.HlsParser');
goog.require('goog.Uri');
goog.require('goog.asserts');
goog.require('shaka.Deprecate');
goog.require('shaka.hls.ManifestTextParser');
goog.require('shaka.hls.Playlist');
goog.require('shaka.hls.PlaylistType');
goog.require('shaka.hls.Tag');
goog.require('shaka.hls.Utils');
goog.require('shaka.log');
goog.require('shaka.media.DrmEngine');
goog.require('shaka.media.InitSegmentReference');
goog.require('shaka.media.ManifestParser');
goog.require('shaka.media.PresentationTimeline');
goog.require('shaka.media.SegmentIndex');
goog.require('shaka.media.SegmentReference');
goog.require('shaka.net.DataUriPlugin');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.text.TextEngine');
goog.require('shaka.util.ArrayUtils');
goog.require('shaka.util.DataViewReader');
goog.require('shaka.util.Error');
goog.require('shaka.util.Functional');
goog.require('shaka.util.LanguageUtils');
goog.require('shaka.util.ManifestParserUtils');
goog.require('shaka.util.MimeUtils');
goog.require('shaka.util.Mp4Parser');
goog.require('shaka.util.OperationManager');
/**
* Creates a new HLS parser.
*
* @struct
* @constructor
* @implements {shaka.extern.ManifestParser}
* @export
*/
shaka.hls.HlsParser = function() {
/** @private {?shaka.extern.ManifestParser.PlayerInterface} */
this.playerInterface_ = null;
/** @private {?shaka.extern.ManifestConfiguration} */
this.config_ = null;
/** @private {number} */
this.globalId_ = 1;
/**
* @private {!Map.<number, shaka.hls.HlsParser.StreamInfo>}
*/
// TODO: This is now only used for text codec detection, try to remove.
this.mediaTagsToStreamInfosMap_ = new Map();
/**
* The values are strings of the form "<VIDEO URI> - <AUDIO URI>",
* where the URIs are the verbatim media playlist URIs as they appeared in the
* master playlist.
*
* Used to avoid duplicates that vary only in their text stream.
*
* @private {!Set.<string>}
*/
this.variantUriSet_ = new Set();
/**
* A map from (verbatim) media playlist URI to stream infos representing the
* playlists.
*
* On update, used to iterate through and update from media playlists.
*
* On initial parse, used to iterate through and determine minimum timestamps,
* offsets, and to handle TS rollover.
*
* During parsing, used to avoid duplicates in the async methods
* createStreamInfoFromMediaTag_ and createStreamInfoFromVariantTag_.
*
* During parsing of updates, used by getStartTime_ to determine the start
* time of the first segment from existing segment references.
*
* @private {!Map.<string, shaka.hls.HlsParser.StreamInfo>}
*/
this.uriToStreamInfosMap_ = new Map();
/** @private {?shaka.media.PresentationTimeline} */
this.presentationTimeline_ = null;
/**
* The master playlist URI, after redirects.
*
* @private {string}
*/
this.masterPlaylistUri_ = '';
/** @private {shaka.hls.ManifestTextParser} */
this.manifestTextParser_ = new shaka.hls.ManifestTextParser();
/**
* The update period in seconds, or null for no updates.
* @private {?number}
*/
this.updatePeriod_ = null;
/** @private {?number} */
this.updateTimer_ = null;
/** @private {shaka.hls.HlsParser.PresentationType_} */
this.presentationType_ = shaka.hls.HlsParser.PresentationType_.VOD;
/** @private {?shaka.extern.Manifest} */
this.manifest_ = null;
/** @private {number} */
this.maxTargetDuration_ = 0;
/** @private {number} */
this.minTargetDuration_ = Infinity;
/** @private {!shaka.util.OperationManager} */
this.operationManager_ = new shaka.util.OperationManager();
/** @private {!Array.<!Array.<!shaka.media.SegmentReference>>} */
this.segmentsToNotifyByStream_ = [];
/** A map from closed captions' group id, to a map of closed captions info.
* {group id -> {closed captions channel id -> language}}
* @private {Map.<string, Map.<string, string>>}
*/
this.groupIdToClosedCaptionsMap_ = new Map();
};
/**
* @typedef {{
* stream: !shaka.extern.Stream,
* segmentIndex: !shaka.media.SegmentIndex,
* drmInfos: !Array.<shaka.extern.DrmInfo>,
* verbatimMediaPlaylistUri: string,
* absoluteMediaPlaylistUri: string,
* minTimestamp: number,
* maxTimestamp: number,
* duration: number
* }}
*
* @description
* Contains a stream and information about it.
*
* @property {!shaka.extern.Stream} stream
* The Stream itself.
* @property {!shaka.media.SegmentIndex} segmentIndex
* SegmentIndex of the stream.
* @property {!Array.<shaka.extern.DrmInfo>} drmInfos
* DrmInfos of the stream. There may be multiple for multi-DRM content.
* @property {string} verbatimMediaPlaylistUri
* The verbatim media playlist URI, as it appeared in the master playlist.
* This has not been canonicalized into an absolute URI. This gives us a
* consistent key for this playlist, even if redirects cause us to update
* from different origins each time.
* @property {string} absoluteMediaPlaylistUri
* The absolute media playlist URI, resolved relative to the master playlist
* and updated to reflect any redirects.
* @property {number} minTimestamp
* The minimum timestamp found in the stream.
* @property {number} maxTimestamp
* The maximum timestamp found in the stream.
* @property {number} duration
* The duration of the playlist. Used for VOD only.
*/
shaka.hls.HlsParser.StreamInfo;
/**
* @override
* @exportInterface
*/
shaka.hls.HlsParser.prototype.configure = function(config) {
this.config_ = config;
};
/**
* @override
* @exportInterface
*/
shaka.hls.HlsParser.prototype.start = function(uri, playerInterface) {
goog.asserts.assert(this.config_, 'Must call configure() before start()!');
this.playerInterface_ = playerInterface;
return this.requestManifest_(uri).then(function(response) {
// Record the master playlist URI after redirects.
this.masterPlaylistUri_ = response.uri;
return this.parseManifest_(response.data).then(function() {
this.setUpdateTimer_(this.updatePeriod_);
return this.manifest_;
}.bind(this));
}.bind(this));
};
/**
* @override
* @exportInterface
*/
shaka.hls.HlsParser.prototype.stop = function() {
this.playerInterface_ = null;
this.config_ = null;
this.mediaTagsToStreamInfosMap_.clear();
this.variantUriSet_.clear();
this.uriToStreamInfosMap_.clear();
this.manifest_ = null;
return this.operationManager_.destroy();
};
/**
* @override
* @exportInterface
*/
shaka.hls.HlsParser.prototype.update = function() {
if (!this.isLive_()) {
return;
}
const promises = [];
for (const streamInfo of this.uriToStreamInfosMap_.values()) {
promises.push(this.updateStream_(streamInfo));
}
return Promise.all(promises);
};
/**
* Updates a stream.
*
* @param {!shaka.hls.HlsParser.StreamInfo} streamInfo
* @throws shaka.util.Error
* @private
*/
shaka.hls.HlsParser.prototype.updateStream_ = function(streamInfo) {
this.requestManifest_(streamInfo.absoluteMediaPlaylistUri).then(
function(response) {
const Utils = shaka.hls.Utils;
const PresentationType = shaka.hls.HlsParser.PresentationType_;
const playlist = this.manifestTextParser_.parsePlaylist(
response.data, response.uri);
if (playlist.type != shaka.hls.PlaylistType.MEDIA) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.HLS_INVALID_PLAYLIST_HIERARCHY);
}
let mediaSequenceTag = Utils.getFirstTagWithName(playlist.tags,
'EXT-X-MEDIA-SEQUENCE');
let startPosition = mediaSequenceTag ? Number(mediaSequenceTag.value) : 0;
let stream = streamInfo.stream;
this.createSegments_(streamInfo.verbatimMediaPlaylistUri, playlist,
startPosition, stream.mimeType, stream.codecs)
.then(function(segments) {
streamInfo.segmentIndex.replace(segments);
let newestSegment = segments[segments.length - 1];
goog.asserts.assert(newestSegment, 'Should have segments!');
// Once the last segment has been added to the playlist,
// #EXT-X-ENDLIST tag will be appended.
// If that happened, treat the rest of the EVENT presentation as VOD.
let endListTag = Utils.getFirstTagWithName(playlist.tags,
'EXT-X-ENDLIST');
if (endListTag) {
// Convert the presentation to VOD and set the duration to the last
// segment's end time.
this.setPresentationType_(PresentationType.VOD);
this.presentationTimeline_.setDuration(newestSegment.endTime);
}
}.bind(this));
}.bind(this));
};
/**
* @override
* @exportInterface
*/
shaka.hls.HlsParser.prototype.onExpirationUpdated = function(
sessionId, expiration) {
// No-op
};
/**
* Parses the manifest.
*
* @param {!ArrayBuffer} data
* @throws shaka.util.Error When there is a parsing error.
* @return {!Promise}
* @private
*/
shaka.hls.HlsParser.prototype.parseManifest_ = function(data) {
goog.asserts.assert(this.masterPlaylistUri_,
'Master playlist URI must be set before calling parseManifest_!');
const playlist = this.manifestTextParser_.parsePlaylist(
data, this.masterPlaylistUri_);
// We don't support directly providing a Media Playlist.
// See the error code for details.
if (playlist.type != shaka.hls.PlaylistType.MASTER) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.HLS_MASTER_PLAYLIST_NOT_PROVIDED);
}
return this.createPeriod_(playlist).then(function(period) {
// HLS has no notion of periods. We're treating the whole presentation as
// one period.
this.playerInterface_.filterAllPeriods([period]);
// Find the min and max timestamp of the earliest segment in all streams.
// Find the minimum duration of all streams as well.
let minFirstTimestamp = Infinity;
let maxFirstTimestamp = 0;
let maxLastTimestamp = 0;
let minDuration = Infinity;
for (const streamInfo of this.uriToStreamInfosMap_.values()) {
minFirstTimestamp =
Math.min(minFirstTimestamp, streamInfo.minTimestamp);
maxFirstTimestamp =
Math.max(maxFirstTimestamp, streamInfo.minTimestamp);
maxLastTimestamp =
Math.max(maxLastTimestamp, streamInfo.maxTimestamp);
if (streamInfo.stream.type != 'text') {
minDuration = Math.min(minDuration, streamInfo.duration);
}
}
// This assert is our own sanity check.
goog.asserts.assert(this.presentationTimeline_ == null,
'Presentation timeline created early!');
this.createPresentationTimeline_(maxLastTimestamp);
// This assert satisfies the compiler that it is not null for the rest of
// the method.
goog.asserts.assert(this.presentationTimeline_,
'Presentation timeline not created!');
if (this.isLive_()) {
// The HLS spec (RFC 8216) states in 6.3.4:
// "the client MUST wait for at least the target duration before
// attempting to reload the Playlist file again"
this.updatePeriod_ = this.minTargetDuration_;
// The spec says nothing much about seeking in live content, but Safari's
// built-in HLS implementation does not allow it. Therefore we will set
// the availability window equal to the presentation delay. The player
// will be able to buffer ahead three segments, but the seek window will
// be zero-sized.
const PresentationType = shaka.hls.HlsParser.PresentationType_;
if (this.presentationType_ == PresentationType.LIVE) {
// This defaults to the presentation delay, which has the effect of
// making the live stream unseekable. This is consistent with Apple's
// HLS implementation.
let segmentAvailabilityDuration = this.presentationTimeline_.getDelay();
// The app can override that with a longer duration, to allow seeking.
if (!isNaN(this.config_.availabilityWindowOverride)) {
segmentAvailabilityDuration = this.config_.availabilityWindowOverride;
}
this.presentationTimeline_.setSegmentAvailabilityDuration(
segmentAvailabilityDuration);
}
let rolloverSeconds =
shaka.hls.HlsParser.TS_ROLLOVER_ / shaka.hls.HlsParser.TS_TIMESCALE_;
let offset = 0;
while (maxFirstTimestamp >= rolloverSeconds) {
offset += rolloverSeconds;
maxFirstTimestamp -= rolloverSeconds;
}
if (offset) {
shaka.log.debug('Offsetting live streams by', offset,
'to compensate for rollover');
for (const streamInfo of this.uriToStreamInfosMap_.values()) {
if (streamInfo.minTimestamp < rolloverSeconds) {
shaka.log.v1('Offset applied to', streamInfo.stream.type);
// This is the offset that StreamingEngine must apply to align the
// actual segment times with the period.
streamInfo.stream.presentationTimeOffset = -offset;
// The segments were created with actual media times, rather than
// period-aligned times, so offset them all to period time.
streamInfo.segmentIndex.offset(offset);
} else {
shaka.log.v1('Offset NOT applied to', streamInfo.stream.type);
}
}
}
} else {
// For VOD/EVENT content, offset everything back to 0.
// Use the minimum timestamp as the offset for all streams.
// Use the minimum duration as the presentation duration.
this.presentationTimeline_.setDuration(minDuration);
// Use a negative offset to adjust towards 0.
this.presentationTimeline_.offset(-minFirstTimestamp);
for (const streamInfo of this.uriToStreamInfosMap_.values()) {
// This is the offset that StreamingEngine must apply to align the
// actual segment times with the period.
streamInfo.stream.presentationTimeOffset = minFirstTimestamp;
// The segments were created with actual media times, rather than
// period-aligned times, so offset them all now.
streamInfo.segmentIndex.offset(-minFirstTimestamp);
// Finally, fit the segments to the period duration.
streamInfo.segmentIndex.fit(minDuration);
}
}
this.manifest_ = {
presentationTimeline: this.presentationTimeline_,
periods: [period],
offlineSessionIds: [],
minBufferTime: 0,
};
}.bind(this));
};
/**
* Parses a playlist into a Period object.
*
* @param {!shaka.hls.Playlist} playlist
* @return {!Promise.<!shaka.extern.Period>}
* @private
*/
shaka.hls.HlsParser.prototype.createPeriod_ = function(playlist) {
const Utils = shaka.hls.Utils;
const Functional = shaka.util.Functional;
let tags = playlist.tags;
let mediaTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-MEDIA');
let textStreamTags = mediaTags.filter(function(tag) {
let type = shaka.hls.HlsParser.getRequiredAttributeValue_(tag, 'TYPE');
return type == 'SUBTITLES';
}.bind(this));
let textStreamPromises = textStreamTags.map(function(tag) {
return this.createTextStream_(tag, playlist);
}.bind(this));
const closedCaptionsTags = mediaTags.filter((tag) => {
const type = shaka.hls.HlsParser.getRequiredAttributeValue_(tag, 'TYPE');
return type == 'CLOSED-CAPTIONS';
});
this.parseClosedCaptions_(closedCaptionsTags);
return Promise.all(textStreamPromises).then(function(textStreams) {
// Create Variants for every 'EXT-X-STREAM-INF' tag. Do this after text
// streams have been created, so that we can push text codecs found on the
// variant tag back into the created text streams.
let variantTags = Utils.filterTagsByName(tags, 'EXT-X-STREAM-INF');
let variantsPromises = variantTags.map(function(tag) {
return this.createVariantsForTag_(tag, playlist);
}.bind(this));
return Promise.all(variantsPromises).then(function(allVariants) {
let variants = allVariants.reduce(Functional.collapseArrays, []);
return {
startTime: 0,
variants: variants,
textStreams: textStreams,
};
}.bind(this));
}.bind(this));
};
/**
* @param {!shaka.hls.Tag} tag
* @param {!shaka.hls.Playlist} playlist
* @return {!Promise.<!Array.<!shaka.extern.Variant>>}
* @private
*/
shaka.hls.HlsParser.prototype.createVariantsForTag_ = function(tag, playlist) {
goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
'Should only be called on variant tags!');
const ContentType = shaka.util.ManifestParserUtils.ContentType;
const HlsParser = shaka.hls.HlsParser;
const Utils = shaka.hls.Utils;
// These are the default codecs to assume if none are specified.
//
// The video codec is H.264, with baseline profile and level 3.0.
// http://blog.pearce.org.nz/2013/11/what-does-h264avc1-codecs-parameters.html
//
// The audio codec is "low-complexity" AAC.
const defaultCodecs = 'avc1.42E01E,mp4a.40.2';
const codecsString = tag.getAttributeValue('CODECS', defaultCodecs);
// Strip out internal whitespace while splitting on commas:
/** @type {!Array.<string>} */
let codecs = codecsString.split(/\s*,\s*/);
let resolutionAttr = tag.getAttribute('RESOLUTION');
let width = null;
let height = null;
let frameRate = tag.getAttributeValue('FRAME-RATE');
let bandwidth =
Number(HlsParser.getRequiredAttributeValue_(tag, 'BANDWIDTH'));
if (resolutionAttr) {
let resBlocks = resolutionAttr.value.split('x');
width = resBlocks[0];
height = resBlocks[1];
}
// After filtering, this is a list of the media tags we will process to
// combine with the variant tag (EXT-X-STREAM-INF) we are working on.
let mediaTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-MEDIA');
// Do not create stream info from closed captions media tags, which are
// embedded in video streams.
mediaTags = mediaTags.filter((tag) => {
const type = HlsParser.getRequiredAttributeValue_(tag, 'TYPE');
return type != 'CLOSED-CAPTIONS';
});
let audioGroupId = tag.getAttributeValue('AUDIO');
let videoGroupId = tag.getAttributeValue('VIDEO');
goog.asserts.assert(audioGroupId == null || videoGroupId == null,
'Unexpected: both video and audio described by media tags!');
// Find any associated audio or video groups and create streams for them.
if (audioGroupId) {
mediaTags = Utils.findMediaTags(mediaTags, 'AUDIO', audioGroupId);
} else if (videoGroupId) {
mediaTags = Utils.findMediaTags(mediaTags, 'VIDEO', videoGroupId);
}
// There may be a codec string for the text stream. We should identify it,
// add it to the appropriate stream, then strip it out of the variant to
// avoid confusing our multiplex detection below.
let textCodecs = this.guessCodecsSafe_(ContentType.TEXT, codecs);
if (textCodecs) {
// We found a text codec in the list, so look for an associated text stream.
let subGroupId = tag.getAttributeValue('SUBTITLES');
if (subGroupId) {
let textTags = Utils.findMediaTags(mediaTags, 'SUBTITLES', subGroupId);
goog.asserts.assert(textTags.length == 1,
'Exactly one text tag expected!');
if (textTags.length) {
// We found a text codec and text stream, so make sure the codec is
// attached to the stream.
const textStreamInfo =
this.mediaTagsToStreamInfosMap_.get(textTags[0].id);
textStreamInfo.stream.codecs = textCodecs;
}
}
// Remove this entry from the list of codecs that belong to audio/video.
shaka.util.ArrayUtils.remove(codecs, textCodecs);
}
let promises = mediaTags.map(function(tag) {
return this.createStreamInfoFromMediaTag_(tag, codecs);
}.bind(this));
let audioStreamInfos = [];
let videoStreamInfos = [];
return Promise.all(promises).then(function(data) {
if (audioGroupId) {
audioStreamInfos = data;
} else if (videoGroupId) {
videoStreamInfos = data;
}
// Make an educated guess about the stream type.
shaka.log.debug('Guessing stream type for', tag.toString());
let type;
let ignoreStream = false;
if (!audioStreamInfos.length && !videoStreamInfos.length) {
// There are no associated streams. This is either an audio-only stream,
// a video-only stream, or a multiplexed stream.
if (codecs.length == 1) {
// There is only one codec, so it shouldn't be multiplexed.
let videoCodecs = this.guessCodecsSafe_(ContentType.VIDEO, codecs);
if (resolutionAttr || frameRate || videoCodecs) {
// Assume video-only.
shaka.log.debug('Guessing video-only.');
type = ContentType.VIDEO;
} else {
// Assume audio-only.
shaka.log.debug('Guessing audio-only.');
type = ContentType.AUDIO;
}
} else {
// There are multiple codecs, so assume multiplexed content.
// Note that the default used when CODECS is missing assumes multiple
// (and therefore multiplexed).
// Recombine the codec strings into one so that MediaSource isn't
// lied to later. (That would trigger an error in Chrome.)
shaka.log.debug('Guessing multiplexed audio+video.');
type = ContentType.VIDEO;
codecs = [codecs.join(',')];
}
} else if (audioStreamInfos.length) {
let streamURI = HlsParser.getRequiredAttributeValue_(tag, 'URI');
let firstAudioStreamURI = audioStreamInfos[0].verbatimMediaPlaylistUri;
if (streamURI == firstAudioStreamURI) {
// The Microsoft HLS manifest generators will make audio-only variants
// that link to their URI both directly and through an audio tag.
// In that case, ignore the local URI and use the version in the
// AUDIO tag, so you inherit its language.
// As an example, see the manifest linked in issue #860.
shaka.log.debug('Guessing audio-only.');
type = ContentType.AUDIO;
ignoreStream = true;
} else {
// There are associated audio streams. Assume this is video.
shaka.log.debug('Guessing video.');
type = ContentType.VIDEO;
}
} else {
// There are associated video streams. Assume this is audio.
goog.asserts.assert(videoStreamInfos.length,
'No video streams! This should have been handled already!');
shaka.log.debug('Guessing audio.');
type = ContentType.AUDIO;
}
goog.asserts.assert(type, 'Type should have been set by now!');
if (ignoreStream) {
return Promise.resolve();
}
return this.createStreamInfoFromVariantTag_(tag, codecs, type);
}.bind(this)).then(function(streamInfo) {
if (streamInfo) {
if (streamInfo.stream.type == ContentType.AUDIO) {
audioStreamInfos = [streamInfo];
} else {
videoStreamInfos = [streamInfo];
}
}
goog.asserts.assert(videoStreamInfos || audioStreamInfos,
'We should have created a stream!');
if (videoStreamInfos) {
this.filterLegacyCodecs_(videoStreamInfos);
}
if (audioStreamInfos) {
this.filterLegacyCodecs_(audioStreamInfos);
}
return this.createVariants_(
audioStreamInfos,
videoStreamInfos,
bandwidth,
width,
height,
frameRate);
}.bind(this));
};
/**
* Filters out unsupported codec strings from an array of stream infos.
* @param {!Array.<shaka.hls.HlsParser.StreamInfo>} streamInfos
* @private
*/
shaka.hls.HlsParser.prototype.filterLegacyCodecs_ = function(streamInfos) {
streamInfos.forEach(function(streamInfo) {
let codecs = streamInfo.stream.codecs.split(',');
codecs = codecs.filter(function(codec) {
// mp4a.40.34 is a nonstandard codec string that is sometimes used in HLS
// for legacy reasons. It is not recognized by non-Apple MSE.
// See https://bugs.chromium.org/p/chromium/issues/detail?id=489520
// Therefore, ignore this codec string.
return codec != 'mp4a.40.34';
});
streamInfo.stream.codecs = codecs.join(',');
});
};
/**
* @param {!Array.<!shaka.hls.HlsParser.StreamInfo>} audioInfos
* @param {!Array.<!shaka.hls.HlsParser.StreamInfo>} videoInfos
* @param {number} bandwidth
* @param {?string} width
* @param {?string} height
* @param {?string} frameRate
* @return {!Array.<!shaka.extern.Variant>}
* @private
*/
shaka.hls.HlsParser.prototype.createVariants_ =
function(audioInfos, videoInfos, bandwidth, width, height, frameRate) {
const DrmEngine = shaka.media.DrmEngine;
videoInfos.forEach(function(info) {
this.addVideoAttributes_(info.stream, width, height, frameRate);
}.bind(this));
// In case of audio-only or video-only content, we create an array of
// one item containing a null. This way, the double-loop works for all
// kinds of content.
// NOTE: we currently don't have support for audio-only content.
if (!audioInfos.length) {
audioInfos = [null];
}
if (!videoInfos.length) {
videoInfos = [null];
}
let variants = [];
for (let i = 0; i < audioInfos.length; i++) {
for (let j = 0; j < videoInfos.length; j++) {
let audioStream = audioInfos[i] ? audioInfos[i].stream : null;
let videoStream = videoInfos[j] ? videoInfos[j].stream : null;
let audioDrmInfos = audioInfos[i] ? audioInfos[i].drmInfos : null;
let videoDrmInfos = videoInfos[j] ? videoInfos[j].drmInfos : null;
let drmInfos;
if (audioStream && videoStream) {
if (DrmEngine.areDrmCompatible(audioDrmInfos, videoDrmInfos)) {
drmInfos = DrmEngine.getCommonDrmInfos(audioDrmInfos, videoDrmInfos);
} else {
shaka.log.warning('Incompatible DRM info in HLS variant. Skipping.');
continue;
}
} else if (audioStream) {
drmInfos = audioDrmInfos;
} else if (videoStream) {
drmInfos = videoDrmInfos;
}
const videoStreamUri =
videoInfos[i] ? videoInfos[i].verbatimMediaPlaylistUri : '';
const audioStreamUri =
audioInfos[i] ? audioInfos[i].verbatimMediaPlaylistUri : '';
const variantUriKey = videoStreamUri + ' - ' + audioStreamUri;
if (this.variantUriSet_.has(variantUriKey)) {
// This happens when two variants only differ in their text streams.
shaka.log.debug('Skipping variant which only differs in text streams.');
continue;
}
let variant = this.createVariant_(
audioStream, videoStream, bandwidth, drmInfos);
variants.push(variant);
this.variantUriSet_.add(variantUriKey);
}
}
return variants;
};
/**
* @param {shaka.extern.Stream} audio
* @param {shaka.extern.Stream} video
* @param {number} bandwidth
* @param {!Array.<shaka.extern.DrmInfo>} drmInfos
* @return {!shaka.extern.Variant}
* @private
*/
shaka.hls.HlsParser.prototype.createVariant_ =
function(audio, video, bandwidth, drmInfos) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
// Since both audio and video are of the same type, this assertion will catch
// certain mistakes at runtime that the compiler would miss.
goog.asserts.assert(!audio || audio.type == ContentType.AUDIO,
'Audio parameter mismatch!');
goog.asserts.assert(!video || video.type == ContentType.VIDEO,
'Video parameter mismatch!');
return {
id: this.globalId_++,
language: audio ? audio.language : 'und',
primary: (!!audio && audio.primary) || (!!video && video.primary),
audio: audio,
video: video,
bandwidth: bandwidth,
drmInfos: drmInfos,
allowedByApplication: true,
allowedByKeySystem: true,
};
};
/**
* Parses an EXT-X-MEDIA tag with TYPE="SUBTITLES" into a text stream.
*
* @param {!shaka.hls.Tag} tag
* @param {!shaka.hls.Playlist} playlist
* @return {!Promise.<?shaka.extern.Stream>}
* @private
*/
shaka.hls.HlsParser.prototype.createTextStream_ = function(tag, playlist) {
goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
'Should only be called on media tags!');
let type = shaka.hls.HlsParser.getRequiredAttributeValue_(tag, 'TYPE');
goog.asserts.assert(type == 'SUBTITLES',
'Should only be called on tags with TYPE="SUBTITLES"!');
return this.createStreamInfoFromMediaTag_(tag, [])
.then(function(streamInfo) {
return streamInfo.stream;
});
};
/**
* Parses an EXT-X-MEDIA tag with TYPE="CLOSED-CAPTIONS", add store the values
* into the map of group id to closed captions.
*
* @param {!Array.<shaka.hls.Tag>} tags
* @private
*/
shaka.hls.HlsParser.prototype.parseClosedCaptions_ = function(tags) {
for (const tag of tags) {
goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
'Should only be called on media tags!');
const type = shaka.hls.HlsParser.getRequiredAttributeValue_(tag, 'TYPE');
goog.asserts.assert(type == 'CLOSED-CAPTIONS',
'Should only be called on tags with TYPE="CLOSED-CAPTIONS"!');
const LanguageUtils = shaka.util.LanguageUtils;
const languageValue = tag.getAttributeValue('LANGUAGE') || 'und';
const language = LanguageUtils.normalize(languageValue);
// The GROUP-ID value is a quoted-string that specifies the group to which
// the Rendition belongs.
const groupId =
shaka.hls.HlsParser.getRequiredAttributeValue_(tag, 'GROUP-ID');
// The value of INSTREAM-ID is a quoted-string that specifies a Rendition
// within the segments in the Media Playlist. This attribute is REQUIRED if
// the TYPE attribute is CLOSED-CAPTIONS.
const instreamId =
shaka.hls.HlsParser.getRequiredAttributeValue_(tag, 'INSTREAM-ID');
if (!this.groupIdToClosedCaptionsMap_.get(groupId)) {
this.groupIdToClosedCaptionsMap_.set(groupId, new Map());
}
this.groupIdToClosedCaptionsMap_.get(groupId).set(instreamId, language);
}
};
/**
* Parse EXT-X-MEDIA media tag into a Stream object.
*
* @param {shaka.hls.Tag} tag
* @param {!Array.<string>} allCodecs
* @return {!Promise.<shaka.hls.HlsParser.StreamInfo>}
* @private
*/
shaka.hls.HlsParser.prototype.createStreamInfoFromMediaTag_ =
function(tag, allCodecs) {
goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
'Should only be called on media tags!');
const HlsParser = shaka.hls.HlsParser;
const verbatimMediaPlaylistUri = HlsParser.getRequiredAttributeValue_(
tag, 'URI');
// Check if the stream has already been created as part of another Variant
// and return it if it has.
if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
return Promise.resolve(
this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri));
}
let type = HlsParser.getRequiredAttributeValue_(tag, 'TYPE').toLowerCase();
// Shaka recognizes the content types 'audio', 'video' and 'text'.
// The HLS 'subtitles' type needs to be mapped to 'text'.
const ContentType = shaka.util.ManifestParserUtils.ContentType;
if (type == 'subtitles') type = ContentType.TEXT;
const LanguageUtils = shaka.util.LanguageUtils;
let language = LanguageUtils.normalize(/** @type {string} */(
tag.getAttributeValue('LANGUAGE', 'und')));
const name = tag.getAttributeValue('NAME');
let defaultAttr = tag.getAttribute('DEFAULT');
let autoselectAttr = tag.getAttribute('AUTOSELECT');
// TODO: Should we take into account some of the currently ignored attributes:
// FORCED, INSTREAM-ID, CHARACTERISTICS, CHANNELS?
// Attribute descriptions: https://bit.ly/2lpjOhj
let channelsAttribute = tag.getAttributeValue('CHANNELS');
let channelsCount = type == 'audio' ?
this.getChannelCount_(channelsAttribute) : null;
let primary = !!defaultAttr || !!autoselectAttr;
return this.createStreamInfo_(
verbatimMediaPlaylistUri, allCodecs, type, language, primary, name,
channelsCount, /* closedCaptions */ null).then(function(streamInfo) {
// TODO: This check is necessary because of the possibility of multiple
// calls to createStreamInfoFromMediaTag_ before either has resolved.
if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
return this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
}
this.mediaTagsToStreamInfosMap_.set(tag.id, streamInfo);
this.uriToStreamInfosMap_.set(verbatimMediaPlaylistUri, streamInfo);
return streamInfo;
}.bind(this));
};
/**
* Get the channel count information for an HLS audio track.
*
* @param {?string} channels A string that specifies an ordered, "/" separated
* list of parameters. If the type is audio, the first parameter will be a
* decimal integer specifying the number of independent, simultaneous audio
* channels.
* No other channels parameters are currently defined.
* @return {?number} channelcount
* @private
*/
shaka.hls.HlsParser.prototype.getChannelCount_ = function(channels) {
if (!channels) return null;
let channelcountstring = channels.split('/')[0];
let count = parseInt(channelcountstring, 10);
return count;
};
/**
* Parse an EXT-X-STREAM-INF media tag into a Stream object.
*
* @param {!shaka.hls.Tag} tag
* @param {!Array.<string>} allCodecs
* @param {string} type
* @return {!Promise.<shaka.hls.HlsParser.StreamInfo>}
* @private
*/
shaka.hls.HlsParser.prototype.createStreamInfoFromVariantTag_ =
function(tag, allCodecs, type) {
goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
'Should only be called on media tags!');
const ContentType = shaka.util.ManifestParserUtils.ContentType;
const HlsParser = shaka.hls.HlsParser;
const verbatimMediaPlaylistUri = HlsParser.getRequiredAttributeValue_(
tag, 'URI');
if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
return Promise.resolve(
this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri));
}
// The attribute of closed captions is optional, and the value may be 'NONE'.
const closedCaptionsAttr = tag.getAttributeValue('CLOSED-CAPTIONS');
// EXT-X-STREAM-INF tags may have CLOSED-CAPTIONS attributes.
// The value can be either a quoted-string or an enumerated-string with the
// value NONE. If the value is a quoted-string, it MUST match the value of
// the GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the Playlist
// whose TYPE attribute is CLOSED-CAPTIONS.
let closedCaptions = null;
if (type == ContentType.VIDEO && closedCaptionsAttr &&
closedCaptionsAttr != 'NONE') {
closedCaptions = this.groupIdToClosedCaptionsMap_.get(closedCaptionsAttr);
}
return this.createStreamInfo_(verbatimMediaPlaylistUri, allCodecs, type,
/* language */ 'und', /* primary */ false, /* name */ null,
/* channelcount */ null, closedCaptions).then(function(streamInfo) {
// TODO: This check is necessary because of the possibility of multiple
// calls to createStreamInfoFromVariantTag_ before either has resolved.
if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
return this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
}
this.uriToStreamInfosMap_.set(verbatimMediaPlaylistUri, streamInfo);
return streamInfo;
}.bind(this));
};
/**
* @param {string} verbatimMediaPlaylistUri
* @param {!Array.<string>} allCodecs
* @param {string} type
* @param {string} language
* @param {boolean} primary
* @param {?string} name
* @param {?number} channelsCount
* @param {Map.<string, string>} closedCaptions
* @return {!Promise.<shaka.hls.HlsParser.StreamInfo>}
* @throws shaka.util.Error
* @private
*/
shaka.hls.HlsParser.prototype.createStreamInfo_ = function(
verbatimMediaPlaylistUri, allCodecs, type, language, primary, name,
channelsCount, closedCaptions) {
// TODO: Refactor, too many parameters
const Utils = shaka.hls.Utils;
const HlsParser = shaka.hls.HlsParser;
let absoluteMediaPlaylistUri = Utils.constructAbsoluteUri(
this.masterPlaylistUri_, verbatimMediaPlaylistUri);
/** @type {!shaka.hls.Playlist} */
let playlist;
/** @type {string} */
let codecs = '';
/** @type {string} */
let mimeType;
return this.requestManifest_(absoluteMediaPlaylistUri).then((response) => {
// Record the final URI after redirects.
absoluteMediaPlaylistUri = response.uri;
// Record the redirected, final URI of this media playlist when we parse it.
playlist = this.manifestTextParser_.parsePlaylist(
response.data, absoluteMediaPlaylistUri);
if (playlist.type != shaka.hls.PlaylistType.MEDIA) {
// EXT-X-MEDIA tags should point to media playlists.
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.HLS_INVALID_PLAYLIST_HIERARCHY);
}
goog.asserts.assert(playlist.segments != null,
'Media playlist should have segments!');
this.determinePresentationType_(playlist);
codecs = this.guessCodecs_(type, allCodecs);
return this.guessMimeType_(type, codecs, playlist);
}).then((mimeTypeArg) => {
mimeType = mimeTypeArg;
let mediaSequenceTag = Utils.getFirstTagWithName(playlist.tags,
'EXT-X-MEDIA-SEQUENCE');
let startPosition = mediaSequenceTag ? Number(mediaSequenceTag.value) : 0;
return this.createSegments_(
verbatimMediaPlaylistUri, playlist, startPosition, mimeType, codecs);
}).then((segments) => {
let minTimestamp = segments[0].startTime;
let lastEndTime = segments[segments.length - 1].endTime;
let duration = lastEndTime - minTimestamp;
let segmentIndex = new shaka.media.SegmentIndex(segments);
const initSegmentReference = this.createInitSegmentReference_(playlist);
let kind = undefined;
const ManifestParserUtils = shaka.util.ManifestParserUtils;
if (type == ManifestParserUtils.ContentType.TEXT) {
kind = ManifestParserUtils.TextStreamKind.SUBTITLE;
}
let drmTags = [];
playlist.segments.forEach(function(segment) {
let segmentKeyTags = Utils.filterTagsByName(segment.tags,
'EXT-X-KEY');
drmTags.push.apply(drmTags, segmentKeyTags);
});
let encrypted = false;
let drmInfos = [];
let keyId = null;
// TODO: May still need changes to support key rotation.
drmTags.forEach(function(drmTag) {
let method = HlsParser.getRequiredAttributeValue_(drmTag, 'METHOD');
if (method != 'NONE') {
encrypted = true;
let keyFormat =
HlsParser.getRequiredAttributeValue_(drmTag, 'KEYFORMAT');
let drmParser =
shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
let drmInfo = drmParser ? drmParser(drmTag) : null;
if (drmInfo) {
if (drmInfo.keyIds.length) {
keyId = drmInfo.keyIds[0];
}
drmInfos.push(drmInfo);
} else {
shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
}
}
});
if (encrypted && !drmInfos.length) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.HLS_KEYFORMATS_NOT_SUPPORTED);
}
/** @type {shaka.extern.Stream} */
let stream = {
id: this.globalId_++,
originalId: name,
createSegmentIndex: Promise.resolve.bind(Promise),
findSegmentPosition: segmentIndex.find.bind(segmentIndex),
getSegmentReference: segmentIndex.get.bind(segmentIndex),
initSegmentReference: initSegmentReference,
presentationTimeOffset: 0,
mimeType: mimeType,
codecs: codecs,
kind: kind,
encrypted: encrypted,
keyId: keyId,
language: language,
label: name, // For historical reasons, since before "originalId".
type: type,
primary: primary,
// TODO: trick mode
trickModeVideo: null,
emsgSchemeIdUris: null,
frameRate: undefined,
width: undefined,
height: undefined,
bandwidth: undefined,
roles: [],
channelsCount: channelsCount,
closedCaptions: closedCaptions,
};
return {
stream: stream,
segmentIndex: segmentIndex,
drmInfos: drmInfos,
verbatimMediaPlaylistUri: verbatimMediaPlaylistUri,
absoluteMediaPlaylistUri: absoluteMediaPlaylistUri,
minTimestamp: minTimestamp,
maxTimestamp: lastEndTime,
duration: duration,
};
});
};
/**
* @param {!shaka.hls.Playlist} playlist
* @private
*/
shaka.hls.HlsParser.prototype.determinePresentationType_ = function(playlist) {
const Utils = shaka.hls.Utils;
const PresentationType = shaka.hls.HlsParser.PresentationType_;
let presentationTypeTag = Utils.getFirstTagWithName(playlist.tags,
'EXT-X-PLAYLIST-TYPE');
let endListTag = Utils.getFirstTagWithName(playlist.tags, 'EXT-X-ENDLIST');
let isVod = (presentationTypeTag && presentationTypeTag.value == 'VOD') ||
endListTag;
let isEvent = presentationTypeTag && presentationTypeTag.value == 'EVENT' &&
!isVod;
let isLive = !isVod && !isEvent;
if (isVod) {
this.setPresentationType_(PresentationType.VOD);
} else {
// If it's not VOD, it must be presentation type LIVE or an ongoing EVENT.
if (isLive) {
this.setPresentationType_(PresentationType.LIVE);
} else {
this.setPresentationType_(PresentationType.EVENT);
}
let targetDurationTag = this.getRequiredTag_(playlist.tags,
'EXT-X-TARGETDURATION');
let targetDuration = Number(targetDurationTag.value);
// According to the HLS spec, updates should not happen more often than
// once in targetDuration. It also requires us to only update the active
// variant. We might implement that later, but for now every variant
// will be updated. To get the update period, choose the smallest
// targetDuration value across all playlists.
// Update the longest target duration if need be to use as a presentation
// delay later.
this.maxTargetDuration_ = Math.max(targetDuration, this.maxTargetDuration_);
// Update the shortest one to use as update period and segment availability
// time (for LIVE).
this.minTargetDuration_ = Math.min(targetDuration, this.minTargetDuration_);
}
};
/**
* @param {number} lastTimestamp
* @throws shaka.util.Error
* @private
*/
shaka.hls.HlsParser.prototype.createPresentationTimeline_ =
function(lastTimestamp) {
if (this.isLive_()) {
// The live edge will be calculated from segments, so we don't need to set
// a presentation start time. We will assert later that this is working as
// expected.
// The HLS spec (RFC 8216) states in 6.3.3:
//
// "The client SHALL choose which Media Segment to play first ... the
// client SHOULD NOT choose a segment that starts less than three target
// durations from the end of the Playlist file. Doing so can trigger
// playback stalls."
//
// We accomplish this in our DASH-y model by setting a presentation delay
// of 3 segments. This will be the "live edge" of the presentation.
this.presentationTimeline_ = new shaka.media.PresentationTimeline(
/* presentationStartTime */ 0, /* delay */ this.maxTargetDuration_ * 3);
this.presentationTimeline_.setStatic(false);
} else {
this.presentationTimeline_ = new shaka.media.PresentationTimeline(
/* presentationStartTime */ null, /* delay */ 0);
this.presentationTimeline_.setStatic(true);
}
this.notifySegments_();
// This asserts that the live edge is being calculated from segment times.
// For VOD and event streams, this check should still pass.
goog.asserts.assert(
!this.presentationTimeline_.usingPresentationStartTime(),
'We should not be using the presentation start time in HLS!');
};
/**
* @param {!shaka.hls.Playlist} playlist
* @return {shaka.media.InitSegmentReference}
* @private
* @throws {shaka.util.Error}
*/
shaka.hls.HlsParser.prototype.createInitSegmentReference_ = function(playlist) {
const Utils = shaka.hls.Utils;
let mapTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-MAP');
// TODO: Support multiple map tags?
// For now, we don't support multiple map tags and will throw an error.
if (!mapTags.length) {
return null;
} else if (mapTags.length > 1) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.HLS_MULTIPLE_MEDIA_INIT_SECTIONS_FOUND);
}
// Map tag example: #EXT-X-MAP:URI="main.mp4",BYTERANGE="720@0"
let mapTag = mapTags[0];
const verbatimInitSegmentUri =
shaka.hls.HlsParser.getRequiredAttributeValue_(mapTag, 'URI');
const absoluteInitSegmentUri =
Utils.constructAbsoluteUri(playlist.absoluteUri, verbatimInitSegmentUri);
let startByte = 0;
let endByte = null;
let byterange = mapTag.getAttributeValue('BYTERANGE');
// If a BYTERANGE attribute is not specified, the segment consists
// of the entire resource.
if (byterange) {
let blocks = byterange.split('@');
let byteLength = Number(blocks[0]);
startByte = Number(blocks[1]);
endByte = startByte + byteLength - 1;
}
return new shaka.media.InitSegmentReference(
() => [absoluteInitSegmentUri],
startByte,
endByte);
};
/**
* Parses one shaka.hls.Segment object into a shaka.media.SegmentReference.
*
* @param {!shaka.hls.Playlist} playlist
* @param {shaka.media.SegmentReference} previousReference
* @param {!shaka.hls.Segment} hlsSegment
* @param {number} position
* @param {number} startTime
* @return {!shaka.media.SegmentReference}
* @private
*/
shaka.hls.HlsParser.prototype.createSegmentReference_ =
function(playlist, previousReference, hlsSegment, position, startTime) {
const Utils = shaka.hls.Utils;
const tags = hlsSegment.tags;
const absoluteSegmentUri = hlsSegment.absoluteUri;
let extinfTag = this.getRequiredTag_(tags, 'EXTINF');
// The EXTINF tag format is '#EXTINF:<duration>,[<title>]'.
// We're interested in the duration part.
let extinfValues = extinfTag.value.split(',');
let duration = Number(extinfValues[0]);
let endTime = startTime + duration;
let startByte = 0;
let endByte = null;
let byterange = Utils.getFirstTagWithName(tags, 'EXT-X-BYTERANGE');
// If BYTERANGE is not specified, the segment consists of the entire resource.
if (byterange) {
let blocks = byterange.value.split('@');
let byteLength = Number(blocks[0]);
if (blocks[1]) {
startByte = Number(blocks[1]);
} else {
goog.asserts.assert(previousReference,
'Cannot refer back to previous HLS segment!');
startByte = previousReference.endByte + 1;
}
endByte = startByte + byteLength - 1;
}
return new shaka.media.SegmentReference(
position,
startTime,
endTime,
() => [absoluteSegmentUri],
startByte,
endByte);
};
/** @private */
shaka.hls.HlsParser.prototype.notifySegments_ = function() {
// The presentation timeline may or may not be set yet.
// If it does not yet exist, hold onto the segments until it does.
if (!this.presentationTimeline_) {
return;
}
this.segmentsToNotifyByStream_.forEach((segments) => {
// HLS doesn't have separate periods.
this.presentationTimeline_.notifySegments(segments, /* periodStart */ 0);
});
this.segmentsToNotifyByStream_ = [];
};
/**
* Parses shaka.hls.Segment objects into shaka.media.SegmentReferences.
*
* @param {string} verbatimMediaPlaylistUri
* @param {!shaka.hls.Playlist} playlist
* @param {number} startPosition
* @param {string} mimeType
* @param {string} codecs
* @return {!Promise<!Array.<!shaka.media.SegmentReference>>}
* @private
*/
shaka.hls.HlsParser.prototype.createSegments_ = function(
verbatimMediaPlaylistUri, playlist, startPosition, mimeType, codecs) {
const hlsSegments = playlist.segments;
const references = [];
goog.asserts.assert(hlsSegments.length, 'Playlist should have segments!');
// We may need to look at the media itself to determine a segment start time.
const firstSegmentUri = hlsSegments[0].absoluteUri;
const firstSegmentRef =
this.createSegmentReference_(
playlist,
null /* previousReference */,
hlsSegments[0],
startPosition,
0 /* startTime, irrelevant */);
const initSegmentRef = this.createInitSegmentReference_(playlist);
return this.getStartTime_(verbatimMediaPlaylistUri, initSegmentRef,
firstSegmentRef, mimeType, codecs)
.then(function(firstStartTime) {
shaka.log.debug('First segment', firstSegmentUri.split('/').pop(),
'starts at', firstStartTime);
for (let i = 0; i < hlsSegments.length; ++i) {
let hlsSegment = hlsSegments[i];
let previousReference = references[references.length - 1];
let startTime = (i == 0) ? firstStartTime : previousReference.endTime;
let position = startPosition + i;
let reference = this.createSegmentReference_(
playlist,
previousReference,
hlsSegment,
position,
startTime);
references.push(reference);
}
this.segmentsToNotifyByStream_.push(references);
this.notifySegments_();
return references;
}.bind(this));
};
/**
* Try to fetch a partial segment, and fall back to a full segment if we have
* to.
*
* @param {!shaka.media.AnySegmentReference} segmentRef
* @return {!Promise.<shaka.extern.Response>}
* @throws {shaka.util.Error}
* @private
*/
shaka.hls.HlsParser.prototype.fetchPartialSegment_ = function(segmentRef) {
let networkingEngine = this.playerInterface_.networkingEngine;
const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
let request = shaka.net.NetworkingEngine.makeRequest(
segmentRef.getUris(), this.config_.retryParameters);
// Try to avoid fetching the entire segment, which can be quite large.
let partialSegmentHeaders = {};
let startByte = segmentRef.startByte;
let partialEndByte =
startByte + shaka.hls.HlsParser.PARTIAL_SEGMENT_SIZE_ - 1;
partialSegmentHeaders['Range'] = 'bytes=' + startByte + '-' + partialEndByte;
// Prepare a fallback to the entire segment.
let fullSegmentHeaders = {};
if ((startByte != 0) || (segmentRef.endByte != null)) {
let range = 'bytes=' + startByte + '-';
if (segmentRef.endByte != null) range += segmentRef.endByte;
fullSegmentHeaders['Range'] = range;
}
// Try a partial request first.
request.headers = partialSegmentHeaders;
let operation = networkingEngine.request(requestType, request);
this.operationManager_.manage(operation);
return operation.promise.catch((error) => {
// The partial request may fail for a number of reasons.
// Some servers do not support Range requests, and others do not support
// the OPTIONS request which must be made before any cross-origin Range
// request. Since this fallback is expensive, warn the app developer.
shaka.log.alwaysWarn('Unable to fetch a partial HLS segment! ' +
'Falling back to a full segment request, ' +
'which is expensive! Your server should ' +
'support Range requests and CORS preflights.',
request.uris[0]);
request.headers = fullSegmentHeaders;
let operation = networkingEngine.request(requestType, request);
this.operationManager_.manage(operation);
return operation.promise;
});
};
/**
* Gets the start time of a segment from the existing manifest (if possible) or
* by downloading it and parsing it otherwise.
*
* @param {string} verbatimMediaPlaylistUri
* @param {shaka.media.InitSegmentReference} initSegmentRef
* @param {!shaka.media.SegmentReference} segmentRef
* @param {string} mimeType
* @param {string} codecs
* @return {!Promise.<number>}
* @throws {shaka.util.Error}
* @private
*/
shaka.hls.HlsParser.prototype.getStartTime_ = function(
verbatimMediaPlaylistUri, initSegmentRef, segmentRef, mimeType, codecs) {
// If we are updating the manifest, we can usually skip fetching the segment
// by examining the references we already have. This won't be possible if
// there was some kind of lag or delay updating the manifest on the server,
// in which extreme case we would fall back to fetching a segment. This
// allows us to both avoid fetching segments when possible, and recover from
// certain server-side issues gracefully.
if (this.manifest_) {
const streamInfo = this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
const segmentIndex = streamInfo.segmentIndex;
const reference = segmentIndex.get(segmentRef.position);
if (reference) {
// We found it! Avoid fetching and parsing the segment.
shaka.log.v1('Found segment start time in previous manifest');
return Promise.resolve(reference.startTime);
}
shaka.log.debug('Unable to find segment start time in previous manifest!');
}
// TODO: Introduce a new tag to extend HLS and provide the first segment's
// start time. This will avoid the need for these fetches in content packaged
// with Shaka Packager. This web-friendly extension to HLS can then be
// proposed to Apple for inclusion in a future version of HLS.
// See https://github.com/google/shaka-packager/issues/294
shaka.log.v1('Fetching segment to find start time');
let fetches = [this.fetchPartialSegment_(segmentRef)];
if (mimeType == 'video/mp4' || mimeType == 'audio/mp4') {
// We also need the init segment to get the correct timescale.
if (initSegmentRef) {
fetches.push(this.fetchPartialSegment_(initSegmentRef));
} else {
// If the stream is self-initializing, use the same response for both.
fetches.push(fetches[0]);
}
}
return Promise.all(fetches).then(function(responses) {
if (mimeType == 'video/mp4' || mimeType == 'audio/mp4') {
return this.getStartTimeFromMp4Segment_(
responses[0].data, responses[1].data);
} else if (mimeType == 'audio/mpeg') {
// There is no standard way to embed a timestamp in an mp3 file, so the
// start time is presumably 0.
return 0;
} else if (mimeType == 'video/mp2t') {
return this.getStartTimeFromTsSegment_(responses[0].data);
} else if (mimeType == 'application/mp4' ||
mimeType.startsWith('text/')) {
return this.getStartTimeFromTextSegment_(
mimeType, codecs, responses[0].data);
} else {
// TODO: Parse WebM?
// TODO: Parse raw AAC?
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.HLS_COULD_NOT_PARSE_SEGMENT_START_TIME);
}
}.bind(this));
};
/**
* Parses an mp4 segment to get its start time.
*
* @param {!ArrayBuffer} mediaData
* @param {!ArrayBuffer} initData
* @return {number}
* @throws {shaka.util.Error}
* @private
*/
shaka.hls.HlsParser.prototype.getStartTimeFromMp4Segment_ =
function(mediaData, initData) {
const Mp4Parser = shaka.util.Mp4Parser;
let timescale = 0;
new Mp4Parser()
.box('moov', Mp4Parser.children)
.box('trak', Mp4Parser.children)
.box('mdia', Mp4Parser.children)
.fullBox('mdhd', function(box) {
goog.asserts.assert(
box.version == 0 || box.version == 1,
'MDHD version can only be 0 or 1');
// Skip "creation_time" and "modification_time".
// They are 4 bytes each if the mdhd box is version 0, 8 bytes each if
// it is version 1.
box.reader.skip(box.version == 0 ? 8 : 16);
timescale = box.reader.readUint32();
box.parser.stop();
}).parse(initData, true /* partialOkay */);
if (!timescale) {
shaka.log.error('Unable to find timescale in init segment!');
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.HLS_COULD_NOT_PARSE_SEGMENT_START_TIME);
}
let startTime = 0;
let parsedMedia = false;
new Mp4Parser()
.box('moof', Mp4Parser.children)
.box('traf', Mp4Parser.children)
.fullBox('tfdt', function(box) {
goog.asserts.assert(
box.version == 0 || box.version == 1,
'TFDT version can only be 0 or 1');
let baseTime = (box.version == 0) ?
box.reader.readUint32() :
box.reader.readUint64();
startTime = baseTime / timescale;
parsedMedia = true;
box.parser.stop();
}).parse(mediaData, true /* partialOkay */);
if (!parsedMedia) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.HLS_COULD_NOT_PARSE_SEGMENT_START_TIME);
}
return startTime;
};
/**
* Parses a TS segment to get its start time.
*
* @param {!ArrayBuffer} data
* @return {number}
* @throws {shaka.util.Error}
* @private
*/
shaka.hls.HlsParser.prototype.getStartTimeFromTsSegment_ = function(data) {
let reader = new shaka.util.DataViewReader(
new DataView(data), shaka.util.DataViewReader.Endianness.BIG_ENDIAN);
const fail = function() {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.HLS_COULD_NOT_PARSE_SEGMENT_START_TIME);
};
let packetStart = 0;
let syncByte = 0;
const skipPacket = function() {
// 188-byte packets are standard, so assume that.
reader.seek(packetStart + 188);
syncByte = reader.readUint8();
if (syncByte != 0x47) {
// We haven't found the sync byte, so try it as a 192-byte packet.
reader.seek(packetStart + 192);
syncByte = reader.readUint8();
}
if (syncByte != 0x47) {
// We still haven't found the sync byte, so try as a 204-byte packet.
reader.seek(packetStart + 204);
syncByte = reader.readUint8();
}
if (syncByte != 0x47) {
// We still haven't found the sync byte, so the packet was of a
// non-standard size.
fail();
}
// Put the sync byte back so we can read it in the next loop.
reader.rewind(1);
};
// eslint-disable-next-line no-constant-condition
while (true) {
// Format reference: https://bit.ly/TsPacket
packetStart = reader.getPosition();
syncByte = reader.readUint8();
if (syncByte != 0x47) fail();
let flagsAndPacketId = reader.readUint16();
let hasPesPacket = flagsAndPacketId & 0x4000;
if (!hasPesPacket) fail();
let flags = reader.readUint8();
let adaptationFieldControl = (flags & 0x30) >> 4;
if (adaptationFieldControl == 0 /* reserved */ ||
adaptationFieldControl == 2 /* adaptation field, no payload */) {
fail();
}
if (adaptationFieldControl == 3) {
// Skip over adaptation field.
let length = reader.readUint8();
reader.skip(length);
}
// Now we come to the PES header (hopefully).
// Format reference: https://bit.ly/TsPES
let startCode = reader.readUint32();
let startCodePrefix = startCode >> 8;
if (startCodePrefix != 1) {
// Not a PES packet yet. Skip this TS packet and try again.
skipPacket();
continue;
}
// Skip the 16-bit PES length and the first 8 bits of the optional header.
reader.skip(3);
// The next 8 bits contain flags about DTS & PTS.
let ptsDtsIndicator = reader.readUint8() >> 6;
if (ptsDtsIndicator == 0 /* no timestamp */ ||
ptsDtsIndicator == 1 /* forbidden */) {
fail();
}
let pesHeaderLengthRemaining = reader.readUint8();
if (pesHeaderLengthRemaining == 0) {
fail();
}
if (ptsDtsIndicator == 2 /* PTS only */) {
goog.asserts.assert(pesHeaderLengthRemaining == 5, 'Bad PES header?');
} else if (ptsDtsIndicator == 3 /* PTS and DTS */) {
goog.asserts.assert(pesHeaderLengthRemaining == 10, 'Bad PES header?');
}
let pts0 = reader.readUint8();
let pts1 = reader.readUint16();
let pts2 = reader.readUint16();
// Reconstruct 33-bit PTS from the 5-byte, padded structure.
let ptsHigh3 = (pts0 & 0x0e) >> 1;
let ptsLow30 = ((pts1 & 0xfffe) << 14) | ((pts2 & 0xfffe) >> 1);
// Reconstruct the PTS as a float. Avoid bitwise operations to combine
// because bitwise ops treat the values as 32-bit ints.
let pts = ptsHigh3 * (1 << 30) + ptsLow30;
return pts / shaka.hls.HlsParser.TS_TIMESCALE_;
}
};
/**
* Parses a text segment to get its start time.
*
* @param {string} mimeType
* @param {string} codecs
* @param {!ArrayBuffer} data
* @return {number}
* @throws {shaka.util.Error}
* @private
*/
shaka.hls.HlsParser.prototype.getStartTimeFromTextSegment_ =
function(mimeType, codecs, data) {
let fullMimeType = shaka.util.MimeUtils.getFullType(mimeType, codecs);
if (!shaka.text.TextEngine.isTypeSupported(fullMimeType)) {
// We won't be able to parse this, but it will be filtered out anyway.
// So we don't have to care about the start time.
return 0;
}
let textEngine = new shaka.text.TextEngine(/* displayer */ null);
textEngine.initParser(fullMimeType);
return textEngine.getStartTime(data);
};
/**
* Attempts to guess which codecs from the codecs list belong to a given content
* type. Does not assume a single codec is anything special, and does not throw
* if it fails to match.
*
* @param {string} contentType
* @param {!Array.<string>} codecs
* @return {?string} or null if no match is found
* @private
*/
shaka.hls.HlsParser.prototype.guessCodecsSafe_ = function(contentType, codecs) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
const HlsParser = shaka.hls.HlsParser;
let formats = HlsParser.CODEC_REGEXPS_BY_CONTENT_TYPE_[contentType];
for (let i = 0; i < formats.length; i++) {
for (let j = 0; j < codecs.length; j++) {
if (formats[i].test(codecs[j].trim())) {
return codecs[j].trim();
}
}
}
// Text does not require a codec string.
if (contentType == ContentType.TEXT) {
return '';
}
return null;
};
/**
* Attempts to guess which codecs from the codecs list belong to a given content
* type. Assumes that at least one codec is correct, and throws if none are.
*
* @param {string} contentType
* @param {!Array.<string>} codecs
* @return {string}
* @private
* @throws {shaka.util.Error}
*/
shaka.hls.HlsParser.prototype.guessCodecs_ = function(contentType, codecs) {
if (codecs.length == 1) {
return codecs[0];
}
let match = this.guessCodecsSafe_(contentType, codecs);
// A failure is specifically denoted by null; an empty string represents a
// valid match of no codec.
if (match != null) {
return match;
}
// Unable to guess codecs.
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.HLS_COULD_NOT_GUESS_CODECS,
codecs);
};
/**
* Attempts to guess stream's mime type based on content type and URI.
*
* @param {string} contentType
* @param {string} codecs
* @param {!shaka.hls.Playlist} playlist
* @return {!Promise.<string>}
* @private
* @throws {shaka.util.Error}
*/
shaka.hls.HlsParser.prototype.guessMimeType_ =
function(contentType, codecs, playlist) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
const HlsParser = shaka.hls.HlsParser;
goog.asserts.assert(playlist.segments.length,
'Playlist should have segments!');
const firstSegmentUri = playlist.segments[0].absoluteUri;
let parsedUri = new goog.Uri(firstSegmentUri);
let extension = parsedUri.getPath().split('.').pop();
let map = HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_[contentType];
let mimeType = map[extension];
if (mimeType) {
return Promise.resolve(mimeType);
}
if (contentType == ContentType.TEXT) {
// The extension map didn't work.
if (!codecs || codecs == 'vtt') {
// If codecs is 'vtt', it's WebVTT.
// If there was no codecs string, assume HLS text streams are WebVTT.
return Promise.resolve('text/vtt');
} else {
// Otherwise, assume MP4-embedded text, since text-based formats tend not
// to have a codecs string at all.
return Promise.resolve('application/mp4');
}
}
// If unable to guess mime type, request a segment and try getting it
// from the response.
let headRequest = shaka.net.NetworkingEngine.makeRequest(
[firstSegmentUri], this.config_.retryParameters);
headRequest.method = 'HEAD';
const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
let networkingEngine = this.playerInterface_.networkingEngine;
let operation = networkingEngine.request(requestType, headRequest);
this.operationManager_.manage(operation);
return operation.promise.then((response) => {
let mimeType = response.headers['content-type'];
if (!mimeType) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.HLS_COULD_NOT_GUESS_MIME_TYPE,
extension);
}
// Split the MIME type in case the server sent additional parameters.
return mimeType.split(';')[0];
});
};
/**
* Find the attribute and returns its value.
* Throws an error if attribute was not found.
*
* @param {shaka.hls.Tag} tag
* @param {string} attributeName
* @return {string}
* @private
* @throws {shaka.util.Error}
*/
shaka.hls.HlsParser.getRequiredAttributeValue_ = function(tag, attributeName) {
let attribute = tag.getAttribute(attributeName);
if (!attribute) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.HLS_REQUIRED_ATTRIBUTE_MISSING,
attributeName);
}
return attribute.value;
};
/**
* Returns a tag with a given name.
* Throws an error if tag was not found.
*
* @param {!Array.<shaka.hls.Tag>} tags
* @param {string} tagName
* @return {!shaka.hls.Tag}
* @private
* @throws {shaka.util.Error}
*/
shaka.hls.HlsParser.prototype.getRequiredTag_ = function(tags, tagName) {
const Utils = shaka.hls.Utils;
let tag = Utils.getFirstTagWithName(tags, tagName);
if (!tag) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.HLS_REQUIRED_TAG_MISSING, tagName);
}
return tag;
};
/**
* @param {shaka.extern.Stream} stream
* @param {?string} width
* @param {?string} height
* @param {?string} frameRate
* @private
*/
shaka.hls.HlsParser.prototype.addVideoAttributes_ =
function(stream, width, height, frameRate) {
if (stream) {
stream.width = Number(width) || undefined;
stream.height = Number(height) || undefined;
stream.frameRate = Number(frameRate) || undefined;
}
};
/**
* Makes a network request for the manifest and returns a Promise
* with the resulting data.
*
* @param {string} absoluteUri
* @return {!Promise.<!shaka.extern.Response>}
* @private
*/
shaka.hls.HlsParser.prototype.requestManifest_ = function(absoluteUri) {
const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
let request = shaka.net.NetworkingEngine.makeRequest(
[absoluteUri], this.config_.retryParameters);
let networkingEngine = this.playerInterface_.networkingEngine;
let operation = networkingEngine.request(requestType, request);
this.operationManager_.manage(operation);
return operation.promise;
};
/**
* A list of regexps to detect well-known video codecs.
*
* @const {!Array.<!RegExp>}
* @private
*/
shaka.hls.HlsParser.VIDEO_CODEC_REGEXPS_ = [
/^avc/,
/^hev/,
/^hvc/,
/^vp0?[89]/,
/^av1$/,
];
/**
* A list of regexps to detect well-known audio codecs.
*
* @const {!Array.<!RegExp>}
* @private
*/
shaka.hls.HlsParser.AUDIO_CODEC_REGEXPS_ = [
/^vorbis$/,
/^opus$/,
/^flac$/,
/^mp4a/,
/^[ae]c-3$/,
];
/**
* A list of regexps to detect well-known text codecs.
*
* @const {!Array.<!RegExp>}
* @private
*/
shaka.hls.HlsParser.TEXT_CODEC_REGEXPS_ = [
/^vtt$/,
/^wvtt/,
/^stpp/,
];
/**
* @const {!Object.<string, !Array.<!RegExp>>}
* @private
*/
shaka.hls.HlsParser.CODEC_REGEXPS_BY_CONTENT_TYPE_ = {
'audio': shaka.hls.HlsParser.AUDIO_CODEC_REGEXPS_,
'video': shaka.hls.HlsParser.VIDEO_CODEC_REGEXPS_,
'text': shaka.hls.HlsParser.TEXT_CODEC_REGEXPS_,
};
/**
* @const {!Object.<string, string>}
* @private
*/
shaka.hls.HlsParser.AUDIO_EXTENSIONS_TO_MIME_TYPES_ = {
'mp4': 'audio/mp4',
'm4s': 'audio/mp4',
'm4i': 'audio/mp4',
'm4a': 'audio/mp4',
// MPEG2-TS also uses video/ for audio: https://bit.ly/TsMse
'ts': 'video/mp2t',
};
/**
* @const {!Object.<string, string>}
* @private
*/
shaka.hls.HlsParser.VIDEO_EXTENSIONS_TO_MIME_TYPES_ = {
'mp4': 'video/mp4',
'm4s': 'video/mp4',
'm4i': 'video/mp4',
'm4v': 'video/mp4',
'ts': 'video/mp2t',
};
/**
* @const {!Object.<string, string>}
* @private
*/
shaka.hls.HlsParser.TEXT_EXTENSIONS_TO_MIME_TYPES_ = {
'mp4': 'application/mp4',
'm4s': 'application/mp4',
'm4i': 'application/mp4',
'vtt': 'text/vtt',
'ttml': 'application/ttml+xml',
};
/**
* @const {!Object.<string, !Object.<string, string>>}
* @private
*/
shaka.hls.HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_ = {
'audio': shaka.hls.HlsParser.AUDIO_EXTENSIONS_TO_MIME_TYPES_,
'video': shaka.hls.HlsParser.VIDEO_EXTENSIONS_TO_MIME_TYPES_,
'text': shaka.hls.HlsParser.TEXT_EXTENSIONS_TO_MIME_TYPES_,
};
/**
* @typedef {function(!shaka.hls.Tag):?shaka.extern.DrmInfo}
* @private
*/
shaka.hls.HlsParser.DrmParser_;
/**
* @param {!shaka.hls.Tag} drmTag
* @return {?shaka.extern.DrmInfo}
* @private
*/
shaka.hls.HlsParser.widevineDrmParser_ = function(drmTag) {
const HlsParser = shaka.hls.HlsParser;
let method = HlsParser.getRequiredAttributeValue_(drmTag, 'METHOD');
shaka.Deprecate.deprecateFeature(
2, 6,
'HLS SAMPLE-AES-CENC',
'SAMPLE-AES-CENC will no longer be supported, see Issue #1227');
const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR', 'SAMPLE-AES-CENC'];
if (!VALID_METHODS.includes(method)) {
shaka.log.error('Widevine in HLS is only supported with [',
VALID_METHODS.join(', '), '], not', method);
return null;
}
let uri = HlsParser.getRequiredAttributeValue_(drmTag, 'URI');
let parsedData = shaka.net.DataUriPlugin.parse(uri);
// The data encoded in the URI is a PSSH box to be used as init data.
let pssh = new Uint8Array(parsedData.data);
let drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
'com.widevine.alpha', [
{initDataType: 'cenc', initData: pssh},
]);
let keyId = drmTag.getAttributeValue('KEYID');
if (keyId) {
// This value should begin with '0x':
goog.asserts.assert(keyId.startsWith('0x'), 'Incorrect KEYID format!');
// But the output should not contain the '0x':
drmInfo.keyIds = [keyId.substr(2).toLowerCase()];
}
return drmInfo;
};
/**
* Called when the update timer ticks.
*
* @private
*/
shaka.hls.HlsParser.prototype.onUpdate_ = function() {
goog.asserts.assert(this.updateTimer_, 'Should only be called by timer');
goog.asserts.assert(this.updatePeriod_ != null,
'There should be an update period');
shaka.log.info('Updating manifest...');
// Detect a call to stop()
if (!this.playerInterface_) {
return;
}
this.updateTimer_ = null;
this.update().then(function() {
this.setUpdateTimer_(this.updatePeriod_);
}.bind(this)).catch(function(error) {
goog.asserts.assert(error instanceof shaka.util.Error,
'Should only receive a Shaka error');
// Try updating again, but ensure we haven't been destroyed.
if (this.playerInterface_) {
// We will retry updating, so override the severity of the error.
error.severity = shaka.util.Error.Severity.RECOVERABLE;
this.playerInterface_.onError(error);
this.setUpdateTimer_(0);
}
}.bind(this));
};
/**
* Sets the update timer.
*
* @param {?number} time in seconds
* @private
*/
shaka.hls.HlsParser.prototype.setUpdateTimer_ = function(time) {
if (this.updatePeriod_ == null || time == null) {
return;
}
goog.asserts.assert(this.updateTimer_ == null,
'Timer should not be already set');
let callback = this.onUpdate_.bind(this);
this.updateTimer_ = window.setTimeout(callback, time * 1000);
};
/**
* @return {boolean}
* @private
*/
shaka.hls.HlsParser.prototype.isLive_ = function() {
const PresentationType = shaka.hls.HlsParser.PresentationType_;
return this.presentationType_ != PresentationType.VOD;
};
/**
* @param {shaka.hls.HlsParser.PresentationType_} type
* @private
*/
shaka.hls.HlsParser.prototype.setPresentationType_ = function(type) {
this.presentationType_ = type;
if (this.presentationTimeline_) {
this.presentationTimeline_.setStatic(!this.isLive_());
}
if (!this.isLive_()) {
if (this.updateTimer_ != null) {
window.clearTimeout(this.updateTimer_);
this.updateTimer_ = null;
this.updatePeriod_ = null;
}
}
};
/**
* @const {!Object.<string, shaka.hls.HlsParser.DrmParser_>}
* @private
*/
shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_ = {
/* TODO: https://github.com/google/shaka-player/issues/382
'com.apple.streamingkeydelivery':
shaka.hls.HlsParser.fairplayDrmParser_,
*/
'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed':
shaka.hls.HlsParser.widevineDrmParser_,
};
/**
* @enum {string}
* @private
*/
shaka.hls.HlsParser.PresentationType_ = {
VOD: 'VOD',
EVENT: 'EVENT',
LIVE: 'LIVE',
};
/**
* @const {number}
* @private
*/
shaka.hls.HlsParser.TS_TIMESCALE_ = 90000;
/**
* At this value, timestamps roll over in TS content.
* @const {number}
* @private
*/
shaka.hls.HlsParser.TS_ROLLOVER_ = 0x200000000;
/**
* The amount of data from the start of a segment we will try to fetch when we
* need to know the segment start time. This allows us to avoid fetching the
* entire segment in many cases.
*
* @const {number}
* @private
*/
shaka.hls.HlsParser.PARTIAL_SEGMENT_SIZE_ = 2048;
shaka.media.ManifestParser.registerParserByExtension(
'm3u8', shaka.hls.HlsParser);
shaka.media.ManifestParser.registerParserByMime(
'application/x-mpegurl', shaka.hls.HlsParser);
shaka.media.ManifestParser.registerParserByMime(
'application/vnd.apple.mpegurl', shaka.hls.HlsParser);