Skip to content

Commit 6271282

Browse files
aveladtykus160
andauthored
fix: Allow use Chapter API on browsers without track element support (#8515)
Co-authored-by: Wojciech Tyczyński <[email protected]>
1 parent 06ad1e8 commit 6271282

File tree

3 files changed

+123
-87
lines changed

3 files changed

+123
-87
lines changed

lib/cast/cast_proxy.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -731,16 +731,6 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
731731
return () => this.localPlayer_.getAdManager();
732732
}
733733

734-
if (name == 'getChapters') {
735-
// This does not follow our standard pattern (takes an arguments), and
736-
// therefore can't be proactively proxied and cached the way other
737-
// synchronous getters can.
738-
if (this.sender_.isCasting()) {
739-
shaka.log.warning('NOTE: getChapters() does not work while casting!');
740-
}
741-
return () => [];
742-
}
743-
744734
if (name == 'setVideoContainer') {
745735
// Always returns a local instance.
746736
if (this.sender_.isCasting()) {
@@ -764,6 +754,16 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
764754
return Promise.resolve();
765755
};
766756
}
757+
758+
if (name == 'getChapters') {
759+
// This does not follow our standard pattern (takes an arguments), and
760+
// therefore can't be proactively proxied and cached the way other
761+
// synchronous getters can.
762+
return () => {
763+
shaka.log.alwaysWarn(name + '() does not work while casting!');
764+
return [];
765+
};
766+
}
767767
} // if (this.sender_.isCasting())
768768

769769
// If we are casting, but the first update has not come in yet, use local

lib/player.js

Lines changed: 107 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
823823
/** @private {!Array<shaka.extern.Stream>} */
824824
this.externalSrcEqualsThumbnailsStreams_ = [];
825825

826+
/** @private {!Array<shaka.extern.Stream>} */
827+
this.externalChaptersStreams_ = [];
828+
826829
/** @private {number} */
827830
this.completionPercent_ = -1;
828831

@@ -1591,6 +1594,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
15911594
this.rebufferingCount_ = -1;
15921595

15931596
this.externalSrcEqualsThumbnailsStreams_ = [];
1597+
this.externalChaptersStreams_ = [];
15941598

15951599
this.completionPercent_ = -1;
15961600

@@ -6213,13 +6217,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
62136217
* @export
62146218
*/
62156219
getChaptersTracks() {
6216-
if (this.video_ && this.video_.currentSrc && this.video_.textTracks) {
6217-
const textTracks = this.getChaptersTracks_();
6218-
const StreamUtils = shaka.util.StreamUtils;
6219-
return textTracks.map((text) => StreamUtils.html5TextTrackToTrack(text));
6220-
} else {
6221-
return [];
6222-
}
6220+
return this.externalChaptersStreams_.map(
6221+
(text) => shaka.util.StreamUtils.textStreamToTrack(text));
62236222
}
62246223

