Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(HLS): Add support for EXT-X-DATERANGE #6718

Merged
merged 7 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
177 changes: 152 additions & 25 deletions lib/hls/hls_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,12 @@ shaka.hls.HlsParser = class {

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

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

/** @private {?number} */
this.initialProgramDateTime_ = null;
}


Expand Down Expand Up @@ -316,6 +322,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 @@ -861,6 +868,8 @@ shaka.hls.HlsParser = class {
imageStreams = await this.parseImages_(imageTags, iFrameTags);
}

this.processDateRangeTags_(playlist.tags);

// Make sure that the parser has not been destroyed.
if (!this.playerInterface_) {
throw new shaka.util.Error(
Expand Down Expand Up @@ -1054,22 +1063,6 @@ shaka.hls.HlsParser = class {
this.presentationTimeline_.setDuration(this.getMinDuration_());
}

if (!this.presentationTimeline_.isStartTimeLocked()) {
for (const streamInfo of this.uriToStreamInfosMap_.values()) {
if (!streamInfo.stream.segmentIndex) {
continue; // Not active.
}
if (streamInfo.type != 'audio' && streamInfo.type != 'video') {
continue;
}
const firstReference = streamInfo.stream.segmentIndex.get(0);
if (firstReference && firstReference.syncTime) {
const syncTime = firstReference.syncTime;
this.presentationTimeline_.setInitialProgramDateTime(syncTime);
}
}
}

// This is the first point where we have a meaningful presentation start
// time, and we need to tell PresentationTimeline that so that it can
// maintain consistency from here on.
Expand Down Expand Up @@ -2436,6 +2429,8 @@ shaka.hls.HlsParser = class {
this.finalizeStreams_([streamInfo]);
}
}

this.processDateRangeTags_(playlist.tags);
};

/** @type {Promise} */
Expand Down Expand Up @@ -3312,6 +3307,8 @@ shaka.hls.HlsParser = class {
createSegmentReference_(
initSegmentReference, previousReference, hlsSegment, startTime,
variables, playlist, stream, getUris, aesKey) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;

const tags = hlsSegment.tags;
const extinfTag =
shaka.hls.Utils.getFirstTagWithName(tags, 'EXTINF');
Expand All @@ -3327,15 +3324,22 @@ shaka.hls.HlsParser = class {
'true, and see https://bit.ly/3clctcj for details.');
}

let syncTime = null;
if (!this.config_.hls.ignoreManifestProgramDateTime) {
const dateTimeTag =
shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-PROGRAM-DATE-TIME');
if (dateTimeTag && dateTimeTag.value) {
syncTime = shaka.util.TXml.parseDate(dateTimeTag.value);
goog.asserts.assert(syncTime != null,
'EXT-X-PROGRAM-DATE-TIME format not valid');
}
let firstProgramDateTime = null;
const dateTimeTag =
shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-PROGRAM-DATE-TIME');
if (dateTimeTag && dateTimeTag.value) {
firstProgramDateTime = shaka.util.TXml.parseDate(dateTimeTag.value);
goog.asserts.assert(firstProgramDateTime != null,
'EXT-X-PROGRAM-DATE-TIME format not valid');
}
const syncTime = this.config_.hls.ignoreManifestProgramDateTime ?
null : firstProgramDateTime;
if (!this.initialProgramDateTime_ && firstProgramDateTime &&
(stream.type == ContentType.AUDIO ||
stream.type == ContentType.VIDEO)) {
avelad marked this conversation as resolved.
Show resolved Hide resolved
this.initialProgramDateTime_ = firstProgramDateTime;
this.presentationTimeline_.setInitialProgramDateTime(
this.initialProgramDateTime_);
avelad marked this conversation as resolved.
Show resolved Hide resolved
}

let status = shaka.media.SegmentReference.Status.AVAILABLE;
Expand Down Expand Up @@ -3616,6 +3620,129 @@ shaka.hls.HlsParser = class {
return [startByte, endByte];
}

/**
* @param {!Array.<!shaka.hls.Tag>} tags
* @private
*/
processDateRangeTags_(tags) {
avelad marked this conversation as resolved.
Show resolved Hide resolved
if (this.initialProgramDateTime_ == null) {
// We cannot process EXT-X-DATERANGE tags without
// EXT-X-PROGRAM-DATE-TIME.
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 - this.initialProgramDateTime_);
avelad marked this conversation as resolved.
Show resolved Hide resolved

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 - this.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 - this.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
12 changes: 11 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,16 @@ 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) {
this.dispatchMetadataEvent_(
avelad marked this conversation as resolved.
Show resolved Hide resolved
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
Loading