feat: add native secondary subtitle support

This commit is contained in:
Ivan Schurawel 2022-09-15 14:22:41 -04:00 committed by Ivan Schurawel
parent d69d4b22d9
commit 145aea184f
4 changed files with 192 additions and 20 deletions

View File

@ -876,6 +876,17 @@ class PlaybackManager {
});
};
self.hasSecondarySubtitleSupport = function (player = self._currentPlayer) {
if (!player) return false;
return Boolean(player.supports('SecondarySubtitles'));
};
self.secondarySubtitleTracks = function (player = self._currentPlayer) {
const streams = self.subtitleTracks(player);
// Currently, only External subtitles are supported
return streams.filter((stream) => getDeliveryMethod(stream) === 'External');
};
function getCurrentSubtitleStream(player) {
if (!player) {
throw new Error('player cannot be null');
@ -890,6 +901,20 @@ class PlaybackManager {
return getSubtitleStream(player, index);
}
function getCurrentSecondarySubtitleStream(player) {
if (!player) {
throw new Error('player cannot be null');
}
const index = getPlayerData(player).secondarySubtitleStreamIndex;
if (index == null || index === -1) {
return null;
}
return getSubtitleStream(player, index);
}
function getSubtitleStream(player, index) {
return self.subtitleTracks(player).filter(function (s) {
return s.Type === 'Subtitle' && s.Index === index;
@ -1522,9 +1547,51 @@ class PlaybackManager {
player.setSubtitleStreamIndex(selectedTrackElementIndex);
// Also disable secondary subtitles when disabling the primary subtitles
if (selectedTrackElementIndex === -1) {
self.setSecondarySubtitleStreamIndex(selectedTrackElementIndex);
}
getPlayerData(player).subtitleStreamIndex = index;
};
self.setSecondarySubtitleStreamIndex = function (index, player) {
player = player || self._currentPlayer;
if (!self.hasSecondarySubtitleSupport(player)) return;
if (player && !enableLocalPlaylistManagement(player)) {
try {
return player.setSecondarySubtitleStreamIndex(index);
} catch (e) {
console.error(`AutoSet - Failed to set secondary track: ${e}`);
}
}
const currentStream = getCurrentSecondarySubtitleStream(player);
const newStream = getSubtitleStream(player, index);
if (!currentStream && !newStream) {
return;
}
const clearingStream = currentStream && !newStream;
const changingStream = currentStream && newStream;
const addingStream = !currentStream && newStream;
// Secondary subtitles are currently only handled client side
// Changes to the server code are required before we can handle other delivery methods
if (!clearingStream && (changingStream || addingStream) && getDeliveryMethod(newStream) !== 'External') {
return;
}
getPlayerData(player).secondarySubtitleStreamIndex = index;
try {
player.setSecondarySubtitleStreamIndex(index);
} catch (e) {
console.error(`AutoSet - Failed to set secondary track: ${e}`);
}
};
self.supportSubtitleOffset = function (player) {
player = player || self._currentPlayer;
return player && 'setSubtitleOffset' in player;

View File

@ -988,9 +988,57 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
});
}
function showSecondarySubtitlesMenu(actionsheet, positionTo) {
const player = currentPlayer;
if (!playbackManager.hasSecondarySubtitleSupport(player)) return;
let currentIndex = playbackManager.getSecondarySubtitleStreamIndex(player);
const streams = playbackManager.secondarySubtitleTracks(player);
if (currentIndex == null) {
currentIndex = -1;
}
streams.unshift({
Index: -1,
DisplayTitle: globalize.translate('Off')
});
const menuItems = streams.map(function (stream) {
const opt = {
name: stream.DisplayTitle,
id: stream.Index
};
if (stream.Index === currentIndex) {
opt.selected = true;
}
return opt;
});
actionsheet.show({
title: globalize.translate('SecondarySubtitles'),
items: menuItems,
positionTo
}).then(function (id) {
if (id) {
const index = parseInt(id);
if (index !== currentIndex) {
playbackManager.setSecondarySubtitleStreamIndex(index, player);
}
}
})
.finally(() => {
resetIdle();
});
setTimeout(resetIdle, 0);
}
function showSubtitleTrackSelection() {
const player = currentPlayer;
const streams = playbackManager.subtitleTracks(player);
const secondaryStreams = playbackManager.secondarySubtitleTracks(player);
let currentIndex = playbackManager.getSubtitleStreamIndex(player);
if (currentIndex == null) {
@ -1013,19 +1061,38 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
return opt;
});
// Only show option if: player has support, has more than 1 subtitle track, has valid secondary tracks, primary subtitle is not off
if (playbackManager.hasSecondarySubtitleSupport(player) && streams.length > 1 && secondaryStreams.length > 0 && currentIndex !== -1) {
const secondarySubtitleMenuItem = {
name: globalize.translate('SecondarySubtitles'),
id: 'secondarysubtitle'
};
menuItems.unshift(secondarySubtitleMenuItem);
}
const positionTo = this;
import('../../../components/actionSheet/actionSheet').then(({default: actionsheet}) => {
actionsheet.show({
title: globalize.translate('Subtitles'),
items: menuItems,
resolveOnClick: true,
positionTo: positionTo
}).then(function (id) {
if (id === 'secondarysubtitle') {
try {
showSecondarySubtitlesMenu(actionsheet, positionTo);
} catch (e) {
console.error(e);
}
} else {
const index = parseInt(id);
if (index !== currentIndex) {
playbackManager.setSubtitleStreamIndex(index, player);
}
}
toggleSubtitleSync();
}).finally(() => {

View File

@ -178,7 +178,6 @@ function tryRemoveElement(elem) {
* @type {boolean}
*/
isFetching = false;
/**
* @type {HTMLDivElement | null | undefined}
*/
@ -207,6 +206,10 @@ function tryRemoveElement(elem) {
* @type {number | undefined}
*/
#customTrackIndex;
/**
* @type {number | undefined}
*/
#customSecondaryTrackIndex;
/**
* @type {boolean | undefined}
*/
@ -270,6 +273,14 @@ function tryRemoveElement(elem) {
* @type {any | undefined}
*/
_currentPlayOptions;
/**
* @type {number}
*/
_PRIMARY_TEXT_TRACK_INDEX = 0;
/**
* @type {number}
*/
_SECONDARY_TEXT_TRACK_INDEX = 1;
/**
* @type {any | undefined}
*/
@ -490,6 +501,10 @@ function tryRemoveElement(elem) {
this.setCurrentTrackElement(index);
}
setSecondarySubtitleStreamIndex(index) {
this.setCurrentTrackElement(index, this._SECONDARY_TEXT_TRACK_INDEX);
}
resetSubtitleOffset() {
this.#currentTrackOffset = 0;
this.#showTrackOffset = false;
@ -514,7 +529,7 @@ function tryRemoveElement(elem) {
const videoElement = this.#mediaElement;
if (videoElement) {
return Array.from(videoElement.textTracks)
.find(function (trackElement) {
.filter(function (trackElement) {
// get showing .vtt textTack
return trackElement.mode === 'showing';
});
@ -591,6 +606,10 @@ function tryRemoveElement(elem) {
return this.#currentTrackOffset;
}
isSecondaryTrack(textTrackIndex) {
return textTrackIndex === this._SECONDARY_TEXT_TRACK_INDEX;
}
/**
* @private
*/
@ -956,7 +975,9 @@ function tryRemoveElement(elem) {
/**
* @private
*/
destroyCustomTrack(videoElement) {
destroyCustomTrack(videoElement, targetTrackIndex) {
const destroySingleTrack = typeof targetTrackIndex === 'number';
if (this.#videoSubtitlesElem) {
const subtitlesContainer = this.#videoSubtitlesElem.parentNode;
if (subtitlesContainer) {
@ -969,7 +990,11 @@ function tryRemoveElement(elem) {
if (videoElement) {
const allTracks = videoElement.textTracks || []; // get list of tracks
for (const track of allTracks) {
for (let index = 0; index < allTracks.length; index++) {
const track = allTracks[index];
if (destroySingleTrack && targetTrackIndex !== index) {
continue;
}
if (track.label.includes('manualTrack')) {
track.mode = 'disabled';
}
@ -1029,23 +1054,34 @@ function tryRemoveElement(elem) {
/**
* @private
*/
setTrackForDisplay(videoElement, track) {
setTrackForDisplay(videoElement, track, targetTextTrackIndex = this._PRIMARY_TEXT_TRACK_INDEX) {
if (!track) {
this.destroyCustomTrack(videoElement);
// Destroy all tracks by passing undefined if there is no valid primary track
this.destroyCustomTrack(videoElement, this.isSecondaryTrack(targetTextTrackIndex) ? targetTextTrackIndex : undefined);
return;
}
let targetTrackIndex = this.#customTrackIndex;
if (this.isSecondaryTrack(targetTextTrackIndex)) {
targetTrackIndex = this.#customSecondaryTrackIndex;
}
// skip if already playing this track
if (this.#customTrackIndex === track.Index) {
if (targetTrackIndex === track.Index) {
return;
}
this.resetSubtitleOffset();
const item = this._currentPlayOptions.item;
this.destroyCustomTrack(videoElement);
this.destroyCustomTrack(videoElement, targetTextTrackIndex);
if (this.isSecondaryTrack(targetTextTrackIndex)) {
this.#customSecondaryTrackIndex = track.Index;
} else {
this.#customTrackIndex = track.Index;
this.renderTracksEvents(videoElement, track, item);
}
this.renderTracksEvents(videoElement, track, item, targetTextTrackIndex);
}
/**
@ -1211,7 +1247,7 @@ function tryRemoveElement(elem) {
/**
* @private
*/
renderTracksEvents(videoElement, track, item) {
renderTracksEvents(videoElement, track, item, targetTextTrackIndex = this._PRIMARY_TEXT_TRACK_INDEX) {
if (!itemHelper.isLocalItem(item) || track.IsExternal) {
const format = (track.Codec || '').toLowerCase();
if (format === 'ssa' || format === 'ass') {
@ -1220,15 +1256,15 @@ function tryRemoveElement(elem) {
}
if (this.requiresCustomSubtitlesElement()) {
this.renderSubtitlesWithCustomElement(videoElement, track, item);
this.renderSubtitlesWithCustomElement(videoElement, track, item, targetTextTrackIndex);
return;
}
}
let trackElement = null;
if (videoElement.textTracks && videoElement.textTracks.length > 0) {
trackElement = videoElement.textTracks[0];
const updatingTrack = videoElement.textTracks && videoElement.textTracks.length > (this.isSecondaryTrack(targetTextTrackIndex) ? 1 : 0);
if (updatingTrack) {
trackElement = videoElement.textTracks[targetTextTrackIndex];
// This throws an error in IE, but is fine in chrome
// In IE it's not necessary anyway because changing the src seems to be enough
try {
@ -1313,7 +1349,7 @@ function tryRemoveElement(elem) {
/**
* @private
*/
setCurrentTrackElement(streamIndex) {
setCurrentTrackElement(streamIndex, targetTextTrackIndex) {
console.debug(`setting new text track index to: ${streamIndex}`);
const mediaStreamTextTracks = getMediaStreamTextTracks(this._currentPlayOptions.mediaSource);
@ -1322,7 +1358,7 @@ function tryRemoveElement(elem) {
return t.Index === streamIndex;
})[0];
this.setTrackForDisplay(this.#mediaElement, track);
this.setTrackForDisplay(this.#mediaElement, track, targetTextTrackIndex);
if (enableNativeTrackSupport(this.#currentSrc, track)) {
if (streamIndex !== -1) {
this.setCueAppearance();
@ -1500,6 +1536,7 @@ function tryRemoveElement(elem) {
list.push('SetBrightness');
list.push('SetAspectRatio');
list.push('SecondarySubtitles');
return list;
}

View File

@ -1412,6 +1412,7 @@
"SearchForSubtitles": "Search for Subtitles",
"SearchResults": "Search Results",
"Season": "Season",
"SecondarySubtitles": "Secondary Subtitles",
"SelectAdminUsername": "Please select a username for the admin account.",
"SelectServer": "Select Server",
"SendMessage": "Send message",