Skip to content

Commit 724b0b2

Browse files
xxooavelad
andauthored
feat: new TextDisplayer implementation to allow selecting subtitles via native API and controls (#8520)
Close #8519 Fixes #8475 Introduce `NativeTextDisplayer` as a replacement of `SimpleTextDisplayer`. But keep them both work. Is MSE mode, `NativeTextDisplayer` creates `<track>` elements for text streams. And listens to change events on both ends to keep them in sync. In SRC mode, `NativeTextDisplayer` would do nothing, the player uses original TextTracks instead. Advantages of `NativeTextDisplayer`: - Allow text track selection using the browser built-in UI - Allow text track manipulation using native APIs - Avoid transferring and processing cues in SRC mode --------- Co-authored-by: Álvaro Velad Galván <[email protected]>
1 parent 023d06a commit 724b0b2

16 files changed

+1187
-218
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,4 @@ João Nabais <[email protected]>
119119
Koen Romers <[email protected]>
120120
Zhenghang Chen <[email protected]>
121121
Xperi <*@xperi.com>
122+
Shen Xiao <[email protected]>

CONTRIBUTORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,4 @@ Zhenghang Chen <[email protected]>
168168
Ashley Manners <[email protected]>
169169
Bidisha Das <[email protected]>
170170
Chafroud Tarek <[email protected]>
171+
Shen Xiao <[email protected]>

build/types/core

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464

6565
+../../lib/text/cue.js
6666
+../../lib/text/cue_region.js
67+
+../../lib/text/native_text_displayer.js
6768
+../../lib/text/simple_text_displayer.js
6869
+../../lib/text/stub_text_displayer.js
6970
+../../lib/text/text_engine.js
@@ -131,4 +132,6 @@
131132
+../../third_party/closure-uri/uri.js
132133
+../../third_party/closure-uri/utils.js
133134

135+
+../../third_party/language-mapping-list/language-mapping-list.js
136+
134137
+@lcevc

build/types/ui

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# UI library.
22

3-
+../../third_party/language-mapping-list/language-mapping-list.js
43
+../../ui/ad_info.js
54
+../../ui/ad_statistics_button.js
65
+../../ui/audio_language_selection.js

demo/main.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1410,7 +1410,7 @@ shakaDemo.Main = class {
14101410
if (this.nativeControlsEnabled_) {
14111411
this.controls_.setEnabledShakaControls(false);
14121412
this.controls_.setEnabledNativeControls(true);
1413-
// This will force the player to use SimpleTextDisplayer.
1413+
// This will force the player to use NativeTextDisplayer.
14141414
this.player_.setVideoContainer(null);
14151415
} else {
14161416
this.controls_.setEnabledShakaControls(true);

docs/tutorials/plugins.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ __Subtitle/caption displayers__
4949
- Use {@link player.configure} and set the `textDisplayFactory` field
5050
- Must implement the {@link shaka.extern.TextDisplayer} interface
5151
- Default TextDisplayer implementation:
52-
{@linksource shaka.text.SimpleTextDisplayer}
52+
{@linksource shaka.text.NativeTextDisplayer}
5353

5454
__Networking plugins__
5555
- Selected by URI scheme (http, https, etc.)

docs/tutorials/text-displayer.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
Shaka Player supports two implementations of {@link shaka.extern.TextDisplayer}
66
which can be used to render & style content subtitles.
77

8-
#### SimpleTextDisplayer
8+
#### NativeTextDisplayer
99

10-
{@link shaka.text.SimpleTextDisplayer} which uses browser's native cue
11-
renderer. Shaka Player creates a custom text track attached to the video
12-
element and provides necessary data so video element can render it. This is
13-
the default displayer when shaka UI is **not** used.
10+
{@link shaka.text.NativeTextDisplayer} which uses browser's native cue
11+
renderer. Shaka Player creates corresponding text tracks for text streams on
12+
the video element and provides necessary data so video element can render it.
13+
This is the default displayer when shaka UI is **not** used.
1414

1515
#### UITextDisplayer
1616

lib/player.js

Lines changed: 103 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ goog.require('shaka.media.TimeRangesUtils');
3535
goog.require('shaka.net.NetworkingEngine');
3636
goog.require('shaka.net.NetworkingUtils');
3737
goog.require('shaka.text.Cue');
38+
goog.require('shaka.text.NativeTextDisplayer');
3839
goog.require('shaka.text.SimpleTextDisplayer');
3940
goog.require('shaka.text.StubTextDisplayer');
4041
goog.require('shaka.text.TextEngine');
@@ -2575,15 +2576,21 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
25752576
this.loadEventManager_.listen(mediaElement, 'ended', updateStateHistory);
25762577
this.loadEventManager_.listen(mediaElement, 'ratechange', onRateChange);
25772578
if (mediaElement.remote) {
2578-
this.loadEventManager_.listen(mediaElement.remote, 'connect',
2579-
() => this.onTracksChanged_());
2579+
this.loadEventManager_.listen(mediaElement.remote, 'connect', () => {
2580+
if (this.streamingEngine_ &&
2581+
mediaElement.remote.state == 'connected') {
2582+
this.onTextChanged_();
2583+
}
2584+
this.onTracksChanged_();
2585+
});
25802586
this.loadEventManager_.listen(mediaElement.remote, 'connecting',
25812587
() => this.onTracksChanged_());
25822588
this.loadEventManager_.listen(mediaElement.remote, 'disconnect',
25832589
async () => {
25842590
if (this.streamingEngine_ &&
25852591
mediaElement.remote.state == 'disconnected') {
25862592
await this.streamingEngine_.resetMediaSource();
2593+
this.onTextChanged_();
25872594
}
25882595
this.onTracksChanged_();
25892596
});
@@ -2606,6 +2613,13 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
26062613
}
26072614

26082615
if (mediaElement.textTracks) {
2616+
const trackChange = () => {
2617+
if (this.loadMode_ === shaka.Player.LoadMode.SRC_EQUALS &&
2618+
this.textDisplayer_ instanceof shaka.text.NativeTextDisplayer) {
2619+
this.onTextChanged_();
2620+
}
2621+
this.onTracksChanged_();
2622+
};
26092623
this.loadEventManager_.listen(
26102624
mediaElement.textTracks, 'addtrack', (e) => {
26112625
const trackEvent = /** @type {!TrackEvent} */(e);
@@ -2624,15 +2638,15 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
26242638
break;
26252639

26262640
default:
2627-
this.onTracksChanged_();
2641+
trackChange();
26282642
break;
26292643
}
26302644
}
26312645
});
26322646
this.loadEventManager_.listen(mediaElement.textTracks, 'removetrack',
2633-
() => this.onTracksChanged_());
2647+
trackChange);
26342648
this.loadEventManager_.listen(mediaElement.textTracks, 'change',
2635-
() => this.onTracksChanged_());
2649+
trackChange);
26362650

