mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-25 17:45:03 +03:00
2e5b856f79
Closes #1189 Change-Id: Iae6f4f4524bb9895f8b211491793cf7626661a5d
1980 lines
63 KiB
JavaScript
1980 lines
63 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.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.DataViewReader');
|
|
goog.require('shaka.util.Error');
|
|
goog.require('shaka.util.Functional');
|
|
goog.require('shaka.util.ManifestParserUtils');
|
|
goog.require('shaka.util.MimeUtils');
|
|
|
|
|
|
|
|
/**
|
|
* Creates a new HLS parser.
|
|
*
|
|
* @struct
|
|
* @constructor
|
|
* @implements {shakaExtern.ManifestParser}
|
|
* @export
|
|
*/
|
|
shaka.hls.HlsParser = function() {
|
|
/** @private {?shakaExtern.ManifestParser.PlayerInterface} */
|
|
this.playerInterface_ = null;
|
|
|
|
/** @private {?shakaExtern.ManifestConfiguration} */
|
|
this.config_ = null;
|
|
|
|
/** @private {number} */
|
|
this.globalId_ = 1;
|
|
|
|
/**
|
|
* TODO: this is now only used for text codec detection, try to remove
|
|
* @private {!Object.<number, shaka.hls.HlsParser.StreamInfo>}
|
|
*/
|
|
this.mediaTagsToStreamInfosMap_ = {};
|
|
|
|
/**
|
|
* The key is a string of the form "<VIDEO URI> - <AUDIO URI>".
|
|
* TODO: should use original, resolved URIs, before redirects
|
|
* @private {!Object.<string, shakaExtern.Variant>}
|
|
*/
|
|
this.urisToVariantsMap_ = {};
|
|
|
|
/** @private {!Object.<number, !shaka.media.SegmentIndex>} */
|
|
this.streamsToIndexMap_ = {};
|
|
|
|
/**
|
|
* A map from media playlists' uris to stream infos
|
|
* representing the playlists.
|
|
* TODO: should use original, resolved URIs, before redirects
|
|
* @private {!Object.<string, shaka.hls.HlsParser.StreamInfo>}
|
|
*/
|
|
this.uriToStreamInfosMap_ = {};
|
|
|
|
/** @private {?shaka.media.PresentationTimeline} */
|
|
this.presentationTimeline_ = null;
|
|
|
|
/**
|
|
* TODO: should be resolved, post-redirect URI, so that media playlist URIs
|
|
* respect master playlist redirects
|
|
* @private {string}
|
|
*/
|
|
this.manifestUri_ = '';
|
|
|
|
/** @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 {?shakaExtern.Manifest} */
|
|
this.manifest_ = null;
|
|
|
|
/** @private {number} */
|
|
this.maxTargetDuration_ = 0;
|
|
|
|
/** @private {number} */
|
|
this.minTargetDuration_ = Infinity;
|
|
};
|
|
|
|
|
|
/**
|
|
* @typedef {{
|
|
* stream: !shakaExtern.Stream,
|
|
* segmentIndex: !shaka.media.SegmentIndex,
|
|
* drmInfos: !Array.<shakaExtern.DrmInfo>,
|
|
* relativeUri: string,
|
|
* minTimestamp: number,
|
|
* duration: number
|
|
* }}
|
|
*
|
|
* @description
|
|
* Contains a stream and information about it.
|
|
*
|
|
* @property {!shakaExtern.Stream} stream
|
|
* The Stream itself.
|
|
* @property {!shaka.media.SegmentIndex} segmentIndex
|
|
* SegmentIndex of the stream.
|
|
* @property {!Array.<shakaExtern.DrmInfo>} drmInfos
|
|
* DrmInfos of the stream. There may be multiple for multi-DRM content.
|
|
* @property {string} relativeUri
|
|
* The uri associated with the stream, relative to the manifest.
|
|
* @property {number} minTimestamp
|
|
* The minimum timestamp found in the stream. Used for VOD only.
|
|
* @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;
|
|
this.manifestUri_ = uri;
|
|
return this.requestManifest_(uri).then(function(response) {
|
|
return this.parseManifest_(response.data, uri).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_ = {};
|
|
this.urisToVariantsMap_ = {};
|
|
this.manifest_ = null;
|
|
|
|
return Promise.resolve();
|
|
};
|
|
|
|
|
|
/**
|
|
* @override
|
|
* @exportInterface
|
|
*/
|
|
shaka.hls.HlsParser.prototype.update = function() {
|
|
if (!this.isLive_())
|
|
return;
|
|
|
|
var promises = [];
|
|
for (var uri in this.uriToStreamInfosMap_) {
|
|
var streamInfo = this.uriToStreamInfosMap_[uri];
|
|
|
|
promises.push(this.updateStream_(streamInfo, uri));
|
|
}
|
|
|
|
return Promise.all(promises);
|
|
};
|
|
|
|
|
|
/**
|
|
* Updates a stream.
|
|
*
|
|
* @param {!shaka.hls.HlsParser.StreamInfo} streamInfo
|
|
* @param {string} uri
|
|
* @throws shaka.util.Error
|
|
* @private
|
|
*/
|
|
shaka.hls.HlsParser.prototype.updateStream_ = function(streamInfo, uri) {
|
|
this.requestManifest_(uri).then(function(response) {
|
|
var Utils = shaka.hls.Utils;
|
|
var PresentationType = shaka.hls.HlsParser.PresentationType_;
|
|
var playlist = this.manifestTextParser_.parsePlaylist(response.data, 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);
|
|
}
|
|
|
|
var mediaSequenceTag = Utils.getFirstTagWithName(playlist.tags,
|
|
'EXT-X-MEDIA-SEQUENCE');
|
|
|
|
var startPosition = mediaSequenceTag ? Number(mediaSequenceTag.value) : 0;
|
|
var stream = streamInfo.stream;
|
|
this.createSegments_(playlist, startPosition,
|
|
stream.mimeType, stream.codecs)
|
|
.then(function(segments) {
|
|
streamInfo.segmentIndex.replace(segments);
|
|
|
|
var 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.
|
|
var 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
|
|
* @param {string} uri
|
|
* @throws shaka.util.Error When there is a parsing error.
|
|
* @return {!Promise}
|
|
* @private
|
|
*/
|
|
shaka.hls.HlsParser.prototype.parseManifest_ = function(data, uri) {
|
|
var playlist = this.manifestTextParser_.parsePlaylist(data, uri);
|
|
|
|
// We don't support directly providing a Media Playlist.
|
|
// See 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]);
|
|
|
|
if (this.isLive_()) {
|
|
// 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.
|
|
var threeSegmentDurations = this.maxTargetDuration_ * 3;
|
|
this.presentationTimeline_.setDelay(threeSegmentDurations);
|
|
|
|
// 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, 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.
|
|
var PresentationType = shaka.hls.HlsParser.PresentationType_;
|
|
if (this.presentationType_ == PresentationType.LIVE) {
|
|
this.presentationTimeline_.setSegmentAvailabilityDuration(
|
|
threeSegmentDurations);
|
|
}
|
|
} else {
|
|
// For VOD/EVENT content, offset everything back to 0.
|
|
// Find the minimum timestamp in all streams, and use that as the
|
|
// presentationTimeOffset for all streams.
|
|
// Find the minimum duration, and use that as the presentation duration.
|
|
var minFirstTimestamp = Infinity;
|
|
var minDuration = Infinity;
|
|
|
|
for (var uri in this.uriToStreamInfosMap_) {
|
|
var streamInfo = this.uriToStreamInfosMap_[uri];
|
|
minFirstTimestamp =
|
|
Math.min(minFirstTimestamp, streamInfo.minTimestamp);
|
|
minDuration = Math.min(minDuration, streamInfo.duration);
|
|
}
|
|
|
|
this.presentationTimeline_.setDuration(minDuration);
|
|
|
|
for (var uri in this.uriToStreamInfosMap_) {
|
|
var streamInfo = this.uriToStreamInfosMap_[uri];
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
goog.asserts.assert(this.presentationTimeline_ != null,
|
|
'presentationTimeline should already be created!');
|
|
|
|
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.<!shakaExtern.Period>}
|
|
* @private
|
|
*/
|
|
shaka.hls.HlsParser.prototype.createPeriod_ = function(playlist) {
|
|
var Utils = shaka.hls.Utils;
|
|
var Functional = shaka.util.Functional;
|
|
var tags = playlist.tags;
|
|
|
|
var mediaTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-MEDIA');
|
|
var textStreamTags = mediaTags.filter(function(tag) {
|
|
var type = shaka.hls.HlsParser.getRequiredAttributeValue_(tag, 'TYPE');
|
|
return type == 'SUBTITLES';
|
|
}.bind(this));
|
|
|
|
// TODO: CLOSED-CAPTIONS requires the parsing of CEA-608 from the video.
|
|
var textStreamPromises = textStreamTags.map(function(tag) {
|
|
return this.createTextStream_(tag, playlist);
|
|
}.bind(this));
|
|
|
|
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.
|
|
var variantTags = Utils.filterTagsByName(tags, 'EXT-X-STREAM-INF');
|
|
var variantsPromises = variantTags.map(function(tag) {
|
|
return this.createVariantsForTag_(tag, playlist);
|
|
}.bind(this));
|
|
|
|
return Promise.all(variantsPromises).then(function(allVariants) {
|
|
var 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.<!shakaExtern.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!');
|
|
var ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
var HlsParser = shaka.hls.HlsParser;
|
|
var 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.
|
|
var defaultCodecs = 'avc1.42E01E,mp4a.40.2';
|
|
|
|
/** @type {!Array.<string>} */
|
|
var codecs = tag.getAttributeValue('CODECS', defaultCodecs).split(',');
|
|
var resolutionAttr = tag.getAttribute('RESOLUTION');
|
|
var width = null;
|
|
var height = null;
|
|
var frameRate = tag.getAttributeValue('FRAME-RATE');
|
|
var bandwidth =
|
|
Number(HlsParser.getRequiredAttributeValue_(tag, 'BANDWIDTH'));
|
|
|
|
if (resolutionAttr) {
|
|
var 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.
|
|
var mediaTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-MEDIA');
|
|
|
|
var audioGroupId = tag.getAttributeValue('AUDIO');
|
|
var 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.
|
|
var textCodecs = this.guessCodecsSafe_(ContentType.TEXT, codecs);
|
|
if (textCodecs) {
|
|
// We found a text codec in the list, so look for an associated text stream.
|
|
var subGroupId = tag.getAttributeValue('SUBTITLES');
|
|
if (subGroupId) {
|
|
var 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.
|
|
var textStreamInfo = this.mediaTagsToStreamInfosMap_[textTags[0].id];
|
|
textStreamInfo.stream.codecs = textCodecs;
|
|
}
|
|
}
|
|
|
|
// Remove this entry from the list of codecs that belong to audio/video.
|
|
codecs.splice(codecs.indexOf(textCodecs), 1);
|
|
}
|
|
|
|
var promises = mediaTags.map(function(tag) {
|
|
return this.createStreamInfoFromMediaTag_(tag, codecs);
|
|
}.bind(this));
|
|
|
|
var audioStreamInfos = [];
|
|
var 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());
|
|
var type;
|
|
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.
|
|
var ignoreStream = false;
|
|
|
|
if (codecs.length == 1) {
|
|
// There is only one codec, so it shouldn't be multiplexed.
|
|
|
|
var 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) {
|
|
var streamURI = HlsParser.getRequiredAttributeValue_(tag, 'URI');
|
|
var firstAudioStreamURI = audioStreamInfos[0].relativeUri;
|
|
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!');
|
|
|
|
return this.createVariants_(
|
|
audioStreamInfos,
|
|
videoStreamInfos,
|
|
bandwidth,
|
|
width,
|
|
height,
|
|
frameRate);
|
|
}.bind(this));
|
|
};
|
|
|
|
|
|
/**
|
|
* @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.<!shakaExtern.Variant>}
|
|
* @private
|
|
*/
|
|
shaka.hls.HlsParser.prototype.createVariants_ =
|
|
function(audioInfos, videoInfos, bandwidth, width, height, frameRate) {
|
|
var 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];
|
|
|
|
var variants = [];
|
|
for (var i = 0; i < audioInfos.length; i++) {
|
|
for (var j = 0; j < videoInfos.length; j++) {
|
|
var audioStream = audioInfos[i] ? audioInfos[i].stream : null;
|
|
var videoStream = videoInfos[j] ? videoInfos[j].stream : null;
|
|
var audioDrmInfos = audioInfos[i] ? audioInfos[i].drmInfos : null;
|
|
var videoDrmInfos = videoInfos[j] ? videoInfos[j].drmInfos : null;
|
|
|
|
var 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;
|
|
}
|
|
|
|
var videoStreamUri = videoInfos[i] ? videoInfos[i].relativeUri : '';
|
|
var audioStreamUri = audioInfos[i] ? audioInfos[i].relativeUri : '';
|
|
var variantMapKey = videoStreamUri + ' - ' + audioStreamUri;
|
|
if (this.urisToVariantsMap_[variantMapKey]) {
|
|
// This happens when two variants only differ in their text streams.
|
|
shaka.log.debug('Skipping variant which only differs in text streams.');
|
|
continue;
|
|
}
|
|
|
|
var variant = this.createVariant_(
|
|
audioStream, videoStream, bandwidth, drmInfos);
|
|
variants.push(variant);
|
|
this.urisToVariantsMap_[variantMapKey] = variant;
|
|
}
|
|
}
|
|
return variants;
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {shakaExtern.Stream} audio
|
|
* @param {shakaExtern.Stream} video
|
|
* @param {number} bandwidth
|
|
* @param {!Array.<shakaExtern.DrmInfo>} drmInfos
|
|
* @return {!shakaExtern.Variant}
|
|
* @private
|
|
*/
|
|
shaka.hls.HlsParser.prototype.createVariant_ =
|
|
function(audio, video, bandwidth, drmInfos) {
|
|
var 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.<?shakaExtern.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!');
|
|
|
|
var 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;
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* 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!');
|
|
|
|
var HlsParser = shaka.hls.HlsParser;
|
|
var uri = HlsParser.getRequiredAttributeValue_(tag, 'URI');
|
|
uri = shaka.hls.Utils.constructAbsoluteUri(this.manifestUri_, uri);
|
|
|
|
// Check if the stream has already been created as part of another Variant
|
|
// and return it if it has.
|
|
if (this.uriToStreamInfosMap_[uri]) {
|
|
return Promise.resolve(this.uriToStreamInfosMap_[uri]);
|
|
}
|
|
|
|
var type = HlsParser.getRequiredAttributeValue_(tag, 'TYPE').toLowerCase();
|
|
// Shaka recognizes content types 'audio', 'video' and 'text'.
|
|
// HLS 'subtitles' type needs to be mapped to 'text'.
|
|
var ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
if (type == 'subtitles') type = ContentType.TEXT;
|
|
|
|
var LanguageUtils = shaka.util.LanguageUtils;
|
|
var language = LanguageUtils.normalize(/** @type {string} */(
|
|
tag.getAttributeValue('LANGUAGE', 'und')));
|
|
var label = tag.getAttributeValue('NAME');
|
|
|
|
var defaultAttr = tag.getAttribute('DEFAULT');
|
|
var autoselectAttr = tag.getAttribute('AUTOSELECT');
|
|
// TODO: Should we take into account some of the currently ignored attributes:
|
|
// FORCED, INSTREAM-ID, CHARACTERISTICS, CHANNELS?
|
|
// Attribute descriptions:
|
|
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-20#section-4.3.4.1
|
|
var channelsAttribute = tag.getAttributeValue('CHANNELS');
|
|
var channelsCount = type == 'audio' ?
|
|
this.getChannelsCount_(channelsAttribute) : null;
|
|
var primary = !!defaultAttr || !!autoselectAttr;
|
|
return this.createStreamInfo_(uri, allCodecs, type,
|
|
language, primary, label, channelsCount).then(function(streamInfo) {
|
|
|
|
// TODO: This check is necessary because of the possibility of multiple
|
|
// calls to createStreamInfoFromMediaTag_ before either has resolved.
|
|
if (this.uriToStreamInfosMap_[uri])
|
|
return this.uriToStreamInfosMap_[uri];
|
|
|
|
this.mediaTagsToStreamInfosMap_[tag.id] = streamInfo;
|
|
this.uriToStreamInfosMap_[uri] = streamInfo;
|
|
return streamInfo;
|
|
}.bind(this));
|
|
};
|
|
|
|
|
|
/**
|
|
* Get the channels count information for HLS audio track.
|
|
* The channels value is a string that specifies an ordered, "/" separated list
|
|
* of parameters. If the type is audio, the first parameter will be a decimal
|
|
* integer, as the number of independent, simultaneous audio channels.
|
|
* No other channels parameters are currently defined.
|
|
*
|
|
* @param {?string} channels
|
|
*
|
|
* @return {?number} channelcount
|
|
* @private
|
|
*/
|
|
shaka.hls.HlsParser.prototype.getChannelsCount_ = function(channels) {
|
|
if (!channels) return null;
|
|
var channelscountstring = channels.split('/')[0];
|
|
var count = parseInt(channelscountstring, 10);
|
|
return count;
|
|
};
|
|
|
|
|
|
/**
|
|
* Parse 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!');
|
|
|
|
var uri = shaka.hls.HlsParser.getRequiredAttributeValue_(tag, 'URI');
|
|
uri = shaka.hls.Utils.constructAbsoluteUri(this.manifestUri_, uri);
|
|
|
|
if (this.uriToStreamInfosMap_[uri]) {
|
|
return Promise.resolve(this.uriToStreamInfosMap_[uri]);
|
|
}
|
|
|
|
return this.createStreamInfo_(uri, allCodecs, type,
|
|
/* language */ 'und', /* primary */ false,
|
|
/* label */ null, /* channelcount */ null).then(
|
|
function(streamInfo) {
|
|
// TODO: This check is necessary because of the possibility of multiple
|
|
// calls to createStreamInfoFromVariantTag_ before either has resolved.
|
|
if (this.uriToStreamInfosMap_[uri])
|
|
return this.uriToStreamInfosMap_[uri];
|
|
|
|
this.uriToStreamInfosMap_[uri] = streamInfo;
|
|
return streamInfo;
|
|
}.bind(this));
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {!string} uri
|
|
* @param {!Array.<!string>} allCodecs
|
|
* @param {!string} type
|
|
* @param {!string} language
|
|
* @param {boolean} primary
|
|
* @param {?string} label
|
|
* @param {?number} channelsCount
|
|
* @return {!Promise.<shaka.hls.HlsParser.StreamInfo>}
|
|
* @throws shaka.util.Error
|
|
* @private
|
|
*/
|
|
shaka.hls.HlsParser.prototype.createStreamInfo_ = function(uri, allCodecs,
|
|
type, language, primary, label, channelsCount) {
|
|
var Utils = shaka.hls.Utils;
|
|
var ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
var HlsParser = shaka.hls.HlsParser;
|
|
|
|
var relativeUri = uri;
|
|
uri = Utils.constructAbsoluteUri(this.manifestUri_, uri);
|
|
|
|
/** @type {!shaka.hls.Playlist} */
|
|
var playlist;
|
|
/** @type {string} */
|
|
var codecs = '';
|
|
/** @type {string} */
|
|
var mimeType;
|
|
|
|
return this.requestManifest_(uri).then(function(response) {
|
|
playlist = this.manifestTextParser_.parsePlaylist(response.data, uri);
|
|
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);
|
|
}.bind(this)).then(function(mimeTypeArg) {
|
|
mimeType = mimeTypeArg;
|
|
|
|
var mediaSequenceTag = Utils.getFirstTagWithName(playlist.tags,
|
|
'EXT-X-MEDIA-SEQUENCE');
|
|
|
|
var startPosition = mediaSequenceTag ? Number(mediaSequenceTag.value) : 0;
|
|
|
|
return this.createSegments_(playlist, startPosition, mimeType, codecs);
|
|
}.bind(this)).then(function(segments) {
|
|
var minTimestamp = segments[0].startTime;
|
|
var lastEndTime = segments[segments.length - 1].endTime;
|
|
var duration = lastEndTime - minTimestamp;
|
|
var segmentIndex = new shaka.media.SegmentIndex(segments);
|
|
|
|
if (!this.presentationTimeline_) {
|
|
// The presentation started last available segment's end time ago.
|
|
// All variants should be in sync in terms of timeline, so just grab
|
|
// this from an arbitrary stream.
|
|
this.createPresentationTimeline_(lastEndTime);
|
|
}
|
|
|
|
var initSegmentReference = null;
|
|
|
|
if (type != ContentType.TEXT) {
|
|
initSegmentReference = this.createInitSegmentReference_(playlist);
|
|
}
|
|
|
|
this.presentationTimeline_.notifySegments(0, segments);
|
|
|
|
var kind = undefined;
|
|
var ManifestParserUtils = shaka.util.ManifestParserUtils;
|
|
if (type == ManifestParserUtils.ContentType.TEXT)
|
|
kind = ManifestParserUtils.TextStreamKind.SUBTITLE;
|
|
// TODO: CLOSED-CAPTIONS requires the parsing of CEA-608
|
|
// from the video.
|
|
|
|
var drmTags = [];
|
|
playlist.segments.forEach(function(segment) {
|
|
var segmentKeyTags = Utils.filterTagsByName(segment.tags,
|
|
'EXT-X-KEY');
|
|
drmTags.push.apply(drmTags, segmentKeyTags);
|
|
});
|
|
|
|
var encrypted = false;
|
|
var drmInfos = [];
|
|
var keyId = null;
|
|
|
|
// TODO: may still need changes to support key rotation
|
|
drmTags.forEach(function(drmTag) {
|
|
var method = HlsParser.getRequiredAttributeValue_(drmTag, 'METHOD');
|
|
if (method != 'NONE') {
|
|
encrypted = true;
|
|
|
|
var keyFormat =
|
|
HlsParser.getRequiredAttributeValue_(drmTag, 'KEYFORMAT');
|
|
var drmParser =
|
|
shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
|
|
|
|
var 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);
|
|
}
|
|
|
|
var stream = {
|
|
id: this.globalId_++,
|
|
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: label || null,
|
|
type: type,
|
|
primary: primary,
|
|
// TODO: trick mode
|
|
trickModeVideo: null,
|
|
containsEmsgBoxes: false,
|
|
frameRate: undefined,
|
|
width: undefined,
|
|
height: undefined,
|
|
bandwidth: undefined,
|
|
roles: [],
|
|
channelsCount: channelsCount
|
|
};
|
|
|
|
this.streamsToIndexMap_[stream.id] = segmentIndex;
|
|
|
|
return {
|
|
stream: stream,
|
|
segmentIndex: segmentIndex,
|
|
drmInfos: drmInfos,
|
|
relativeUri: relativeUri,
|
|
minTimestamp: minTimestamp,
|
|
duration: duration
|
|
};
|
|
}.bind(this));
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {!shaka.hls.Playlist} playlist
|
|
* @private
|
|
*/
|
|
shaka.hls.HlsParser.prototype.determinePresentationType_ = function(playlist) {
|
|
var Utils = shaka.hls.Utils;
|
|
var PresentationType = shaka.hls.HlsParser.PresentationType_;
|
|
var presentationTypeTag = Utils.getFirstTagWithName(playlist.tags,
|
|
'EXT-X-PLAYLIST-TYPE');
|
|
var endListTag = Utils.getFirstTagWithName(playlist.tags, 'EXT-X-ENDLIST');
|
|
|
|
var isVod = (presentationTypeTag && presentationTypeTag.value == 'VOD') ||
|
|
endListTag;
|
|
var isEvent = presentationTypeTag && presentationTypeTag.value == 'EVENT' &&
|
|
!isVod;
|
|
var isLive = !isVod && !isEvent;
|
|
|
|
if (isVod) {
|
|
this.setPresentationType_(PresentationType.VOD);
|
|
} else {
|
|
// presentation type LIVE or an ongoing EVENT
|
|
if (isLive) {
|
|
this.setPresentationType_(PresentationType.LIVE);
|
|
} else {
|
|
this.setPresentationType_(PresentationType.EVENT);
|
|
}
|
|
|
|
var targetDurationTag = this.getRequiredTag_(playlist.tags,
|
|
'EXT-X-TARGETDURATION');
|
|
var targetDuration = Number(targetDurationTag.value);
|
|
|
|
// According to HLS spec, updates should not happen more often than
|
|
// once in targetDuration. It also requires 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 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} endTime
|
|
* @throws shaka.util.Error
|
|
* @private
|
|
*/
|
|
shaka.hls.HlsParser.prototype.createPresentationTimeline_ = function(endTime) {
|
|
var presentationStartTime = null;
|
|
var delay = 0;
|
|
|
|
if (this.isLive_()) {
|
|
presentationStartTime = (Date.now() / 1000) - endTime;
|
|
|
|
// We should have a delay of at least 3 target durations.
|
|
delay = this.maxTargetDuration_ * 3;
|
|
}
|
|
|
|
this.presentationTimeline_ = new shaka.media.PresentationTimeline(
|
|
presentationStartTime, delay);
|
|
this.presentationTimeline_.setStatic(!this.isLive_());
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {!shaka.hls.Playlist} playlist
|
|
* @return {shaka.media.InitSegmentReference}
|
|
* @private
|
|
* @throws {shaka.util.Error}
|
|
*/
|
|
shaka.hls.HlsParser.prototype.createInitSegmentReference_ = function(playlist) {
|
|
var Utils = shaka.hls.Utils;
|
|
var 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"
|
|
var mapTag = mapTags[0];
|
|
var initUri = shaka.hls.HlsParser.getRequiredAttributeValue_(mapTag, 'URI');
|
|
var uri = Utils.constructAbsoluteUri(playlist.uri, initUri);
|
|
var startByte = 0;
|
|
var endByte = null;
|
|
var byterange = mapTag.getAttributeValue('BYTERANGE');
|
|
// If BYTERANGE attribute is not specified, the segment consists
|
|
// of the entire resourse.
|
|
if (byterange) {
|
|
var blocks = byterange.split('@');
|
|
var byteLength = Number(blocks[0]);
|
|
startByte = Number(blocks[1]);
|
|
endByte = startByte + byteLength - 1;
|
|
}
|
|
|
|
return new shaka.media.InitSegmentReference(function() { return [uri]; },
|
|
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) {
|
|
var Utils = shaka.hls.Utils;
|
|
var tags = hlsSegment.tags;
|
|
var uri = Utils.constructAbsoluteUri(playlist.uri, hlsSegment.uri);
|
|
|
|
var extinfTag = this.getRequiredTag_(tags, 'EXTINF');
|
|
// EXTINF tag format is '#EXTINF:<duration>,[<title>]'.
|
|
// We're interested in the duration part.
|
|
var extinfValues = extinfTag.value.split(',');
|
|
var duration = Number(extinfValues[0]);
|
|
var endTime = startTime + duration;
|
|
|
|
var startByte = 0;
|
|
var endByte = null;
|
|
var byterange = Utils.getFirstTagWithName(tags, 'EXT-X-BYTERANGE');
|
|
|
|
// If BYTERANGE is not specified, the segment consists of the
|
|
// entire resourse.
|
|
if (byterange) {
|
|
var blocks = byterange.value.split('@');
|
|
var 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,
|
|
function() { return [uri]; },
|
|
startByte,
|
|
endByte);
|
|
};
|
|
|
|
|
|
/**
|
|
* Parses shaka.hls.Segment objects into shaka.media.SegmentReferences.
|
|
*
|
|
* @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(playlist, startPosition, mimeType, codecs) {
|
|
var Utils = shaka.hls.Utils;
|
|
var hlsSegments = playlist.segments;
|
|
var 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.
|
|
var firstSegmentUri = Utils.constructAbsoluteUri(playlist.uri,
|
|
hlsSegments[0].uri);
|
|
var firstSegmentRef =
|
|
this.createSegmentReference_(
|
|
playlist,
|
|
null /* previousReference */,
|
|
hlsSegments[0],
|
|
startPosition,
|
|
0 /* startTime, irrelevant */);
|
|
|
|
return this.getStartTime_(playlist.uri, firstSegmentRef, mimeType, codecs)
|
|
.then(function(firstStartTime) {
|
|
shaka.log.debug('First segment', firstSegmentUri.split('/').pop(),
|
|
'starts at', firstStartTime);
|
|
for (var i = 0; i < hlsSegments.length; ++i) {
|
|
var hlsSegment = hlsSegments[i];
|
|
var previousReference = references[references.length - 1];
|
|
var startTime = (i == 0) ? firstStartTime : previousReference.endTime;
|
|
var position = startPosition + i;
|
|
|
|
var reference = this.createSegmentReference_(
|
|
playlist,
|
|
previousReference,
|
|
hlsSegment,
|
|
position,
|
|
startTime);
|
|
references.push(reference);
|
|
}
|
|
|
|
return references;
|
|
}.bind(this));
|
|
};
|
|
|
|
|
|
/**
|
|
* Try to fetch a partial segment, and fall back to a full segment if we have
|
|
* to.
|
|
*
|
|
* @param {!shaka.media.SegmentReference} segmentRef
|
|
* @return {!Promise.<shakaExtern.Response>}
|
|
* @throws {shaka.util.Error}
|
|
* @private
|
|
*/
|
|
shaka.hls.HlsParser.prototype.fetchPartialSegment_ = function(segmentRef) {
|
|
var networkingEngine = this.playerInterface_.networkingEngine;
|
|
var requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
|
|
var request = shaka.net.NetworkingEngine.makeRequest(
|
|
segmentRef.getUris(), this.config_.retryParameters);
|
|
|
|
// Try to avoid fetching the entire segment, which can be quite large.
|
|
var partialSegmentHeaders = {};
|
|
var startByte = segmentRef.startByte;
|
|
var partialEndByte =
|
|
startByte + shaka.hls.HlsParser.PARTIAL_SEGMENT_SIZE_ - 1;
|
|
partialSegmentHeaders['Range'] = 'bytes=' + startByte + '-' + partialEndByte;
|
|
|
|
// Prepare a fallback to the entire segment.
|
|
var fullSegmentHeaders = {};
|
|
if ((startByte != 0) || (segmentRef.endByte != null)) {
|
|
var range = 'bytes=' + startByte + '-';
|
|
if (segmentRef.endByte != null) range += segmentRef.endByte;
|
|
|
|
fullSegmentHeaders['Range'] = range;
|
|
}
|
|
|
|
// Try a partial request first.
|
|
request.headers = partialSegmentHeaders;
|
|
return networkingEngine.request(requestType, request).catch(function(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;
|
|
return networkingEngine.request(requestType, request);
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* Gets start time of a segment from the existing manifest (if possible) or by
|
|
* downloading it and parsing it otherwise.
|
|
*
|
|
* @param {string} playlistUri
|
|
* @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(playlistUri, 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_) {
|
|
var streamInfo = this.uriToStreamInfosMap_[playlistUri];
|
|
var segmentIndex = streamInfo.segmentIndex;
|
|
var 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');
|
|
return this.fetchPartialSegment_(segmentRef).then(function(response) {
|
|
if (mimeType == 'video/mp4' || mimeType == 'audio/mp4') {
|
|
return this.getStartTimeFromMp4Segment_(response.data);
|
|
} else if (mimeType == 'video/mp2t') {
|
|
return this.getStartTimeFromTsSegment_(response.data);
|
|
} else if (mimeType == 'application/mp4' ||
|
|
mimeType.indexOf('text/') == 0) {
|
|
return this.getStartTimeFromTextSegment_(
|
|
mimeType, codecs, response.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} data
|
|
* @return {number}
|
|
* @throws {shaka.util.Error}
|
|
* @private
|
|
*/
|
|
shaka.hls.HlsParser.prototype.getStartTimeFromMp4Segment_ = function(data) {
|
|
var startTime = 0;
|
|
var Mp4Parser = shaka.util.Mp4Parser;
|
|
var parsed = 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');
|
|
var baseTime = (box.version == 0) ?
|
|
box.reader.readUint32() :
|
|
box.reader.readUint64();
|
|
startTime = baseTime / shaka.hls.HlsParser.TS_TIMESCALE_;
|
|
parsed = true;
|
|
box.parser.stop();
|
|
}).parse(data, true /* partialOkay */);
|
|
|
|
if (!parsed) {
|
|
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) {
|
|
var reader = new shaka.util.DataViewReader(
|
|
new DataView(data), shaka.util.DataViewReader.Endianness.BIG_ENDIAN);
|
|
|
|
var 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);
|
|
};
|
|
|
|
var packetStart = 0;
|
|
|
|
var 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) {
|
|
fail();
|
|
}
|
|
// Put the sync byte back so we can read it in the next loop.
|
|
reader.rewind(1);
|
|
};
|
|
|
|
while (true) {
|
|
// Format reference: https://goo.gl/wk6wwu
|
|
packetStart = reader.getPosition();
|
|
|
|
var syncByte = reader.readUint8();
|
|
if (syncByte != 0x47) fail();
|
|
|
|
var flagsAndPacketId = reader.readUint16();
|
|
var hasPesPacket = flagsAndPacketId & 0x4000;
|
|
if (!hasPesPacket) fail();
|
|
|
|
var flags = reader.readUint8();
|
|
var adaptationFieldControl = (flags & 0x30) >> 4;
|
|
if (adaptationFieldControl == 0 /* reserved */ ||
|
|
adaptationFieldControl == 2 /* adaptation field, no payload */) {
|
|
fail();
|
|
}
|
|
|
|
if (adaptationFieldControl == 3) {
|
|
// Skip over adaptation field.
|
|
var length = reader.readUint8();
|
|
reader.skip(length);
|
|
}
|
|
|
|
// Now we come to the PES header (hopefully).
|
|
// Format reference: https://goo.gl/1166Mr
|
|
var startCode = reader.readUint32();
|
|
var 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.
|
|
var ptsDtsIndicator = reader.readUint8() >> 6;
|
|
if (ptsDtsIndicator == 0 /* no timestamp */ ||
|
|
ptsDtsIndicator == 1 /* forbidden */) {
|
|
fail();
|
|
}
|
|
|
|
var 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?');
|
|
}
|
|
|
|
var pts0 = reader.readUint8();
|
|
var pts1 = reader.readUint16();
|
|
var pts2 = reader.readUint16();
|
|
// Reconstruct 33-bit PTS from the 5-byte, padded structure.
|
|
var ptsHigh3 = (pts0 & 0x0e) >> 1;
|
|
var 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.
|
|
var 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) {
|
|
var 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;
|
|
}
|
|
|
|
var 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) {
|
|
var ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
var HlsParser = shaka.hls.HlsParser;
|
|
var formats = HlsParser.CODEC_REGEXPS_BY_CONTENT_TYPE_[contentType];
|
|
|
|
for (var i = 0; i < formats.length; i++) {
|
|
for (var 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 a single codec is correct, and throws if not found.
|
|
*
|
|
* @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];
|
|
}
|
|
|
|
var match = this.guessCodecsSafe_(contentType, codecs);
|
|
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) {
|
|
var ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
var HlsParser = shaka.hls.HlsParser;
|
|
var Utils = shaka.hls.Utils;
|
|
|
|
goog.asserts.assert(playlist.segments.length,
|
|
'Playlist should have segments!');
|
|
var firstSegmentUri = Utils.constructAbsoluteUri(playlist.uri,
|
|
playlist.segments[0].uri);
|
|
|
|
var parsedUri = new goog.Uri(firstSegmentUri);
|
|
var extension = parsedUri.getPath().split('.').pop();
|
|
var map = HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_[contentType];
|
|
|
|
var 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.
|
|
var headRequest = shaka.net.NetworkingEngine.makeRequest(
|
|
[firstSegmentUri], this.config_.retryParameters);
|
|
headRequest.method = 'HEAD';
|
|
var requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
|
|
var networkingEngine = this.playerInterface_.networkingEngine;
|
|
return networkingEngine.request(requestType, headRequest)
|
|
.then(function(response) {
|
|
var 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) {
|
|
var 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) {
|
|
var Utils = shaka.hls.Utils;
|
|
var 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 {shakaExtern.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} uri
|
|
* @return {!Promise.<!shakaExtern.Response>}
|
|
* @private
|
|
*/
|
|
shaka.hls.HlsParser.prototype.requestManifest_ = function(uri) {
|
|
var requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
|
|
var request = shaka.net.NetworkingEngine.makeRequest(
|
|
[uri], this.config_.retryParameters);
|
|
var networkingEngine = this.playerInterface_.networkingEngine;
|
|
var isCanceled = (function() {
|
|
return !this.playerInterface_;
|
|
}).bind(this);
|
|
return networkingEngine.request(requestType, request, isCanceled);
|
|
};
|
|
|
|
|
|
/**
|
|
* 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 aslo uses video/ for audio: http://goo.gl/tYHXiS
|
|
'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):?shakaExtern.DrmInfo}
|
|
* @private
|
|
*/
|
|
shaka.hls.HlsParser.DrmParser_;
|
|
|
|
|
|
/**
|
|
* @param {!shaka.hls.Tag} drmTag
|
|
* @return {?shakaExtern.DrmInfo}
|
|
* @private
|
|
*/
|
|
shaka.hls.HlsParser.widevineDrmParser_ = function(drmTag) {
|
|
var HlsParser = shaka.hls.HlsParser;
|
|
var method = HlsParser.getRequiredAttributeValue_(drmTag, 'METHOD');
|
|
if (method != 'SAMPLE-AES-CENC') {
|
|
shaka.log.error(
|
|
'Widevine in HLS is only supported with SAMPLE-AES-CENC, not', method);
|
|
return null;
|
|
}
|
|
|
|
var uri = HlsParser.getRequiredAttributeValue_(drmTag, 'URI');
|
|
var parsedData = shaka.net.DataUriPlugin.parse(uri);
|
|
|
|
// The data encoded in the URI is a PSSH box to be used as init data.
|
|
var pssh = new Uint8Array(parsedData.data);
|
|
var drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
|
|
'com.widevine.alpha', [
|
|
{initDataType: 'cenc', initData: pssh}
|
|
]);
|
|
|
|
var keyId = drmTag.getAttributeValue('KEYID');
|
|
if (keyId) {
|
|
// This value begins with '0x':
|
|
goog.asserts.assert(keyId.substr(0, 2) == '0x',
|
|
'Incorrect KEYID format!');
|
|
// But the output does 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');
|
|
|
|
var callback = this.onUpdate_.bind(this);
|
|
this.updateTimer_ = window.setTimeout(callback, time * 1000);
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {boolean}
|
|
* @private
|
|
*/
|
|
shaka.hls.HlsParser.prototype.isLive_ = function() {
|
|
var 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;
|
|
// TODO: Consider extracting this from the MP4 instead of assuming a
|
|
// TS-compatible timescale in fMP4 HLS content.
|
|
|
|
|
|
/**
|
|
* 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);
|