Merge pull request #3906 from is343/feat/secondary-subtitles-htmlplayer
Add secondary subtitle support to html video player
This commit is contained in:
commit
c47b843c05
5 changed files with 463 additions and 84 deletions
|
@ -422,7 +422,8 @@ function getPlaybackInfo(player,
|
||||||
enableDirectPlay,
|
enableDirectPlay,
|
||||||
enableDirectStream,
|
enableDirectStream,
|
||||||
allowVideoStreamCopy,
|
allowVideoStreamCopy,
|
||||||
allowAudioStreamCopy) {
|
allowAudioStreamCopy,
|
||||||
|
secondarySubtitleStreamIndex) {
|
||||||
if (!itemHelper.isLocalItem(item) && item.MediaType === 'Audio' && !player.useServerPlaybackInfoForAudio) {
|
if (!itemHelper.isLocalItem(item) && item.MediaType === 'Audio' && !player.useServerPlaybackInfoForAudio) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
MediaSources: [
|
MediaSources: [
|
||||||
|
@ -462,6 +463,9 @@ function getPlaybackInfo(player,
|
||||||
if (subtitleStreamIndex != null) {
|
if (subtitleStreamIndex != null) {
|
||||||
query.SubtitleStreamIndex = subtitleStreamIndex;
|
query.SubtitleStreamIndex = subtitleStreamIndex;
|
||||||
}
|
}
|
||||||
|
if (secondarySubtitleStreamIndex != null) {
|
||||||
|
query.SecondarySubtitleStreamIndex = secondarySubtitleStreamIndex;
|
||||||
|
}
|
||||||
if (enableDirectPlay != null) {
|
if (enableDirectPlay != null) {
|
||||||
query.EnableDirectPlay = enableDirectPlay;
|
query.EnableDirectPlay = enableDirectPlay;
|
||||||
}
|
}
|
||||||
|
@ -876,25 +880,49 @@ class PlaybackManager {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function getCurrentSubtitleStream(player) {
|
self.playerHasSecondarySubtitleSupport = function (player = self._currentPlayer) {
|
||||||
|
if (!player) return false;
|
||||||
|
return Boolean(player.supports('SecondarySubtitles'));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if:
|
||||||
|
* - the track can be used directly as a secondary subtitle
|
||||||
|
* - or if it can be paired with a secondary subtitle when used as a primary subtitle
|
||||||
|
*/
|
||||||
|
self.trackHasSecondarySubtitleSupport = function (track, player = self._currentPlayer) {
|
||||||
|
if (!player || !track) return false;
|
||||||
|
const format = (track.Codec || '').toLowerCase();
|
||||||
|
// Currently, only non-SSA/non-ASS external subtitles are supported.
|
||||||
|
// Showing secondary subtitles does not work with any SSA/ASS subtitle combinations because
|
||||||
|
// of the complexity of how they are rendered and the risk of the subtitles overlapping
|
||||||
|
return format !== 'ssa' && format !== 'ass' && getDeliveryMethod(track) === 'External';
|
||||||
|
};
|
||||||
|
|
||||||
|
self.secondarySubtitleTracks = function (player = self._currentPlayer) {
|
||||||
|
const streams = self.subtitleTracks(player);
|
||||||
|
return streams.filter((stream) => self.trackHasSecondarySubtitleSupport(stream, player));
|
||||||
|
};
|
||||||
|
|
||||||
|
function getCurrentSubtitleStream(player, isSecondaryStream = false) {
|
||||||
if (!player) {
|
if (!player) {
|
||||||
throw new Error('player cannot be null');
|
throw new Error('player cannot be null');
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = getPlayerData(player).subtitleStreamIndex;
|
const index = isSecondaryStream ? getPlayerData(player).secondarySubtitleStreamIndex : getPlayerData(player).subtitleStreamIndex;
|
||||||
|
|
||||||
if (index == null || index === -1) {
|
if (index == null || index === -1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getSubtitleStream(player, index);
|
return self.getSubtitleStream(player, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSubtitleStream(player, index) {
|
self.getSubtitleStream = function (player, index) {
|
||||||
return self.subtitleTracks(player).filter(function (s) {
|
return self.subtitleTracks(player).filter(function (s) {
|
||||||
return s.Type === 'Subtitle' && s.Index === index;
|
return s.Type === 'Subtitle' && s.Index === index;
|
||||||
})[0];
|
})[0];
|
||||||
}
|
};
|
||||||
|
|
||||||
self.getPlaylist = function (player) {
|
self.getPlaylist = function (player) {
|
||||||
player = player || self._currentPlayer;
|
player = player || self._currentPlayer;
|
||||||
|
@ -1463,6 +1491,24 @@ class PlaybackManager {
|
||||||
return getPlayerData(player).subtitleStreamIndex;
|
return getPlayerData(player).subtitleStreamIndex;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.getSecondarySubtitleStreamIndex = function (player) {
|
||||||
|
player = player || self._currentPlayer;
|
||||||
|
|
||||||
|
if (!player) {
|
||||||
|
throw new Error('player cannot be null');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!enableLocalPlaylistManagement(player)) {
|
||||||
|
return player.getSecondarySubtitleStreamIndex();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[playbackmanager] Failed to get secondary stream index:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getPlayerData(player).secondarySubtitleStreamIndex;
|
||||||
|
};
|
||||||
|
|
||||||
function getDeliveryMethod(subtitleStream) {
|
function getDeliveryMethod(subtitleStream) {
|
||||||
// This will be null for internal subs for local items
|
// This will be null for internal subs for local items
|
||||||
if (subtitleStream.DeliveryMethod) {
|
if (subtitleStream.DeliveryMethod) {
|
||||||
|
@ -1480,7 +1526,7 @@ class PlaybackManager {
|
||||||
|
|
||||||
const currentStream = getCurrentSubtitleStream(player);
|
const currentStream = getCurrentSubtitleStream(player);
|
||||||
|
|
||||||
const newStream = getSubtitleStream(player, index);
|
const newStream = self.getSubtitleStream(player, index);
|
||||||
|
|
||||||
if (!currentStream && !newStream) {
|
if (!currentStream && !newStream) {
|
||||||
return;
|
return;
|
||||||
|
@ -1522,9 +1568,48 @@ class PlaybackManager {
|
||||||
|
|
||||||
player.setSubtitleStreamIndex(selectedTrackElementIndex);
|
player.setSubtitleStreamIndex(selectedTrackElementIndex);
|
||||||
|
|
||||||
|
// Also disable secondary subtitles when disabling the primary
|
||||||
|
// subtitles, or if it doesn't support a secondary pair
|
||||||
|
if (selectedTrackElementIndex === -1 || !self.trackHasSecondarySubtitleSupport(newStream)) {
|
||||||
|
self.setSecondarySubtitleStreamIndex(-1);
|
||||||
|
}
|
||||||
|
|
||||||
getPlayerData(player).subtitleStreamIndex = index;
|
getPlayerData(player).subtitleStreamIndex = index;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.setSecondarySubtitleStreamIndex = function (index, player) {
|
||||||
|
player = player || self._currentPlayer;
|
||||||
|
if (!self.playerHasSecondarySubtitleSupport(player)) return;
|
||||||
|
if (player && !enableLocalPlaylistManagement(player)) {
|
||||||
|
try {
|
||||||
|
return player.setSecondarySubtitleStreamIndex(index);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[playbackmanager] AutoSet - Failed to set secondary track:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentStream = getCurrentSubtitleStream(player, true);
|
||||||
|
|
||||||
|
const newStream = self.getSubtitleStream(player, index);
|
||||||
|
|
||||||
|
if (!currentStream && !newStream) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secondary subtitles are currently only handled client side
|
||||||
|
// Changes to the server code are required before we can handle other delivery methods
|
||||||
|
if (newStream && !self.trackHasSecondarySubtitleSupport(newStream, player)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
player.setSecondarySubtitleStreamIndex(index);
|
||||||
|
getPlayerData(player).secondarySubtitleStreamIndex = index;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[playbackmanager] AutoSet - Failed to set secondary track:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
self.supportSubtitleOffset = function (player) {
|
self.supportSubtitleOffset = function (player) {
|
||||||
player = player || self._currentPlayer;
|
player = player || self._currentPlayer;
|
||||||
return player && 'setSubtitleOffset' in player;
|
return player && 'setSubtitleOffset' in player;
|
||||||
|
@ -1548,7 +1633,7 @@ class PlaybackManager {
|
||||||
};
|
};
|
||||||
|
|
||||||
self.isSubtitleStreamExternal = function (index, player) {
|
self.isSubtitleStreamExternal = function (index, player) {
|
||||||
const stream = getSubtitleStream(player, index);
|
const stream = self.getSubtitleStream(player, index);
|
||||||
return stream ? getDeliveryMethod(stream) === 'External' : false;
|
return stream ? getDeliveryMethod(stream) === 'External' : false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1639,6 +1724,7 @@ class PlaybackManager {
|
||||||
}).then(function (deviceProfile) {
|
}).then(function (deviceProfile) {
|
||||||
const audioStreamIndex = params.AudioStreamIndex == null ? getPlayerData(player).audioStreamIndex : params.AudioStreamIndex;
|
const audioStreamIndex = params.AudioStreamIndex == null ? getPlayerData(player).audioStreamIndex : params.AudioStreamIndex;
|
||||||
const subtitleStreamIndex = params.SubtitleStreamIndex == null ? getPlayerData(player).subtitleStreamIndex : params.SubtitleStreamIndex;
|
const subtitleStreamIndex = params.SubtitleStreamIndex == null ? getPlayerData(player).subtitleStreamIndex : params.SubtitleStreamIndex;
|
||||||
|
const secondarySubtitleStreamIndex = params.SecondarySubtitleStreamIndex == null ? getPlayerData(player).secondarySubtitleStreamIndex : params.SecondarySubtitleStreamIndex;
|
||||||
|
|
||||||
let currentMediaSource = self.currentMediaSource(player);
|
let currentMediaSource = self.currentMediaSource(player);
|
||||||
const apiClient = ServerConnections.getApiClient(currentItem.ServerId);
|
const apiClient = ServerConnections.getApiClient(currentItem.ServerId);
|
||||||
|
@ -1665,6 +1751,7 @@ class PlaybackManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlayerData(player).subtitleStreamIndex = subtitleStreamIndex;
|
getPlayerData(player).subtitleStreamIndex = subtitleStreamIndex;
|
||||||
|
getPlayerData(player).secondarySubtitleStreamIndex = secondarySubtitleStreamIndex;
|
||||||
getPlayerData(player).audioStreamIndex = audioStreamIndex;
|
getPlayerData(player).audioStreamIndex = audioStreamIndex;
|
||||||
getPlayerData(player).maxStreamingBitrate = maxBitrate;
|
getPlayerData(player).maxStreamingBitrate = maxBitrate;
|
||||||
|
|
||||||
|
@ -1950,6 +2037,7 @@ class PlaybackManager {
|
||||||
state.PlayState.PlaybackRate = self.getPlaybackRate(player);
|
state.PlayState.PlaybackRate = self.getPlaybackRate(player);
|
||||||
|
|
||||||
state.PlayState.SubtitleStreamIndex = self.getSubtitleStreamIndex(player);
|
state.PlayState.SubtitleStreamIndex = self.getSubtitleStreamIndex(player);
|
||||||
|
state.PlayState.SecondarySubtitleStreamIndex = self.getSecondarySubtitleStreamIndex(player);
|
||||||
state.PlayState.AudioStreamIndex = self.getAudioStreamIndex(player);
|
state.PlayState.AudioStreamIndex = self.getAudioStreamIndex(player);
|
||||||
state.PlayState.BufferedRanges = self.getBufferedRanges(player);
|
state.PlayState.BufferedRanges = self.getBufferedRanges(player);
|
||||||
|
|
||||||
|
@ -2230,11 +2318,16 @@ class PlaybackManager {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function rankStreamType(prevIndex, prevSource, mediaSource, streamType) {
|
function rankStreamType(prevIndex, prevSource, mediaSource, streamType, isSecondarySubtitle) {
|
||||||
if (prevIndex == -1) {
|
if (prevIndex == -1) {
|
||||||
console.debug(`AutoSet ${streamType} - No Stream Set`);
|
console.debug(`AutoSet ${streamType} - No Stream Set`);
|
||||||
if (streamType == 'Subtitle')
|
if (streamType == 'Subtitle') {
|
||||||
mediaSource.DefaultSubtitleStreamIndex = -1;
|
if (isSecondarySubtitle) {
|
||||||
|
mediaSource.DefaultSecondarySubtitleStreamIndex = -1;
|
||||||
|
} else {
|
||||||
|
mediaSource.DefaultSubtitleStreamIndex = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2292,8 +2385,13 @@ class PlaybackManager {
|
||||||
|
|
||||||
if (bestStreamIndex != null) {
|
if (bestStreamIndex != null) {
|
||||||
console.debug(`AutoSet ${streamType} - Using ${bestStreamIndex} score ${bestStreamScore}.`);
|
console.debug(`AutoSet ${streamType} - Using ${bestStreamIndex} score ${bestStreamScore}.`);
|
||||||
if (streamType == 'Subtitle')
|
if (streamType == 'Subtitle') {
|
||||||
mediaSource.DefaultSubtitleStreamIndex = bestStreamIndex;
|
if (isSecondarySubtitle) {
|
||||||
|
mediaSource.DefaultSecondarySubtitleStreamIndex = bestStreamIndex;
|
||||||
|
} else {
|
||||||
|
mediaSource.DefaultSubtitleStreamIndex = bestStreamIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (streamType == 'Audio')
|
if (streamType == 'Audio')
|
||||||
mediaSource.DefaultAudioStreamIndex = bestStreamIndex;
|
mediaSource.DefaultAudioStreamIndex = bestStreamIndex;
|
||||||
} else {
|
} else {
|
||||||
|
@ -2317,6 +2415,10 @@ class PlaybackManager {
|
||||||
if (subtitle && typeof prevSource.DefaultSubtitleStreamIndex == 'number') {
|
if (subtitle && typeof prevSource.DefaultSubtitleStreamIndex == 'number') {
|
||||||
rankStreamType(prevSource.DefaultSubtitleStreamIndex, prevSource, mediaSource, 'Subtitle');
|
rankStreamType(prevSource.DefaultSubtitleStreamIndex, prevSource, mediaSource, 'Subtitle');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (subtitle && typeof prevSource.DefaultSecondarySubtitleStreamIndex == 'number') {
|
||||||
|
rankStreamType(prevSource.DefaultSecondarySubtitleStreamIndex, prevSource, mediaSource, 'Subtitle', true);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`AutoSet - Caught unexpected error: ${e}`);
|
console.error(`AutoSet - Caught unexpected error: ${e}`);
|
||||||
}
|
}
|
||||||
|
@ -2384,6 +2486,19 @@ class PlaybackManager {
|
||||||
const user = await apiClient.getCurrentUser();
|
const user = await apiClient.getCurrentUser();
|
||||||
autoSetNextTracks(prevSource, mediaSource, user.Configuration.RememberAudioSelections, user.Configuration.RememberSubtitleSelections);
|
autoSetNextTracks(prevSource, mediaSource, user.Configuration.RememberAudioSelections, user.Configuration.RememberSubtitleSelections);
|
||||||
|
|
||||||
|
if (mediaSource.DefaultSubtitleStreamIndex == null || mediaSource.DefaultSubtitleStreamIndex < 0) {
|
||||||
|
mediaSource.DefaultSubtitleStreamIndex = mediaSource.DefaultSecondarySubtitleStreamIndex;
|
||||||
|
mediaSource.DefaultSecondarySubtitleStreamIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subtitleTrack1 = mediaSource.MediaStreams[mediaSource.DefaultSubtitleStreamIndex];
|
||||||
|
const subtitleTrack2 = mediaSource.MediaStreams[mediaSource.DefaultSecondarySubtitleStreamIndex];
|
||||||
|
|
||||||
|
if (!self.trackHasSecondarySubtitleSupport(subtitleTrack1, player)
|
||||||
|
|| !self.trackHasSecondarySubtitleSupport(subtitleTrack2, player)) {
|
||||||
|
mediaSource.DefaultSecondarySubtitleStreamIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
const streamInfo = createStreamInfo(apiClient, item.MediaType, item, mediaSource, startPosition, player);
|
const streamInfo = createStreamInfo(apiClient, item.MediaType, item, mediaSource, startPosition, player);
|
||||||
|
|
||||||
streamInfo.fullscreen = playOptions.fullscreen;
|
streamInfo.fullscreen = playOptions.fullscreen;
|
||||||
|
@ -2751,7 +2866,8 @@ class PlaybackManager {
|
||||||
return {
|
return {
|
||||||
...prevSource,
|
...prevSource,
|
||||||
DefaultAudioStreamIndex: prevPlayerData.audioStreamIndex,
|
DefaultAudioStreamIndex: prevPlayerData.audioStreamIndex,
|
||||||
DefaultSubtitleStreamIndex: prevPlayerData.subtitleStreamIndex
|
DefaultSubtitleStreamIndex: prevPlayerData.subtitleStreamIndex,
|
||||||
|
DefaultSecondarySubtitleStreamIndex: prevPlayerData.secondarySubtitleStreamIndex
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2910,9 +3026,11 @@ class PlaybackManager {
|
||||||
if (mediaSource) {
|
if (mediaSource) {
|
||||||
playerData.audioStreamIndex = mediaSource.DefaultAudioStreamIndex;
|
playerData.audioStreamIndex = mediaSource.DefaultAudioStreamIndex;
|
||||||
playerData.subtitleStreamIndex = mediaSource.DefaultSubtitleStreamIndex;
|
playerData.subtitleStreamIndex = mediaSource.DefaultSubtitleStreamIndex;
|
||||||
|
playerData.secondarySubtitleStreamIndex = mediaSource.DefaultSecondarySubtitleStreamIndex;
|
||||||
} else {
|
} else {
|
||||||
playerData.audioStreamIndex = null;
|
playerData.audioStreamIndex = null;
|
||||||
playerData.subtitleStreamIndex = null;
|
playerData.subtitleStreamIndex = null;
|
||||||
|
playerData.secondarySubtitleStreamIndex = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
self._playNextAfterEnded = true;
|
self._playNextAfterEnded = true;
|
||||||
|
|
|
@ -988,9 +988,57 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showSecondarySubtitlesMenu(actionsheet, positionTo) {
|
||||||
|
const player = currentPlayer;
|
||||||
|
if (!playbackManager.playerHasSecondarySubtitleSupport(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() {
|
function showSubtitleTrackSelection() {
|
||||||
const player = currentPlayer;
|
const player = currentPlayer;
|
||||||
const streams = playbackManager.subtitleTracks(player);
|
const streams = playbackManager.subtitleTracks(player);
|
||||||
|
const secondaryStreams = playbackManager.secondarySubtitleTracks(player);
|
||||||
let currentIndex = playbackManager.getSubtitleStreamIndex(player);
|
let currentIndex = playbackManager.getSubtitleStreamIndex(player);
|
||||||
|
|
||||||
if (currentIndex == null) {
|
if (currentIndex == null) {
|
||||||
|
@ -1013,6 +1061,29 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
|
||||||
|
|
||||||
return opt;
|
return opt;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only show option if:
|
||||||
|
* - player has support
|
||||||
|
* - has more than 1 subtitle track
|
||||||
|
* - has valid secondary tracks
|
||||||
|
* - primary subtitle is not off
|
||||||
|
* - primary subtitle has support
|
||||||
|
*/
|
||||||
|
const currentTrackCanAddSecondarySubtitle = playbackManager.playerHasSecondarySubtitleSupport(player)
|
||||||
|
&& streams.length > 1
|
||||||
|
&& secondaryStreams.length > 0
|
||||||
|
&& currentIndex !== -1
|
||||||
|
&& playbackManager.trackHasSecondarySubtitleSupport(playbackManager.getSubtitleStream(player, currentIndex), player);
|
||||||
|
|
||||||
|
if (currentTrackCanAddSecondarySubtitle) {
|
||||||
|
const secondarySubtitleMenuItem = {
|
||||||
|
name: globalize.translate('SecondarySubtitles'),
|
||||||
|
id: 'secondarysubtitle'
|
||||||
|
};
|
||||||
|
menuItems.unshift(secondarySubtitleMenuItem);
|
||||||
|
}
|
||||||
|
|
||||||
const positionTo = this;
|
const positionTo = this;
|
||||||
|
|
||||||
import('../../../components/actionSheet/actionSheet').then(({default: actionsheet}) => {
|
import('../../../components/actionSheet/actionSheet').then(({default: actionsheet}) => {
|
||||||
|
@ -1021,10 +1092,18 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
|
||||||
items: menuItems,
|
items: menuItems,
|
||||||
positionTo: positionTo
|
positionTo: positionTo
|
||||||
}).then(function (id) {
|
}).then(function (id) {
|
||||||
const index = parseInt(id);
|
if (id === 'secondarysubtitle') {
|
||||||
|
try {
|
||||||
|
showSecondarySubtitlesMenu(actionsheet, positionTo);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const index = parseInt(id);
|
||||||
|
|
||||||
if (index !== currentIndex) {
|
if (index !== currentIndex) {
|
||||||
playbackManager.setSubtitleStreamIndex(index, player);
|
playbackManager.setSubtitleStreamIndex(index, player);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSubtitleSync();
|
toggleSubtitleSync();
|
||||||
|
|
|
@ -155,6 +155,9 @@ function tryRemoveElement(elem) {
|
||||||
return profileBuilder({});
|
return profileBuilder({});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PRIMARY_TEXT_TRACK_INDEX = 0;
|
||||||
|
const SECONDARY_TEXT_TRACK_INDEX = 1;
|
||||||
|
|
||||||
export class HtmlVideoPlayer {
|
export class HtmlVideoPlayer {
|
||||||
/**
|
/**
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
@ -178,7 +181,6 @@ function tryRemoveElement(elem) {
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
isFetching = false;
|
isFetching = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {HTMLDivElement | null | undefined}
|
* @type {HTMLDivElement | null | undefined}
|
||||||
*/
|
*/
|
||||||
|
@ -187,6 +189,10 @@ function tryRemoveElement(elem) {
|
||||||
* @type {number | undefined}
|
* @type {number | undefined}
|
||||||
*/
|
*/
|
||||||
#subtitleTrackIndexToSetOnPlaying;
|
#subtitleTrackIndexToSetOnPlaying;
|
||||||
|
/**
|
||||||
|
* @type {number | undefined}
|
||||||
|
*/
|
||||||
|
#secondarySubtitleTrackIndexToSetOnPlaying;
|
||||||
/**
|
/**
|
||||||
* @type {number | null}
|
* @type {number | null}
|
||||||
*/
|
*/
|
||||||
|
@ -207,6 +213,10 @@ function tryRemoveElement(elem) {
|
||||||
* @type {number | undefined}
|
* @type {number | undefined}
|
||||||
*/
|
*/
|
||||||
#customTrackIndex;
|
#customTrackIndex;
|
||||||
|
/**
|
||||||
|
* @type {number | undefined}
|
||||||
|
*/
|
||||||
|
#customSecondaryTrackIndex;
|
||||||
/**
|
/**
|
||||||
* @type {boolean | undefined}
|
* @type {boolean | undefined}
|
||||||
*/
|
*/
|
||||||
|
@ -215,14 +225,26 @@ function tryRemoveElement(elem) {
|
||||||
* @type {number | undefined}
|
* @type {number | undefined}
|
||||||
*/
|
*/
|
||||||
#currentTrackOffset;
|
#currentTrackOffset;
|
||||||
|
/**
|
||||||
|
* @type {HTMLElement | null | undefined}
|
||||||
|
*/
|
||||||
|
#secondaryTrackOffset;
|
||||||
/**
|
/**
|
||||||
* @type {HTMLElement | null | undefined}
|
* @type {HTMLElement | null | undefined}
|
||||||
*/
|
*/
|
||||||
#videoSubtitlesElem;
|
#videoSubtitlesElem;
|
||||||
|
/**
|
||||||
|
* @type {HTMLElement | null | undefined}
|
||||||
|
*/
|
||||||
|
#videoSecondarySubtitlesElem;
|
||||||
/**
|
/**
|
||||||
* @type {any | null | undefined}
|
* @type {any | null | undefined}
|
||||||
*/
|
*/
|
||||||
#currentTrackEvents;
|
#currentTrackEvents;
|
||||||
|
/**
|
||||||
|
* @type {any | null | undefined}
|
||||||
|
*/
|
||||||
|
#currentSecondaryTrackEvents;
|
||||||
/**
|
/**
|
||||||
* @type {string[] | undefined}
|
* @type {string[] | undefined}
|
||||||
*/
|
*/
|
||||||
|
@ -448,18 +470,39 @@ function tryRemoveElement(elem) {
|
||||||
destroyFlvPlayer(this);
|
destroyFlvPlayer(this);
|
||||||
destroyCastPlayer(this);
|
destroyCastPlayer(this);
|
||||||
|
|
||||||
|
let secondaryTrackValid = true;
|
||||||
|
|
||||||
this.#subtitleTrackIndexToSetOnPlaying = options.mediaSource.DefaultSubtitleStreamIndex == null ? -1 : options.mediaSource.DefaultSubtitleStreamIndex;
|
this.#subtitleTrackIndexToSetOnPlaying = options.mediaSource.DefaultSubtitleStreamIndex == null ? -1 : options.mediaSource.DefaultSubtitleStreamIndex;
|
||||||
if (this.#subtitleTrackIndexToSetOnPlaying != null && this.#subtitleTrackIndexToSetOnPlaying >= 0) {
|
if (this.#subtitleTrackIndexToSetOnPlaying != null && this.#subtitleTrackIndexToSetOnPlaying >= 0) {
|
||||||
const initialSubtitleStream = options.mediaSource.MediaStreams[this.#subtitleTrackIndexToSetOnPlaying];
|
const initialSubtitleStream = options.mediaSource.MediaStreams[this.#subtitleTrackIndexToSetOnPlaying];
|
||||||
if (!initialSubtitleStream || initialSubtitleStream.DeliveryMethod === 'Encode') {
|
if (!initialSubtitleStream || initialSubtitleStream.DeliveryMethod === 'Encode') {
|
||||||
this.#subtitleTrackIndexToSetOnPlaying = -1;
|
this.#subtitleTrackIndexToSetOnPlaying = -1;
|
||||||
|
secondaryTrackValid = false;
|
||||||
}
|
}
|
||||||
|
// secondary track should not be shown if primary track is no longer a valid pair
|
||||||
|
if (initialSubtitleStream && !playbackManager.trackHasSecondarySubtitleSupport(initialSubtitleStream, this)) {
|
||||||
|
secondaryTrackValid = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
secondaryTrackValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#audioTrackIndexToSetOnPlaying = options.playMethod === 'Transcode' ? null : options.mediaSource.DefaultAudioStreamIndex;
|
this.#audioTrackIndexToSetOnPlaying = options.playMethod === 'Transcode' ? null : options.mediaSource.DefaultAudioStreamIndex;
|
||||||
|
|
||||||
this._currentPlayOptions = options;
|
this._currentPlayOptions = options;
|
||||||
|
|
||||||
|
if (secondaryTrackValid) {
|
||||||
|
this.#secondarySubtitleTrackIndexToSetOnPlaying = options.mediaSource.DefaultSecondarySubtitleStreamIndex == null ? -1 : options.mediaSource.DefaultSecondarySubtitleStreamIndex;
|
||||||
|
if (this.#secondarySubtitleTrackIndexToSetOnPlaying != null && this.#secondarySubtitleTrackIndexToSetOnPlaying >= 0) {
|
||||||
|
const initialSecondarySubtitleStream = options.mediaSource.MediaStreams[this.#secondarySubtitleTrackIndexToSetOnPlaying];
|
||||||
|
if (!initialSecondarySubtitleStream || !playbackManager.trackHasSecondarySubtitleSupport(initialSecondarySubtitleStream, this)) {
|
||||||
|
this.#secondarySubtitleTrackIndexToSetOnPlaying = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.#secondarySubtitleTrackIndexToSetOnPlaying = -1;
|
||||||
|
}
|
||||||
|
|
||||||
const crossOrigin = getCrossOriginValue(options.mediaSource);
|
const crossOrigin = getCrossOriginValue(options.mediaSource);
|
||||||
if (crossOrigin) {
|
if (crossOrigin) {
|
||||||
elem.crossOrigin = crossOrigin;
|
elem.crossOrigin = crossOrigin;
|
||||||
|
@ -490,8 +533,13 @@ function tryRemoveElement(elem) {
|
||||||
this.setCurrentTrackElement(index);
|
this.setCurrentTrackElement(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSecondarySubtitleStreamIndex(index) {
|
||||||
|
this.setCurrentTrackElement(index, SECONDARY_TEXT_TRACK_INDEX);
|
||||||
|
}
|
||||||
|
|
||||||
resetSubtitleOffset() {
|
resetSubtitleOffset() {
|
||||||
this.#currentTrackOffset = 0;
|
this.#currentTrackOffset = 0;
|
||||||
|
this.#secondaryTrackOffset = 0;
|
||||||
this.#showTrackOffset = false;
|
this.#showTrackOffset = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -510,11 +558,11 @@ function tryRemoveElement(elem) {
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
getTextTrack() {
|
getTextTracks() {
|
||||||
const videoElement = this.#mediaElement;
|
const videoElement = this.#mediaElement;
|
||||||
if (videoElement) {
|
if (videoElement) {
|
||||||
return Array.from(videoElement.textTracks)
|
return Array.from(videoElement.textTracks)
|
||||||
.find(function (trackElement) {
|
.filter(function (trackElement) {
|
||||||
// get showing .vtt textTack
|
// get showing .vtt textTack
|
||||||
return trackElement.mode === 'showing';
|
return trackElement.mode === 'showing';
|
||||||
});
|
});
|
||||||
|
@ -523,9 +571,6 @@ function tryRemoveElement(elem) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
setSubtitleOffset(offset) {
|
setSubtitleOffset(offset) {
|
||||||
const offsetValue = parseFloat(offset);
|
const offsetValue = parseFloat(offset);
|
||||||
|
|
||||||
|
@ -534,12 +579,15 @@ function tryRemoveElement(elem) {
|
||||||
this.updateCurrentTrackOffset(offsetValue);
|
this.updateCurrentTrackOffset(offsetValue);
|
||||||
this.#currentSubtitlesOctopus.timeOffset = (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000 + offsetValue;
|
this.#currentSubtitlesOctopus.timeOffset = (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000 + offsetValue;
|
||||||
} else {
|
} else {
|
||||||
const trackElement = this.getTextTrack();
|
const trackElements = this.getTextTracks();
|
||||||
// if .vtt currently rendering
|
// if .vtt currently rendering
|
||||||
if (trackElement) {
|
if (trackElements?.length > 0) {
|
||||||
this.setTextTrackSubtitleOffset(trackElement, offsetValue);
|
trackElements.forEach((trackElement, index) => {
|
||||||
} else if (this.#currentTrackEvents) {
|
this.setTextTrackSubtitleOffset(trackElement, offsetValue, index);
|
||||||
this.setTrackEventsSubtitleOffset(this.#currentTrackEvents, offsetValue);
|
});
|
||||||
|
} else if (this.#currentTrackEvents || this.#currentSecondaryTrackEvents) {
|
||||||
|
this.#currentTrackEvents && this.setTrackEventsSubtitleOffset(this.#currentTrackEvents, offsetValue, PRIMARY_TEXT_TRACK_INDEX);
|
||||||
|
this.#currentSecondaryTrackEvents && this.setTrackEventsSubtitleOffset(this.#currentSecondaryTrackEvents, offsetValue, SECONDARY_TEXT_TRACK_INDEX);
|
||||||
} else {
|
} else {
|
||||||
console.debug('No available track, cannot apply offset: ', offsetValue);
|
console.debug('No available track, cannot apply offset: ', offsetValue);
|
||||||
}
|
}
|
||||||
|
@ -549,13 +597,25 @@ function tryRemoveElement(elem) {
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
updateCurrentTrackOffset(offsetValue) {
|
updateCurrentTrackOffset(offsetValue, currentTrackIndex = PRIMARY_TEXT_TRACK_INDEX) {
|
||||||
|
let offsetToCompare = this.#currentTrackOffset;
|
||||||
|
if (this.isSecondaryTrack(currentTrackIndex)) {
|
||||||
|
offsetToCompare = this.#secondaryTrackOffset;
|
||||||
|
}
|
||||||
|
|
||||||
let relativeOffset = offsetValue;
|
let relativeOffset = offsetValue;
|
||||||
const newTrackOffset = offsetValue;
|
const newTrackOffset = offsetValue;
|
||||||
if (this.#currentTrackOffset) {
|
|
||||||
relativeOffset -= this.#currentTrackOffset;
|
if (offsetToCompare) {
|
||||||
|
relativeOffset -= offsetToCompare;
|
||||||
}
|
}
|
||||||
this.#currentTrackOffset = newTrackOffset;
|
|
||||||
|
if (this.isSecondaryTrack(currentTrackIndex)) {
|
||||||
|
this.#secondaryTrackOffset = newTrackOffset;
|
||||||
|
} else {
|
||||||
|
this.#currentTrackOffset = newTrackOffset;
|
||||||
|
}
|
||||||
|
|
||||||
// relative to currentTrackOffset
|
// relative to currentTrackOffset
|
||||||
return relativeOffset;
|
return relativeOffset;
|
||||||
}
|
}
|
||||||
|
@ -563,9 +623,12 @@ function tryRemoveElement(elem) {
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
setTextTrackSubtitleOffset(currentTrack, offsetValue) {
|
setTextTrackSubtitleOffset(currentTrack, offsetValue, currentTrackIndex) {
|
||||||
if (currentTrack.cues) {
|
if (currentTrack.cues) {
|
||||||
offsetValue = this.updateCurrentTrackOffset(offsetValue);
|
offsetValue = this.updateCurrentTrackOffset(offsetValue, currentTrackIndex);
|
||||||
|
if (offsetValue === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Array.from(currentTrack.cues)
|
Array.from(currentTrack.cues)
|
||||||
.forEach(function (cue) {
|
.forEach(function (cue) {
|
||||||
cue.startTime -= offsetValue;
|
cue.startTime -= offsetValue;
|
||||||
|
@ -577,9 +640,12 @@ function tryRemoveElement(elem) {
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
setTrackEventsSubtitleOffset(trackEvents, offsetValue) {
|
setTrackEventsSubtitleOffset(trackEvents, offsetValue, currentTrackIndex) {
|
||||||
if (Array.isArray(trackEvents)) {
|
if (Array.isArray(trackEvents)) {
|
||||||
offsetValue = this.updateCurrentTrackOffset(offsetValue) * 1e7; // ticks
|
offsetValue = this.updateCurrentTrackOffset(offsetValue, currentTrackIndex) * 1e7; // ticks
|
||||||
|
if (offsetValue === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
trackEvents.forEach(function (trackEvent) {
|
trackEvents.forEach(function (trackEvent) {
|
||||||
trackEvent.StartPositionTicks -= offsetValue;
|
trackEvent.StartPositionTicks -= offsetValue;
|
||||||
trackEvent.EndPositionTicks -= offsetValue;
|
trackEvent.EndPositionTicks -= offsetValue;
|
||||||
|
@ -591,6 +657,14 @@ function tryRemoveElement(elem) {
|
||||||
return this.#currentTrackOffset;
|
return this.#currentTrackOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isPrimaryTrack(textTrackIndex) {
|
||||||
|
return textTrackIndex === PRIMARY_TEXT_TRACK_INDEX;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSecondaryTrack(textTrackIndex) {
|
||||||
|
return textTrackIndex === SECONDARY_TEXT_TRACK_INDEX;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
|
@ -819,6 +893,16 @@ function tryRemoveElement(elem) {
|
||||||
if (this.#audioTrackIndexToSetOnPlaying != null && this.canSetAudioStreamIndex()) {
|
if (this.#audioTrackIndexToSetOnPlaying != null && this.canSetAudioStreamIndex()) {
|
||||||
this.setAudioStreamIndex(this.#audioTrackIndexToSetOnPlaying);
|
this.setAudioStreamIndex(this.#audioTrackIndexToSetOnPlaying);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.#secondarySubtitleTrackIndexToSetOnPlaying != null && this.#secondarySubtitleTrackIndexToSetOnPlaying >= 0) {
|
||||||
|
/**
|
||||||
|
* Using a 0ms timeout to set the secondary subtitles because of some weird race condition when
|
||||||
|
* setting both primary and secondary tracks at the same time.
|
||||||
|
* The `TextTrack` content and cues will somehow get mixed up and each track will play a mix of both languages.
|
||||||
|
* Putting this in a timeout fixes it completely.
|
||||||
|
*/
|
||||||
|
setTimeout(() => this.setSecondarySubtitleStreamIndex(this.#secondarySubtitleTrackIndexToSetOnPlaying), 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -956,27 +1040,75 @@ function tryRemoveElement(elem) {
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
destroyCustomTrack(videoElement) {
|
destroyCustomRenderedTrackElements(targetTrackIndex) {
|
||||||
if (this.#videoSubtitlesElem) {
|
if (this.isPrimaryTrack(targetTrackIndex)) {
|
||||||
const subtitlesContainer = this.#videoSubtitlesElem.parentNode;
|
if (this.#videoSubtitlesElem) {
|
||||||
if (subtitlesContainer) {
|
tryRemoveElement(this.#videoSubtitlesElem);
|
||||||
tryRemoveElement(subtitlesContainer);
|
this.#videoSubtitlesElem = null;
|
||||||
|
}
|
||||||
|
} else if (this.isSecondaryTrack(targetTrackIndex)) {
|
||||||
|
if (this.#videoSecondarySubtitlesElem) {
|
||||||
|
tryRemoveElement(this.#videoSecondarySubtitlesElem);
|
||||||
|
this.#videoSecondarySubtitlesElem = null;
|
||||||
|
}
|
||||||
|
} else { // destroy all
|
||||||
|
if (this.#videoSubtitlesElem) {
|
||||||
|
const subtitlesContainer = this.#videoSubtitlesElem.parentNode;
|
||||||
|
if (subtitlesContainer) {
|
||||||
|
tryRemoveElement(subtitlesContainer);
|
||||||
|
}
|
||||||
|
this.#videoSubtitlesElem = null;
|
||||||
|
this.#videoSecondarySubtitlesElem = null;
|
||||||
}
|
}
|
||||||
this.#videoSubtitlesElem = null;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.#currentTrackEvents = null;
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
destroyNativeTracks(videoElement, targetTrackIndex) {
|
||||||
if (videoElement) {
|
if (videoElement) {
|
||||||
|
const destroySingleTrack = typeof targetTrackIndex === 'number';
|
||||||
const allTracks = videoElement.textTracks || []; // get list of tracks
|
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];
|
||||||
|
// Skip all other tracks if we are targeting just one
|
||||||
|
if (destroySingleTrack && targetTrackIndex !== index) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (track.label.includes('manualTrack')) {
|
if (track.label.includes('manualTrack')) {
|
||||||
track.mode = 'disabled';
|
track.mode = 'disabled';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
destroyStoredTrackInfo(targetTrackIndex) {
|
||||||
|
if (this.isPrimaryTrack(targetTrackIndex)) {
|
||||||
|
this.#customTrackIndex = -1;
|
||||||
|
this.#currentTrackEvents = null;
|
||||||
|
} else if (this.isSecondaryTrack(targetTrackIndex)) {
|
||||||
|
this.#customSecondaryTrackIndex = -1;
|
||||||
|
this.#currentSecondaryTrackEvents = null;
|
||||||
|
} else { // destroy all
|
||||||
|
this.#customTrackIndex = -1;
|
||||||
|
this.#customSecondaryTrackIndex = -1;
|
||||||
|
this.#currentTrackEvents = null;
|
||||||
|
this.#currentSecondaryTrackEvents = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
destroyCustomTrack(videoElement, targetTrackIndex) {
|
||||||
|
this.destroyCustomRenderedTrackElements(targetTrackIndex);
|
||||||
|
this.destroyNativeTracks(videoElement, targetTrackIndex);
|
||||||
|
this.destroyStoredTrackInfo(targetTrackIndex);
|
||||||
|
|
||||||
this.#customTrackIndex = -1;
|
|
||||||
this.#currentClock = null;
|
this.#currentClock = null;
|
||||||
this._currentAspectRatio = null;
|
this._currentAspectRatio = null;
|
||||||
|
|
||||||
|
@ -1029,23 +1161,34 @@ function tryRemoveElement(elem) {
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
setTrackForDisplay(videoElement, track) {
|
setTrackForDisplay(videoElement, track, targetTextTrackIndex = PRIMARY_TEXT_TRACK_INDEX) {
|
||||||
if (!track) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let targetTrackIndex = this.#customTrackIndex;
|
||||||
|
if (this.isSecondaryTrack(targetTextTrackIndex)) {
|
||||||
|
targetTrackIndex = this.#customSecondaryTrackIndex;
|
||||||
|
}
|
||||||
|
|
||||||
// skip if already playing this track
|
// skip if already playing this track
|
||||||
if (this.#customTrackIndex === track.Index) {
|
if (targetTrackIndex === track.Index) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.resetSubtitleOffset();
|
this.resetSubtitleOffset();
|
||||||
const item = this._currentPlayOptions.item;
|
const item = this._currentPlayOptions.item;
|
||||||
|
|
||||||
this.destroyCustomTrack(videoElement);
|
this.destroyCustomTrack(videoElement, targetTextTrackIndex);
|
||||||
this.#customTrackIndex = track.Index;
|
|
||||||
this.renderTracksEvents(videoElement, track, item);
|
if (this.isSecondaryTrack(targetTextTrackIndex)) {
|
||||||
|
this.#customSecondaryTrackIndex = track.Index;
|
||||||
|
} else {
|
||||||
|
this.#customTrackIndex = track.Index;
|
||||||
|
}
|
||||||
|
this.renderTracksEvents(videoElement, track, item, targetTextTrackIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1155,16 +1298,39 @@ function tryRemoveElement(elem) {
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
renderSubtitlesWithCustomElement(videoElement, track, item) {
|
renderSubtitlesWithCustomElement(videoElement, track, item, targetTextTrackIndex) {
|
||||||
this.fetchSubtitles(track, item).then((data) => {
|
Promise.all([import('../../scripts/settings/userSettings'), this.fetchSubtitles(track, item)]).then((results) => {
|
||||||
if (!this.#videoSubtitlesElem) {
|
const [userSettings, subtitleData] = results;
|
||||||
const subtitlesContainer = document.createElement('div');
|
const subtitleAppearance = userSettings.getSubtitleAppearanceSettings();
|
||||||
subtitlesContainer.classList.add('videoSubtitles');
|
const subtitleVerticalPosition = parseInt(subtitleAppearance.verticalPosition, 10);
|
||||||
subtitlesContainer.innerHTML = '<div class="videoSubtitlesInner"></div>';
|
|
||||||
this.#videoSubtitlesElem = subtitlesContainer.querySelector('.videoSubtitlesInner');
|
if (!this.#videoSubtitlesElem && !this.isSecondaryTrack(targetTextTrackIndex)) {
|
||||||
|
let subtitlesContainer = document.querySelector('.videoSubtitles');
|
||||||
|
if (!subtitlesContainer) {
|
||||||
|
subtitlesContainer = document.createElement('div');
|
||||||
|
subtitlesContainer.classList.add('videoSubtitles');
|
||||||
|
}
|
||||||
|
const subtitlesElement = document.createElement('div');
|
||||||
|
subtitlesElement.classList.add('videoSubtitlesInner');
|
||||||
|
subtitlesContainer.appendChild(subtitlesElement);
|
||||||
|
this.#videoSubtitlesElem = subtitlesElement;
|
||||||
this.setSubtitleAppearance(subtitlesContainer, this.#videoSubtitlesElem);
|
this.setSubtitleAppearance(subtitlesContainer, this.#videoSubtitlesElem);
|
||||||
videoElement.parentNode.appendChild(subtitlesContainer);
|
videoElement.parentNode.appendChild(subtitlesContainer);
|
||||||
this.#currentTrackEvents = data.TrackEvents;
|
this.#currentTrackEvents = subtitleData.TrackEvents;
|
||||||
|
} else if (!this.#videoSecondarySubtitlesElem && this.isSecondaryTrack(targetTextTrackIndex)) {
|
||||||
|
const subtitlesContainer = document.querySelector('.videoSubtitles');
|
||||||
|
if (!subtitlesContainer) return;
|
||||||
|
const secondarySubtitlesElement = document.createElement('div');
|
||||||
|
secondarySubtitlesElement.classList.add('videoSecondarySubtitlesInner');
|
||||||
|
// determine the order of the subtitles
|
||||||
|
if (subtitleVerticalPosition < 0) {
|
||||||
|
subtitlesContainer.insertBefore(secondarySubtitlesElement, subtitlesContainer.firstChild);
|
||||||
|
} else {
|
||||||
|
subtitlesContainer.appendChild(secondarySubtitlesElement);
|
||||||
|
}
|
||||||
|
this.#videoSecondarySubtitlesElem = secondarySubtitlesElement;
|
||||||
|
this.setSubtitleAppearance(subtitlesContainer, this.#videoSecondarySubtitlesElem);
|
||||||
|
this.#currentSecondaryTrackEvents = subtitleData.TrackEvents;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1211,7 +1377,7 @@ function tryRemoveElement(elem) {
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
renderTracksEvents(videoElement, track, item) {
|
renderTracksEvents(videoElement, track, item, targetTextTrackIndex = PRIMARY_TEXT_TRACK_INDEX) {
|
||||||
if (!itemHelper.isLocalItem(item) || track.IsExternal) {
|
if (!itemHelper.isLocalItem(item) || track.IsExternal) {
|
||||||
const format = (track.Codec || '').toLowerCase();
|
const format = (track.Codec || '').toLowerCase();
|
||||||
if (format === 'ssa' || format === 'ass') {
|
if (format === 'ssa' || format === 'ass') {
|
||||||
|
@ -1220,15 +1386,15 @@ function tryRemoveElement(elem) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.requiresCustomSubtitlesElement()) {
|
if (this.requiresCustomSubtitlesElement()) {
|
||||||
this.renderSubtitlesWithCustomElement(videoElement, track, item);
|
this.renderSubtitlesWithCustomElement(videoElement, track, item, targetTextTrackIndex);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let trackElement = null;
|
let trackElement = null;
|
||||||
if (videoElement.textTracks && videoElement.textTracks.length > 0) {
|
const updatingTrack = videoElement.textTracks && videoElement.textTracks.length > (this.isSecondaryTrack(targetTextTrackIndex) ? 1 : 0);
|
||||||
trackElement = videoElement.textTracks[0];
|
if (updatingTrack) {
|
||||||
|
trackElement = videoElement.textTracks[targetTextTrackIndex];
|
||||||
// This throws an error in IE, but is fine in chrome
|
// 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
|
// In IE it's not necessary anyway because changing the src seems to be enough
|
||||||
try {
|
try {
|
||||||
|
@ -1288,24 +1454,29 @@ function tryRemoveElement(elem) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const trackEvents = this.#currentTrackEvents;
|
const allTrackEvents = [this.#currentTrackEvents, this.#currentSecondaryTrackEvents];
|
||||||
const subtitleTextElement = this.#videoSubtitlesElem;
|
const subtitleTextElements = [this.#videoSubtitlesElem, this.#videoSecondarySubtitlesElem];
|
||||||
|
|
||||||
if (trackEvents && subtitleTextElement) {
|
for (let i = 0; i < allTrackEvents.length; i++) {
|
||||||
const ticks = timeMs * 10000;
|
const trackEvents = allTrackEvents[i];
|
||||||
let selectedTrackEvent;
|
const subtitleTextElement = subtitleTextElements[i];
|
||||||
for (const trackEvent of trackEvents) {
|
|
||||||
if (trackEvent.StartPositionTicks <= ticks && trackEvent.EndPositionTicks >= ticks) {
|
if (trackEvents && subtitleTextElement) {
|
||||||
selectedTrackEvent = trackEvent;
|
const ticks = timeMs * 10000;
|
||||||
break;
|
let selectedTrackEvent;
|
||||||
|
for (const trackEvent of trackEvents) {
|
||||||
|
if (trackEvent.StartPositionTicks <= ticks && trackEvent.EndPositionTicks >= ticks) {
|
||||||
|
selectedTrackEvent = trackEvent;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedTrackEvent && selectedTrackEvent.Text) {
|
if (selectedTrackEvent && selectedTrackEvent.Text) {
|
||||||
subtitleTextElement.innerHTML = normalizeTrackEventText(selectedTrackEvent.Text, true);
|
subtitleTextElement.innerHTML = normalizeTrackEventText(selectedTrackEvent.Text, true);
|
||||||
subtitleTextElement.classList.remove('hide');
|
subtitleTextElement.classList.remove('hide');
|
||||||
} else {
|
} else {
|
||||||
subtitleTextElement.classList.add('hide');
|
subtitleTextElement.classList.add('hide');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1313,7 +1484,7 @@ function tryRemoveElement(elem) {
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
setCurrentTrackElement(streamIndex) {
|
setCurrentTrackElement(streamIndex, targetTextTrackIndex) {
|
||||||
console.debug(`setting new text track index to: ${streamIndex}`);
|
console.debug(`setting new text track index to: ${streamIndex}`);
|
||||||
|
|
||||||
const mediaStreamTextTracks = getMediaStreamTextTracks(this._currentPlayOptions.mediaSource);
|
const mediaStreamTextTracks = getMediaStreamTextTracks(this._currentPlayOptions.mediaSource);
|
||||||
|
@ -1322,7 +1493,7 @@ function tryRemoveElement(elem) {
|
||||||
return t.Index === streamIndex;
|
return t.Index === streamIndex;
|
||||||
})[0];
|
})[0];
|
||||||
|
|
||||||
this.setTrackForDisplay(this.#mediaElement, track);
|
this.setTrackForDisplay(this.#mediaElement, track, targetTextTrackIndex);
|
||||||
if (enableNativeTrackSupport(this.#currentSrc, track)) {
|
if (enableNativeTrackSupport(this.#currentSrc, track)) {
|
||||||
if (streamIndex !== -1) {
|
if (streamIndex !== -1) {
|
||||||
this.setCueAppearance();
|
this.setCueAppearance();
|
||||||
|
@ -1500,6 +1671,7 @@ function tryRemoveElement(elem) {
|
||||||
|
|
||||||
list.push('SetBrightness');
|
list.push('SetBrightness');
|
||||||
list.push('SetAspectRatio');
|
list.push('SetAspectRatio');
|
||||||
|
list.push('SecondarySubtitles');
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,13 +65,22 @@ video[controls]::-webkit-media-controls {
|
||||||
padding-left: env(safe-area-inset-left);
|
padding-left: env(safe-area-inset-left);
|
||||||
padding-right: env(safe-area-inset-right);
|
padding-right: env(safe-area-inset-right);
|
||||||
padding-bottom: env(safe-area-inset-bottom);
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.videoSubtitlesInner {
|
.videoSubtitlesInner {
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
background-color: rgba(0, 0, 0, 0.8);
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
margin: auto;
|
}
|
||||||
display: inline-block;
|
|
||||||
|
.videoSecondarySubtitlesInner {
|
||||||
|
max-width: 70%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
min-height: 0 !important;
|
||||||
|
margin-top: 0.5em !important;
|
||||||
|
margin-bottom: 0.5em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes htmlvideoplayer-zoomin {
|
@keyframes htmlvideoplayer-zoomin {
|
||||||
|
|
|
@ -1412,6 +1412,7 @@
|
||||||
"SearchForSubtitles": "Search for Subtitles",
|
"SearchForSubtitles": "Search for Subtitles",
|
||||||
"SearchResults": "Search Results",
|
"SearchResults": "Search Results",
|
||||||
"Season": "Season",
|
"Season": "Season",
|
||||||
|
"SecondarySubtitles": "Secondary Subtitles",
|
||||||
"SelectAdminUsername": "Please select a username for the admin account.",
|
"SelectAdminUsername": "Please select a username for the admin account.",
|
||||||
"SelectServer": "Select Server",
|
"SelectServer": "Select Server",
|
||||||
"SendMessage": "Send message",
|
"SendMessage": "Send message",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue