Skip to content

Commit 2b5319f

Browse files
committed
Render embedded ASS subtitles in Web players
1 parent 1e95245 commit 2b5319f

3 files changed

Lines changed: 344 additions & 73 deletions

File tree

src/HTMLVideo/HTMLVideo.js

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,19 @@ function HTMLVideo(options) {
195195
return [];
196196
}
197197

198+
if (hls !== null && Array.isArray(hls.subtitleTracks)) {
199+
return hls.subtitleTracks.map(function(track, index) {
200+
return Object.freeze({
201+
id: 'EMBEDDED_' + String(index),
202+
lang: typeof track.lang === 'string' && track.lang.length > 0 ? track.lang : '',
203+
label: typeof track.name === 'string' && track.name.length > 0 ? track.name : null,
204+
origin: 'EMBEDDED',
205+
embedded: true,
206+
_hlsSubtitlePlaylistURL: typeof track.url === 'string' ? track.url : null
207+
});
208+
});
209+
}
210+
198211
return Array.from(videoElement.textTracks)
199212
.map(function(track, index) {
200213
return Object.freeze({
@@ -211,6 +224,10 @@ function HTMLVideo(options) {
211224
return null;
212225
}
213226

227+
if (hls !== null) {
228+
return hls.subtitleTrack >= 0 ? 'EMBEDDED_' + String(hls.subtitleTrack) : null;
229+
}
230+
214231
return Array.from(videoElement.textTracks)
215232
.reduce(function(result, track, index) {
216233
if (result === null && track.mode === 'showing') {
@@ -422,10 +439,17 @@ function HTMLVideo(options) {
422439
}
423440
case 'selectedSubtitlesTrackId': {
424441
if (stream !== null) {
425-
Array.from(videoElement.textTracks)
426-
.forEach(function(track, index) {
427-
track.mode = 'EMBEDDED_' + String(index) === propValue ? 'showing' : 'disabled';
442+
if (hls !== null) {
443+
var selectedIndex = getProp('subtitlesTracks').findIndex(function(track) {
444+
return track.id === propValue;
428445
});
446+
hls.subtitleTrack = selectedIndex;
447+
} else {
448+
Array.from(videoElement.textTracks)
449+
.forEach(function(track, index) {
450+
track.mode = 'EMBEDDED_' + String(index) === propValue ? 'showing' : 'disabled';
451+
});
452+
}
429453
var selecterdSubtitlesTrack = getProp('subtitlesTracks')
430454
.find(function(track) {
431455
return track.id === propValue;
@@ -619,6 +643,17 @@ function HTMLVideo(options) {
619643
onPropChanged('audioTracks');
620644
onPropChanged('selectedAudioTrackId');
621645
});
646+
hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, function() {
647+
onPropChanged('subtitlesTracks');
648+
onPropChanged('selectedSubtitlesTrackId');
649+
});
650+
hls.on(Hls.Events.SUBTITLE_TRACKS_CLEARED, function() {
651+
onPropChanged('subtitlesTracks');
652+
onPropChanged('selectedSubtitlesTrackId');
653+
});
654+
hls.on(Hls.Events.SUBTITLE_TRACK_SWITCH, function() {
655+
onPropChanged('selectedSubtitlesTrackId');
656+
});
622657
hls.on(Hls.Events.MANIFEST_LOADING, function() {
623658
hls.subtitleTrack = -1;
624659
});

src/withHTMLSubtitles/withHTMLSubtitles.js

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ function withHTMLSubtitles(Video) {
3737
var assRenderer = createASSRenderer({
3838
containerElement: containerElement,
3939
onError: function(error, track) {
40+
if (track && track.id === selectedEmbeddedASSTrackId) {
41+
fallbackFromEmbeddedASS(error, track);
42+
return;
43+
}
4044
fallbackFromASS(error, track, selectedSubtitleText)
4145
.catch(function(fallbackError) {
4246
reportSubtitleError(fallbackError, track);
@@ -141,6 +145,10 @@ function withHTMLSubtitles(Video) {
141145
var usingASSRenderer = false;
142146
var selectedSubtitleText = null;
143147
var selectionRequestId = 0;
148+
var embeddedASSSources = [];
149+
var videoSubtitleTracks = [];
150+
var selectedEmbeddedASSTrackId = null;
151+
var embeddedSubtitlesOpacity = 1;
144152
var nativeAssSubtitlesSupported = options.shellTransport &&
145153
options.shellTransport.capabilities &&
146154
options.shellTransport.capabilities.nativeAssSubtitles === true;
@@ -277,6 +285,7 @@ function withHTMLSubtitles(Video) {
277285
break;
278286
}
279287
case 'subtitlesTracks': {
288+
videoSubtitleTracks = Array.isArray(propValue) ? propValue : [];
280289
updateNativeASSTrack(propValue);
281290
break;
282291
}
@@ -371,11 +380,33 @@ function withHTMLSubtitles(Video) {
371380
}
372381
function getPublicSubtitleTrack(track) {
373382
var publicTrack = Object.assign({}, track);
383+
if (getEmbeddedASSSource(track) !== null) {
384+
publicTrack.ass = true;
385+
}
374386
delete publicTrack.nativeExternal;
375387
delete publicTrack.nativeExternalFilename;
376388
delete publicTrack.nativeExternalTitle;
389+
delete publicTrack._hlsSubtitlePlaylistURL;
377390
return publicTrack;
378391
}
392+
function getEmbeddedSubtitleId(track) {
393+
if (!track || typeof track._hlsSubtitlePlaylistURL !== 'string') {
394+
return null;
395+
}
396+
397+
var match = track._hlsSubtitlePlaylistURL.match(/(?:^|\/)subtitle(\d+)\.m3u8(?:$|[?#])/i);
398+
return match === null ? null : match[1];
399+
}
400+
function getEmbeddedASSSource(track) {
401+
var subtitleId = getEmbeddedSubtitleId(track);
402+
if (subtitleId === null) {
403+
return null;
404+
}
405+
406+
return embeddedASSSources.find(function(source) {
407+
return String(source.id) === subtitleId;
408+
}) || null;
409+
}
379410
function isManagedNativeASSTrack(track) {
380411
return track.id === nativeAssTrackId ||
381412
removingNativeAssTrackIds.includes(track.id) ||
@@ -503,6 +534,78 @@ function withHTMLSubtitles(Video) {
503534
loadPlainSubtitles(track, text, true, currentRequestId) :
504535
Promise.resolve(null);
505536
}
537+
function clearEmbeddedASS() {
538+
if (selectedEmbeddedASSTrackId === null) {
539+
return;
540+
}
541+
542+
selectedEmbeddedASSTrackId = null;
543+
usingASSRenderer = false;
544+
setASSSubtitlesStylingActive(false);
545+
assRenderer.destroy();
546+
subtitlesElement.style.display = '';
547+
if (selectedTrackId === null) {
548+
stopRenderLoop();
549+
}
550+
}
551+
function fallbackFromEmbeddedASS(error, track, currentRequestId) {
552+
currentRequestId = currentRequestId || selectionRequestId;
553+
if (!track || currentRequestId !== selectionRequestId || selectedEmbeddedASSTrackId !== track.id) {
554+
return;
555+
}
556+
557+
var trackId = selectedEmbeddedASSTrackId;
558+
reportSubtitleError(error, getPublicSubtitleTrack(track));
559+
clearEmbeddedASS();
560+
video.dispatch({
561+
type: 'setProp',
562+
propName: 'selectedSubtitlesTrackId',
563+
propValue: trackId,
564+
});
565+
}
566+
function loadEmbeddedASS(track, source, currentRequestId) {
567+
var assTrack = Object.assign({}, getPublicSubtitleTrack(track), {
568+
ass: true,
569+
codec: source.codec,
570+
fonts: Array.isArray(source.fonts) ? source.fonts : [],
571+
url: source.url,
572+
});
573+
574+
cuesByTime = null;
575+
usingASSRenderer = true;
576+
setASSSubtitlesStylingActive(false);
577+
subtitlesElement.style.display = 'none';
578+
while (subtitlesElement.hasChildNodes()) {
579+
subtitlesElement.removeChild(subtitlesElement.lastChild);
580+
}
581+
lastTimeIndex = null;
582+
assRenderer.setDelay(0);
583+
assRenderer.setOpacity(embeddedSubtitlesOpacity);
584+
startRenderLoop();
585+
586+
return getSubtitlesData(assTrack, false)
587+
.then(function(text) {
588+
if (currentRequestId !== selectionRequestId || selectedEmbeddedASSTrackId !== track.id) {
589+
return null;
590+
}
591+
592+
return assRenderer.load(assTrack, text);
593+
})
594+
.then(function(loadedTrack) {
595+
if (currentRequestId !== selectionRequestId || selectedEmbeddedASSTrackId !== track.id) {
596+
return null;
597+
}
598+
if (loadedTrack !== null) {
599+
setASSSubtitlesStylingActive(true);
600+
events.emit('subtitlesTrackLoaded', getPublicSubtitleTrack(track));
601+
}
602+
return loadedTrack;
603+
})
604+
.catch(function(error) {
605+
fallbackFromEmbeddedASS(error, assTrack, currentRequestId);
606+
return null;
607+
});
608+
}
506609
function loadASSSubtitles(track, text, currentRequestId) {
507610
cuesByTime = null;
508611
usingASSRenderer = true;
@@ -692,6 +795,9 @@ function withHTMLSubtitles(Video) {
692795
return videoPropValue;
693796
}
694797
case 'selectedSubtitlesTrackId': {
798+
if (selectedEmbeddedASSTrackId !== null) {
799+
return selectedEmbeddedASSTrackId;
800+
}
695801
if (isNativeASSSelection(videoPropValue)) {
696802
return null;
697803
}
@@ -739,10 +845,44 @@ function withHTMLSubtitles(Video) {
739845
}
740846
function setProp(propName, propValue) {
741847
switch (propName) {
848+
case 'selectedSubtitlesTrackId': {
849+
var selectedVideoTrack = videoSubtitleTracks.find(function(track) {
850+
return track.id === propValue;
851+
});
852+
var embeddedASSSource = getEmbeddedASSSource(selectedVideoTrack);
853+
if (assSubtitlesStylingEnabled && selectedVideoTrack && embeddedASSSource !== null) {
854+
if (selectedEmbeddedASSTrackId === propValue) {
855+
return true;
856+
}
857+
if (selectedTrackId !== null) {
858+
setProp('selectedExtraSubtitlesTrackId', null);
859+
}
860+
var embeddedRequestId = ++selectionRequestId;
861+
clearEmbeddedASS();
862+
selectedEmbeddedASSTrackId = propValue;
863+
video.dispatch({
864+
type: 'setProp',
865+
propName: 'selectedSubtitlesTrackId',
866+
propValue: null,
867+
});
868+
loadEmbeddedASS(selectedVideoTrack, embeddedASSSource, embeddedRequestId);
869+
return true;
870+
}
871+
if (selectedEmbeddedASSTrackId !== null) {
872+
selectionRequestId = selectionRequestId + 1;
873+
clearEmbeddedASS();
874+
}
875+
876+
return false;
877+
}
742878
case 'selectedExtraSubtitlesTrackId': {
743879
if (propValue !== null && selectedTrackId === propValue) {
744880
return true;
745881
}
882+
if (propValue !== null && selectedEmbeddedASSTrackId !== null) {
883+
selectionRequestId = selectionRequestId + 1;
884+
clearEmbeddedASS();
885+
}
746886
clearNativeASS();
747887
if (propValue !== null) {
748888
video.dispatch({
@@ -888,6 +1028,16 @@ function withHTMLSubtitles(Video) {
8881028

8891029
return true;
8901030
}
1031+
case 'subtitlesOpacity': {
1032+
if (typeof propValue === 'number') {
1033+
embeddedSubtitlesOpacity = Math.min(Math.max(propValue / 100, 0), 1);
1034+
if (selectedEmbeddedASSTrackId !== null) {
1035+
assRenderer.setOpacity(embeddedSubtitlesOpacity);
1036+
}
1037+
}
1038+
1039+
return false;
1040+
}
8911041
default: {
8921042
return false;
8931043
}
@@ -947,6 +1097,10 @@ function withHTMLSubtitles(Video) {
9471097
case 'load': {
9481098
command('unload');
9491099
assSubtitlesStylingEnabled = commandArgs.assSubtitlesStyling === true;
1100+
embeddedASSSources = commandArgs.stream && Array.isArray(commandArgs.stream._embeddedASSSources) ?
1101+
commandArgs.stream._embeddedASSSources
1102+
:
1103+
[];
9501104
if (commandArgs.stream && Array.isArray(commandArgs.stream.subtitles)) {
9511105
command('addExtraSubtitlesTracks', {
9521106
tracks: commandArgs.stream.subtitles.map(function(track) {
@@ -973,6 +1127,10 @@ function withHTMLSubtitles(Video) {
9731127
tracks = [];
9741128
selectedTrackId = null;
9751129
selectedSubtitleText = null;
1130+
embeddedASSSources = [];
1131+
videoSubtitleTracks = [];
1132+
selectedEmbeddedASSTrackId = null;
1133+
embeddedSubtitlesOpacity = 1;
9761134
delay = null;
9771135
usingASSRenderer = false;
9781136
setASSSubtitlesStylingActive(false);

0 commit comments

Comments
 (0)