26372651
if (this.config_.streaming.crossBoundaryStrategy !==
26382652
shaka.config.CrossBoundaryStrategy.KEEP) {
@@ -3121,55 +3135,43 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
31213135

31223136
if (mediaElement.textTracks) {
31233137
this.createTextDisplayer_();
3124-
const setShowingMode = () => {
3125-
const track = this.getFilteredTextTracks_()
3126-
.find((t) => t.mode !== 'disabled');
3127-
if (track) {
3128-
track.mode = 'showing';
3129-
}
3130-
const generatedTrack = this.getGeneratedTextTrack_();
3131-
if (generatedTrack) {
3132-
generatedTrack.mode = 'hidden';
3133-
}
3134-
};
3135-
const setHiddenMode = () => {
3136-
const track = this.getFilteredTextTracks_()
3137-
.find((t) => t.mode !== 'disabled');
3138-
if (track) {
3139-
track.mode = 'hidden';
3140-
}
3141-
const generatedTrack = this.getGeneratedTextTrack_();
3142-
const isTextVisible = this.textDisplayer_.isTextVisible();
3143-
if (generatedTrack && isTextVisible) {
3144-
generatedTrack.mode = 'showing';
3138+
const setMode = (showing) => {
3139+
if (!(this.textDisplayer_ instanceof shaka.text.NativeTextDisplayer)) {
3140+
const track = this.getFilteredTextTracks_()
3141+
.find((t) => t.mode !== 'disabled');
3142+
if (track) {
3143+
track.mode = showing ? 'showing' : 'hidden';
3144+
}
3145+
if (this.textDisplayer_ instanceof shaka.text.SimpleTextDisplayer) {
3146+
const generatedTrack = this.getGeneratedTextTrack_();
3147+
if (generatedTrack) {
3148+
generatedTrack.mode =
3149+
!showing && this.textDisplayer_.isTextVisible() ?
3150+
'showing' : 'hidden';
3151+
}
3152+
}
31453153
}
31463154
};
31473155
this.loadEventManager_.listen(mediaElement, 'enterpictureinpicture',
3148-
() => setShowingMode());
3156+
() => setMode(true));
31493157
this.loadEventManager_.listen(mediaElement, 'leavepictureinpicture',
3150-
() => setHiddenMode());
3158+
() => setMode(false));
31513159
if (mediaElement.remote) {
31523160
this.loadEventManager_.listen(mediaElement.remote, 'connect',
3153-
() => setHiddenMode());
3161+
() => setMode(false));
31543162
this.loadEventManager_.listen(mediaElement.remote, 'connecting',
3155-
() => setHiddenMode());
3163+
() => setMode(false));
31563164
this.loadEventManager_.listen(mediaElement.remote, 'disconnect',
3157-
() => setHiddenMode());
3165+
() => setMode(false));
31583166
} else if ('webkitCurrentPlaybackTargetIsWireless' in mediaElement) {
31593167
this.loadEventManager_.listen(mediaElement,
31603168
'webkitcurrentplaybacktargetiswirelesschanged',
3161-
() => setHiddenMode());
3169+
() => setMode(false));
31623170
}
31633171
const video = /** @type {HTMLVideoElement} */(mediaElement);
31643172
if (video.webkitSupportsFullscreen) {
31653173
this.loadEventManager_.listen(video, 'webkitpresentationmodechanged',
3166-
() => {
3167-
if (video.webkitPresentationMode != 'inline') {
3168-
setShowingMode();
3169-
} else {
3170-
setHiddenMode();
3171-
}
3172-
});
3174+
() => setMode(video.webkitPresentationMode !== 'inline'));
31733175
}
31743176
}
31753177
// Add all media element listeners.
@@ -3273,27 +3275,33 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
32733275
this.textDisplayer_.setTextVisibility(true);
32743276
}
32753277

