Skip to content

Commit

Permalink
feat(HLS): Add support for EXT-X-DATERANGE (#6718)
Browse files Browse the repository at this point in the history
Closes #3523
  • Loading branch information
avelad authored Jun 3, 2024
1 parent 89c8361 commit 2ec6444
Show file tree
Hide file tree
Showing 19 changed files with 365 additions and 11 deletions.
7 changes: 6 additions & 1 deletion externs/shaka/manifest_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,9 @@ shaka.extern.ManifestParser = class {
* updateDuration: function(),
* newDrmInfo: function(shaka.extern.Stream),
* onManifestUpdated: function(),
* getBandwidthEstimate: function():number
* getBandwidthEstimate: function():number,
* onMetadata: function(string, number, ?number,
* !Array.<shaka.extern.MetadataFrame>)
* }}
*
* @description
Expand Down Expand Up @@ -167,6 +169,9 @@ shaka.extern.ManifestParser = class {
* Should be called when the manifest is updated.
* @property {function():number} getBandwidthEstimate
* Get the estimated bandwidth in bits per second.
* @property {function(string, number, ?number,
* !Array.<shaka.extern.MetadataFrame>)} onMetadata
* Called when an metadata is found in the manifest.
* @exportDoc
*/
shaka.extern.ManifestParser.PlayerInterface;
Expand Down
6 changes: 3 additions & 3 deletions externs/shaka/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -1071,9 +1071,9 @@ shaka.extern.DashManifestConfiguration;
* @property {boolean} ignoreManifestProgramDateTime
* If <code>true</code>, the HLS parser will ignore the
* <code>EXT-X-PROGRAM-DATE-TIME</code> tags in the manifest and use media
* sequence numbers instead.
* Meant for streams where <code>EXT-X-PROGRAM-DATE-TIME</code> is incorrect
* or malformed.
* sequence numbers instead. It also causes EXT-X-DATERANGE tags to be
* ignored. Meant for streams where <code>EXT-X-PROGRAM-DATE-TIME</code> is
* incorrect or malformed.
* <i>Defaults to <code>false</code>.</i>
* @property {!Array.<string>} ignoreManifestProgramDateTimeForTypes
* An array of strings representing types for which
Expand Down
151 changes: 145 additions & 6 deletions lib/hls/hls_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,9 @@ shaka.hls.HlsParser = class {

/** @private {boolean} */
this.needsClosedCaptionsDetection_ = true;

/** @private {Set.<string>} */
this.dateRangeIdsEmitted_ = new Set();
}


Expand Down Expand Up @@ -316,6 +319,7 @@ shaka.hls.HlsParser = class {
this.aesKeyMap_.clear();
this.identityKeyMap_.clear();
this.identityKidMap_.clear();
this.dateRangeIdsEmitted_.clear();

if (this.contentSteeringManager_) {
this.contentSteeringManager_.destroy();
Expand Down Expand Up @@ -760,6 +764,9 @@ shaka.hls.HlsParser = class {
return [uri];
};

/** @type {?string} */
let mediaPlaylistType = null;

// Parsing a media playlist results in a single-variant stream.
if (playlist.type == shaka.hls.PlaylistType.MEDIA) {
this.needsClosedCaptionsDetection_ = false;
Expand All @@ -775,7 +782,7 @@ shaka.hls.HlsParser = class {
// find from the master playlist (e.g. from values on EXT-X-MEDIA tags).
const basicInfo = await this.getMediaPlaylistBasicInfo_(
playlist, getUris, mediaVariables);
const type = basicInfo.type;
mediaPlaylistType = basicInfo.type;
const mimeType = basicInfo.mimeType;
const codecs = basicInfo.codecs;
const languageValue = basicInfo.language;
Expand All @@ -800,11 +807,12 @@ shaka.hls.HlsParser = class {
// Make the stream info, with those values.
const streamInfo = await this.convertParsedPlaylistIntoStreamInfo_(
this.globalId_++, mediaVariables, playlist, getUris, uri, codecs,
type, languageValue, primary, name, channelsCount, closedCaptions,
characteristics, forced, sampleRate, spatialAudio, mimeType);
mediaPlaylistType, languageValue, primary, name, channelsCount,
closedCaptions, characteristics, forced, sampleRate, spatialAudio,
mimeType);
this.uriToStreamInfosMap_.set(uri, streamInfo);

if (type == 'video') {
if (mediaPlaylistType == 'video') {
this.addVideoAttributes_(streamInfo.stream, width, height,
/* frameRate= */ null, videoRange, /* videoLayout= */ null,
colorGamut);
Expand All @@ -816,8 +824,8 @@ shaka.hls.HlsParser = class {
language: this.getLanguage_(languageValue),
disabledUntilTime: 0,
primary: true,
audio: type == 'audio' ? streamInfo.stream : null,
video: type == 'video' ? streamInfo.stream : null,
audio: mediaPlaylistType == 'audio' ? streamInfo.stream : null,
video: mediaPlaylistType == 'video' ? streamInfo.stream : null,
bandwidth: streamInfo.stream.bandwidth || 0,
allowedByApplication: true,
allowedByKeySystem: true,
Expand Down Expand Up @@ -880,6 +888,10 @@ shaka.hls.HlsParser = class {
const streamInfos = Array.from(this.uriToStreamInfosMap_.values());
this.finalizeStreams_(streamInfos);
this.determineDuration_();

goog.asserts.assert(mediaPlaylistType,
'mediaPlaylistType should be non-null');
this.processDateRangeTags_(playlist.tags, mediaPlaylistType);
}

this.manifest_ = {
Expand Down Expand Up @@ -2436,6 +2448,8 @@ shaka.hls.HlsParser = class {
this.finalizeStreams_([streamInfo]);
}
}

this.processDateRangeTags_(playlist.tags, stream.type);
};

/** @type {Promise} */
Expand Down Expand Up @@ -3616,6 +3630,131 @@ shaka.hls.HlsParser = class {
return [startByte, endByte];
}

/**
* @param {!Array.<!shaka.hls.Tag>} tags
* @param {string} contentType
* @private
*/
processDateRangeTags_(tags, contentType) {
const initialProgramDateTime =
this.presentationTimeline_.getInitialProgramDateTime();
if (!initialProgramDateTime ||
this.ignoreManifestProgramDateTimeFor_(contentType)) {
return;
}
let dateRangeTags =
shaka.hls.Utils.filterTagsByName(tags, 'EXT-X-DATERANGE');
dateRangeTags = dateRangeTags.sort((a, b) => {
const aStartDateValue = a.getRequiredAttrValue('START-DATE');
const bStartDateValue = b.getRequiredAttrValue('START-DATE');
if (aStartDateValue < bStartDateValue) {
return -1;
}
if (aStartDateValue > bStartDateValue) {
return 1;
}
return 0;
});
for (let i = 0; i < dateRangeTags.length; i++) {
const tag = dateRangeTags[i];
const id = tag.getRequiredAttrValue('ID');
if (this.dateRangeIdsEmitted_.has(id)) {
continue;
}
const startDateValue = tag.getRequiredAttrValue('START-DATE');
const startDate = shaka.util.TXml.parseDate(startDateValue);
if (isNaN(startDate)) {
// Invalid START-DATE
continue;
}
goog.asserts.assert(startDate != null, 'Start date should not be null!');
const startTime = Math.max(0, startDate - initialProgramDateTime);

let endTime = null;
const endDateValue = tag.getAttributeValue('END-DATE');
if (endDateValue) {
const endDate = shaka.util.TXml.parseDate(endDateValue);
if (!isNaN(endDate)) {
goog.asserts.assert(endDate != null, 'End date should not be null!');
endTime = Math.max(0, endDate - initialProgramDateTime);
}
}
if (endTime == null) {
const durationValue = tag.getAttributeValue('DURATION');
if (durationValue) {
const duration = parseFloat(durationValue);
if (!isNaN(duration)) {
endTime = startTime + duration;
}
}
}
const type = tag.getAttributeValue('CLASS') || 'com.apple.quicktime.HLS';

const endOnNext = tag.getAttributeValue('END-ON-NEXT') == 'YES';
if (endTime == null && endOnNext) {
for (let j = i + 1; j < dateRangeTags.length; j++) {
const otherDateRangeType =
dateRangeTags[j].getAttributeValue('CLASS') ||
'com.apple.quicktime.HLS';
if (type != otherDateRangeType) {
continue;
}
const otherDateRangeStartDateValue =
dateRangeTags[j].getRequiredAttrValue('START-DATE');
const otherDateRangeStartDate =
shaka.util.TXml.parseDate(otherDateRangeStartDateValue);
if (isNaN(otherDateRangeStartDate)) {
// Invalid START-DATE
continue;
}
if (otherDateRangeStartDate && otherDateRangeStartDate > startDate) {
endTime = Math.max(0,
otherDateRangeStartDate - initialProgramDateTime);
break;
}
}
if (endTime == null) {
// Since we cannot know when it ends, we omit it for now and in the
// future with an update we will be able to have more information.
continue;
}
}

// Exclude these attributes from the metadata since they already go into
// other fields (eg: startTime or endTime) or are not necessary..
const excludedAttributes = [
'ID',
'CLASS',
'START-DATE',
'END-DATE',
'DURATION',
'END-ON-NEXT',
];

/* @type {!Array.<shaka.extern.MetadataFrame>} */
const values = [];
for (const attribute of tag.attributes) {
if (excludedAttributes.includes(attribute.name)) {
continue;
}
const metadataFrame = {
key: attribute.name,
description: '',
data: attribute.value,
mimeType: null,
pictureType: null,
};
values.push(metadataFrame);
}

if (values.length) {
this.playerInterface_.onMetadata(type, startTime, endTime, values);
}

this.dateRangeIdsEmitted_.add(id);
}
}

/**
* Parses shaka.hls.Segment objects into shaka.media.SegmentReferences and
* get the bandwidth necessary for this segments If it's defined in the
Expand Down
1 change: 1 addition & 0 deletions lib/hls/manifest_text_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ shaka.hls.ManifestTextParser.MEDIA_PLAYLIST_TAGS = [
'EXT-X-SERVER-CONTROL',
'EXT-X-SKIP',
'EXT-X-PART-INF',
'EXT-X-DATERANGE',
];


Expand Down
1 change: 1 addition & 0 deletions lib/offline/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,7 @@ shaka.offline.Storage = class {
newDrmInfo: (stream) => {},
onManifestUpdated: () => {},
getBandwidthEstimate: () => config.abr.defaultBandwidthEstimate,
onMetadata: () => {},
};

parser.configure(config.manifest);
Expand Down
14 changes: 13 additions & 1 deletion lib/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ goog.requireType('shaka.media.PresentationTimeline');
* The time that describes the end of the range of the metadata to which
* the cue applies.
* @property {string} metadataType
* Type of metadata. Eg: org.id3 or org.mp4ra
* Type of metadata. Eg: 'org.id3' or 'com.apple.quicktime.HLS'
* @property {shaka.extern.MetadataFrame} payload
* The metadata itself
* @exportDoc
Expand Down Expand Up @@ -2034,6 +2034,18 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
});
},
getBandwidthEstimate: () => this.abrManager_.getBandwidthEstimate(),
onMetadata: (type, startTime, endTime, values) => {
let metadataType = type;
if (type == 'com.apple.hls.interstitial') {
metadataType = 'com.apple.quicktime.HLS';
}
for (const payload of values) {
preloadManager.addQueuedOperation(false, () => {
this.dispatchMetadataEvent_(
startTime, endTime, metadataType, payload);
});
}
},
};
const regionTimeline =
new shaka.media.RegionTimeline(() => this.seekRange());
Expand Down
1 change: 1 addition & 0 deletions test/dash/dash_parser_content_protection_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ describe('DashParser ContentProtection', () => {
newDrmInfo: (stream) => {},
onManifestUpdated: () => {},
getBandwidthEstimate: () => 1e6,
onMetadata: () => {},
};

const actual = await dashParser.start(
Expand Down
1 change: 1 addition & 0 deletions test/dash/dash_parser_live_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ describe('DashParser Live', () => {
newDrmInfo: (stream) => {},
onManifestUpdated: () => {},
getBandwidthEstimate: () => 1e6,
onMetadata: () => {},
};
});

Expand Down
1 change: 1 addition & 0 deletions test/dash/dash_parser_manifest_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ describe('DashParser Manifest', () => {
newDrmInfo: (stream) => {},
onManifestUpdated: () => {},
getBandwidthEstimate: () => 1e6,
onMetadata: () => {},
};
});

Expand Down
1 change: 1 addition & 0 deletions test/dash/dash_parser_patch_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ describe('DashParser Patch', () => {
newDrmInfo: (stream) => {},
onManifestUpdated: () => {},
getBandwidthEstimate: () => 1e6,
onMetadata: () => {},
};
Date.now = () => publishTime.getTime() + 10;

Expand Down
1 change: 1 addition & 0 deletions test/dash/dash_parser_segment_base_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('DashParser SegmentBase', () => {
newDrmInfo: (stream) => {},
onManifestUpdated: () => {},
getBandwidthEstimate: () => 1e6,
onMetadata: () => {},
};
});

Expand Down
1 change: 1 addition & 0 deletions test/dash/dash_parser_segment_list_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ describe('DashParser SegmentList', () => {
newDrmInfo: (stream) => {},
onManifestUpdated: () => {},
getBandwidthEstimate: () => 1e6,
onMetadata: () => {},
};
try {
const manifest = await dashParser.start('dummy://foo', playerInterface);
Expand Down
1 change: 1 addition & 0 deletions test/dash/dash_parser_segment_template_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ describe('DashParser SegmentTemplate', () => {
newDrmInfo: (stream) => {},
onManifestUpdated: () => {},
getBandwidthEstimate: () => 1e6,
onMetadata: () => {},
};
});

Expand Down
1 change: 1 addition & 0 deletions test/hls/hls_live_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ describe('HlsParser live', () => {
newDrmInfo: (stream) => {},
onManifestUpdated: () => {},
getBandwidthEstimate: () => 1e6,
onMetadata: () => {},
};

parser = new shaka.hls.HlsParser();
Expand Down
Loading

0 comments on commit 2ec6444

Please sign in to comment.