62256224
/**
@@ -6230,38 +6229,35 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
62306229
* @export
62316230
*/
62326231
getChapters(language) {
6233-
if (!this.video_ || !this.video_.currentSrc || !this.video_.textTracks) {
6232+
if (!this.externalChaptersStreams_.length) {
62346233
return [];
62356234
}
62366235
const LanguageUtils = shaka.util.LanguageUtils;
62376236
const inputLanguage = LanguageUtils.normalize(language);
6238-
const chaptersTracks = this.getChaptersTracks_();
6239-
const chaptersTracksWithLanguage = chaptersTracks
6240-
.filter((t) => LanguageUtils.normalize(t.language) == inputLanguage);
6241-
if (!chaptersTracksWithLanguage || !chaptersTracksWithLanguage.length) {
6237+
const chapterStreams = this.externalChaptersStreams_
6238+
.filter((c) => LanguageUtils.normalize(c.language) == inputLanguage);
6239+
if (!chapterStreams.length) {
62426240
return [];
62436241
}
62446242
const chapters = [];
62456243
const uniqueChapters = new Set();
6246-
for (const chaptersTrack of chaptersTracksWithLanguage) {
6247-
if (chaptersTrack && chaptersTrack.cues) {
6248-
for (const cue of chaptersTrack.cues) {
6249-
let id = cue.id;
6250-
if (!id || id == '') {
6251-
id = cue.startTime + '-' + cue.endTime + '-' + cue.text;
6252-
}
6244+
for (const chapterStream of chapterStreams) {
6245+
if (chapterStream.segmentIndex) {
6246+
chapterStream.segmentIndex.forEachTopLevelReference((ref) => {
6247+
const title = ref.getUris()[0];
6248+
const id = ref.startTime + '-' + ref.endTime + '-' + title;
62536249
/** @type {shaka.extern.Chapter} */
62546250
const chapter = {
6255-
id: id,
6256-
title: cue.text,
6257-
startTime: cue.startTime,
6258-
endTime: cue.endTime,
6251+
id,
6252+
title,
6253+
startTime: ref.startTime,
6254+
endTime: ref.endTime,
62596255
};
62606256
if (!uniqueChapters.has(id)) {
62616257
chapters.push(chapter);
62626258
uniqueChapters.add(id);
62636259
}
6264-
}
6260+
});
62656261
}
62666262
}
62676263
return chapters;
@@ -6308,19 +6304,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
63086304
.filter((t) => t.kind == 'metadata');
63096305
}
63106306

6311-
/**
6312-
* Get the TextTracks with the 'chapters' kind.
6313-
*
6314-
* @return {!Array<TextTrack>}
6315-
* @private
6316-
*/
6317-
getChaptersTracks_() {
6318-
goog.asserts.assert(this.video_.textTracks,
6319-
'TextTracks should be valid.');
6320-
return Array.from(this.video_.textTracks)
6321-
.filter((t) => t.kind == 'chapters');
6322-
}
6323-
63246307
/**
63256308
* Enable or disable the text displayer. If the player is in an unloaded
63266309
* state, the request will be applied next time content is loaded.
@@ -6985,47 +6968,100 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
69856968
mimeType = await this.getTextMimetype_(uri);
69866969
}
69876970

6988-
let adCuePoints = [];
6989-
if (this.adManager_) {
6990-
adCuePoints = this.adManager_.getCuePoints();
6971+
const ContentType = shaka.util.ManifestParserUtils.ContentType;
6972+
const seekRange = this.seekRange();
6973+
let duration = seekRange.end - seekRange.start;
6974+
if (this.manifest_) {
6975+
duration = this.manifest_.presentationTimeline.getDuration();
6976+
}
6977+
if (duration == Infinity) {
6978+
throw new shaka.util.Error(
6979+
shaka.util.Error.Severity.RECOVERABLE,
6980+
shaka.util.Error.Category.MANIFEST,
6981+
shaka.util.Error.Code.CANNOT_ADD_EXTERNAL_CHAPTERS_TO_LIVE_STREAM);
69916982
}
69926983

6993-
/** @type {!HTMLTrackElement} */
6994-
const trackElement = await this.addSrcTrackElement_(
6995-
uri, language, /* kind= */ 'chapters', mimeType, /* label= */ '',
6996-
adCuePoints);
6984+
goog.asserts.assert(
6985+
this.networkingEngine_, 'Need networking engine.');
6986+
const buffer = await this.getTextData_(uri,
6987+
this.networkingEngine_,
6988+
this.config_.streaming.retryParameters);
69976989

6998-
const chaptersTracks = this.getChaptersTracks();
6999-
const chaptersTrack = chaptersTracks.find((t) => {
7000-
return t.language == language;
7001-
});
6990+
const factory = shaka.text.TextEngine.findParser(mimeType);
6991+
if (!factory) {
6992+
throw new shaka.util.Error(
6993+
shaka.util.Error.Severity.CRITICAL,
6994+
shaka.util.Error.Category.TEXT,
6995+
shaka.util.Error.Code.MISSING_TEXT_PLUGIN,
6996+
mimeType);
6997+
}
6998+
const textParser = factory();
6999+
const time = {
7000+
periodStart: 0,
7001+
segmentStart: 0,
7002+
segmentEnd: duration,
7003+
vttOffset: 0,
7004+
};
7005+
const data = shaka.util.BufferUtils.toUint8(buffer);
7006+
const cues = textParser.parseMedia(data, time, uri, /* images= */ []);
70027007

7003-
if (chaptersTrack) {
7004-
await new Promise((resolve, reject) => {
7005-
// The chapter data isn't available until the 'load' event fires, and
7006-
// that won't happen until the chapters track is activated by the
7007-
// activateChaptersTrack_ method.
7008-
this.loadEventManager_.listenOnce(trackElement, 'load', resolve);
7009-
this.loadEventManager_.listenOnce(trackElement, 'error', (event) => {
7010-
reject(new shaka.util.Error(
7011-
shaka.util.Error.Severity.RECOVERABLE,
7012-
shaka.util.Error.Category.TEXT,
7013-
shaka.util.Error.Code.CHAPTERS_TRACK_FAILED));
7014-
});
7015-
});
7008+
const references = [];
7009+
for (const cue of cues) {
7010+
const reference = new shaka.media.SegmentReference(
7011+
cue.startTime,
7012+
cue.endTime,
7013+
() => [cue.payload],
7014+
/* startByte= */ 0,
7015+
/* endByte= */ null,
7016+
/* initSegmentReference= */ null,
7017+
/* timestampOffset= */ 0,
7018+
/* appendWindowStart= */ 0,
7019+
/* appendWindowEnd= */ Infinity,
7020+
);
7021+
references.push(reference);
7022+
}
70167023

7017-
this.onTracksChanged_();
7024+
const chaptersMimeType = 'text/plain';
70187025

7019-
return chaptersTrack;
7020-
}
7026+
/** @type {shaka.extern.Stream} */
7027+
const stream = {
7028+
id: this.nextExternalStreamId_++,
7029+
originalId: null,
7030+
groupId: null,
7031+
createSegmentIndex: () => Promise.resolve(),
7032+
segmentIndex: new shaka.media.SegmentIndex(references),
7033+
mimeType: chaptersMimeType,
7034+
codecs: '',
7035+
kind: '',
7036+
encrypted: false,
7037+
drmInfos: [],
7038+
keyIds: new Set(),
7039+
language: language,
7040+
originalLanguage: language,
7041+
label: null,
7042+
type: ContentType.TEXT,
7043+
primary: false,
7044+
trickModeVideo: null,
7045+
dependencyStream: null,
7046+
emsgSchemeIdUris: null,
7047+
roles: [],
7048+
forced: false,
7049+
channelsCount: null,
7050+
audioSamplingRate: null,
7051+
spatialAudio: false,
7052+
closedCaptions: null,
7053+
accessibilityPurpose: null,
7054+
external: true,
7055+
fastSwitching: false,
7056+
fullMimeTypes: new Set([shaka.util.MimeUtils.getFullType(
7057+
chaptersMimeType, '')]),
7058+
isAudioMuxedInVideo: false,
7059+
baseOriginalId: null,
7060+
};
70217061

7022-
// This should not happen, but there are browser implementations that may
7023-
// not support the Track element.
7024-
shaka.log.error('Cannot add this text when loaded with src=');
7025-
throw new shaka.util.Error(
7026-
shaka.util.Error.Severity.RECOVERABLE,
7027-
shaka.util.Error.Category.TEXT,
7028-
shaka.util.Error.Code.CANNOT_ADD_EXTERNAL_TEXT_TO_SRC_EQUALS);
7062+
this.externalChaptersStreams_.push(stream);
7063+
this.onTracksChanged_();
7064+
return shaka.util.StreamUtils.textStreamToTrack(stream);
70297065
}
70307066

70317067
/**

lib/util/error.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -355,12 +355,7 @@ shaka.util.Error.Code = {
355355
*/
356356
'MISSING_TEXT_PLUGIN': 2014,
357357

358-
/**
359-
* The chapters track failed to load. The browser does not provide any
360-
* information in this case to identify why it failed, but there may be
361-
* details in the JavaScript console.
362-
*/
363-
'CHAPTERS_TRACK_FAILED': 2015,
358+
// RETIRED: 'CHAPTERS_TRACK_FAILED': 2015,
364359

365360
// RETIRED: 'CANNOT_ADD_EXTERNAL_THUMBNAILS_TO_SRC_EQUALS': 2016,
366361

@@ -797,6 +792,11 @@ shaka.util.Error.Code = {
797792
*/
798793
'DASH_MSE_ENCRYPTED_LEGACY_APPLE_MEDIA_KEYS_NOT_SUPPORTED': 4054,
799794

795+
/**
796+
* External chapters cannot be added to live streams.
797+
*/
798+
'CANNOT_ADD_EXTERNAL_CHAPTERS_TO_LIVE_STREAM': 4054,
799+
800800
// RETIRED: 'INCONSISTENT_BUFFER_STATE': 5000,
801801
// RETIRED: 'INVALID_SEGMENT_INDEX': 5001,
802802
// RETIRED: 'SEGMENT_DOES_NOT_EXIST': 5002,

0 commit comments

Comments
 (0)