3276-
if (textTracks.length) {
3277-
if (this.textDisplayer_.enableTextDisplayer) {
3278-
this.textDisplayer_.enableTextDisplayer();
3279-
} else {
3280-
shaka.Deprecate.deprecateFeature(5,
3281-
'Text displayer w/ enableTextDisplayer',
3282-
'Text displayer should have a "enableTextDisplayer" method!');
3278+
if (
3279+
!(this.textDisplayer_ instanceof shaka.text.NativeTextDisplayer)
3280+
) {
3281+
if (textTracks.length) {
3282+
if (this.textDisplayer_.enableTextDisplayer) {
3283+
this.textDisplayer_.enableTextDisplayer();
3284+
} else {
3285+
shaka.Deprecate.deprecateFeature(
3286+
5,
3287+
'Text displayer w/ enableTextDisplayer',
3288+
'Text displayer should have a "enableTextDisplayer" method',
3289+
);
3290+
}
32833291
}
3284-
}
32853292

3286-
let enabledNativeTrack = false;
3287-
for (const track of textTracks) {
3288-
if (track.mode !== 'disabled') {
3289-
if (!enabledNativeTrack) {
3290-
this.enableNativeTrack_(track);
3291-
enabledNativeTrack = true;
3292-
} else {
3293-
track.mode = 'disabled';
3294-
shaka.log.alwaysWarn(
3295-
'Found more than one enabled text track, disabling it',
3296-
track);
3293+
let enabledNativeTrack = false;
3294+
for (const track of textTracks) {
3295+
if (track.mode !== 'disabled') {
3296+
if (!enabledNativeTrack) {
3297+
this.enableNativeTrack_(track);
3298+
enabledNativeTrack = true;
3299+
} else {
3300+
track.mode = 'disabled';
3301+
shaka.log.alwaysWarn(
3302+
'Found more than one enabled text track, disabling it',
3303+
track);
3304+
}
32973305
}
32983306
}
32993307
}
@@ -5222,20 +5230,24 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
52225230
* @export
52235231
*/
52245232
getTextTracks() {
5225-
if (this.manifest_ && !this.isRemotePlayback()) {
5226-
const currentTextStream = this.streamingEngine_ ?
5233+
if (this.manifest_) {
5234+
if (this.isRemotePlayback()) {
5235+
return [];
5236+
} else {
5237+
const currentTextStream = this.streamingEngine_ ?
52275238
this.streamingEngine_.getCurrentTextStream() : null;
5228-
const tracks = [];
5239+
const tracks = [];
52295240

5230-
// Convert all selectable text streams to tracks.
5231-
for (const text of this.manifest_.textStreams) {
5232-
const track = shaka.util.StreamUtils.textStreamToTrack(text);
5233-
track.active = text == currentTextStream;
5241+
// Convert all selectable text streams to tracks.
5242+
for (const text of this.manifest_.textStreams) {
5243+
const track = shaka.util.StreamUtils.textStreamToTrack(text);
5244+
track.active = text == currentTextStream;
52345245

5235-
tracks.push(track);
5236-
}
5246+
tracks.push(track);
5247+
}
52375248

5238-
return tracks;
5249+
return tracks;
5250+
}
52395251
} else if (this.video_ && this.video_.src && this.video_.textTracks) {
52405252
const textTracks = this.getFilteredTextTracks_();
52415253
const StreamUtils = shaka.util.StreamUtils;
@@ -5501,22 +5513,33 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
55015513
const selectSrcEqualsMode = () => {
55025514
if (this.video_ && this.video_.textTracks) {
55035515
const textTracks = this.getFilteredTextTracks_();
5504-
const oldTrack = textTracks.find((textTrack) =>
5505-
textTrack.mode !== 'disabled');
55065516
const newTrack = textTracks.find((textTrack) =>
55075517
shaka.util.StreamUtils.html5TrackId(textTrack) === track.id);
55085518
if (!newTrack) {
55095519
shaka.log.error('No track with id', track.id);
55105520
return;
55115521
}
5512-
if (oldTrack !== newTrack) {
5513-
if (oldTrack) {
5514-
oldTrack.mode = 'disabled';
5515-
this.loadEventManager_.unlisten(oldTrack, 'cuechange');
5516-
this.textDisplayer_.remove(0, Infinity);
5522+
if (this.textDisplayer_ instanceof shaka.text.NativeTextDisplayer) {
5523+
for (const texTrack of textTracks) {
5524+
const mode = texTrack === newTrack ?
5525+
this.isTextVisible_ ? 'showing' : 'hidden' :
5526+
'disabled';
5527+
if (texTrack.mode !== mode) {
5528+
texTrack.mode = mode;
5529+
}
55175530
}
5518-
if (newTrack) {
5519-
this.enableNativeTrack_(newTrack);
5531+
} else {
5532+
const oldTrack = textTracks.find((textTrack) =>
5533+
textTrack.mode !== 'disabled');
5534+
if (oldTrack !== newTrack) {
5535+
if (oldTrack) {
5536+
oldTrack.mode = 'disabled';
5537+
this.loadEventManager_.unlisten(oldTrack, 'cuechange');
5538+
this.textDisplayer_.remove(0, Infinity);
5539+
}
5540+
if (newTrack) {
5541+
this.enableNativeTrack_(newTrack);
5542+
}
55205543
}
55215544
}
55225545
this.onTextChanged_();
@@ -7388,17 +7411,16 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
73887411
// TextDisplay factory must capture a reference to "this".
73897412
config.textDisplayFactory = () => {
73907413
// On iOS where the Fullscreen API is not available we prefer
7391-
// SimpleTextDisplayer because it works with the Fullscreen API of the
7414+
// NativeTextDisplayer because it works with the Fullscreen API of the
73927415
// video element itself.
73937416
const Platform = shaka.util.Platform;
73947417
if (this.videoContainer_ &&
73957418
(!Platform.isApple() || document.fullscreenEnabled)) {
73967419
return new shaka.text.UITextDisplayer(
73977420
this.video_, this.videoContainer_);
73987421
} else {
7399-
if ('addTextTrack' in this.video_) {
7400-
return new shaka.text.SimpleTextDisplayer(
7401-
this.video_, shaka.Player.TextTrackLabel);
7422+
if ('track' in document.createElement('track')) {
7423+
return new shaka.text.NativeTextDisplayer(this);
74027424
} else {
74037425
shaka.log.warning('Text tracks are not supported by the ' +
74047426
'browser, disabling.');

0 commit comments

Comments
 (0)