diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js
index b3c519111e4..f5a8214601f 100644
--- a/src/components/playback/playbackmanager.js
+++ b/src/components/playback/playbackmanager.js
@@ -29,6 +29,7 @@ import { MediaError } from 'types/mediaError';
import { getMediaError } from 'utils/mediaError';
import { toApi } from 'utils/jellyfin-apiclient/compat';
import { bindSkipSegment } from './skipsegment.ts';
+import { TICKS_PER_SECOND } from 'constants/time';
const UNLIMITED_ITEMS = -1;
@@ -1640,6 +1641,50 @@ export class PlaybackManager {
return index !== -1 && self.isSubtitleStreamExternal(index, player);
};
+ /**
+ * Gets the current subtitle track events in a normalized format.
+ * This method attempts to get subtitle events from either:
+ * 1. The player's getCurrentTrackEvents method (for Jellyfin events)
+ * 2. The player's getTextTracks method (for VTT cues)
+ *
+ * @param {Object} player - The player instance to get subtitle events from
+ * @returns {Array<{startTime: number, endTime: number, text: string}>|null} An array of normalized subtitle events or null if none found
+ */
+ self.getCurrentSubtitleTrackEvents = function (player) {
+ player = player || self._currentPlayer;
+
+ // Try to get events using the new method
+ if (player && player.getCurrentTrackEvents) {
+ const events = player.getCurrentTrackEvents();
+ if (events && events.length) {
+ // Convert ticks to seconds format
+ return events.map(event => ({
+ startTime: event.StartPositionTicks / TICKS_PER_SECOND,
+ endTime: event.EndPositionTicks / TICKS_PER_SECOND,
+ text: event.Text || ''
+ }));
+ }
+ }
+
+ // Try to get text tracks using the player's getTextTracks method
+ if (player && player.getTextTracks) {
+ const textTracks = player.getTextTracks();
+ if (textTracks && textTracks.length > 0) {
+ const activeTrack = textTracks[0]; // Get the first active track
+ if (activeTrack && activeTrack.cues && activeTrack.cues.length > 0) {
+ // Convert VTTCues to our format
+ return Array.from(activeTrack.cues).map(cue => ({
+ startTime: cue.startTime,
+ endTime: cue.endTime,
+ text: cue.text || ''
+ }));
+ }
+ }
+ }
+
+ return null;
+ };
+
self.seek = function (ticks, player) {
ticks = Math.max(0, ticks);
diff --git a/src/components/subtitlesync/subtitlesync.js b/src/components/subtitlesync/subtitlesync.js
index aae73957e56..32e504ca4a8 100644
--- a/src/components/subtitlesync/subtitlesync.js
+++ b/src/components/subtitlesync/subtitlesync.js
@@ -1,170 +1,412 @@
-
import { playbackManager } from '../playback/playbackmanager';
import layoutManager from '../layoutManager';
import template from './subtitlesync.template.html';
import './subtitlesync.scss';
+import { PlaybackSubscriber } from '../../apps/stable/features/playback/utils/playbackSubscriber';
-let player;
-let subtitleSyncSlider;
-let subtitleSyncTextField;
-let subtitleSyncCloseButton;
-let subtitleSyncContainer;
-
-function init(instance) {
- const parent = document.createElement('div');
- document.body.appendChild(parent);
- parent.innerHTML = template;
-
- subtitleSyncSlider = parent.querySelector('.subtitleSyncSlider');
- subtitleSyncTextField = parent.querySelector('.subtitleSyncTextField');
- subtitleSyncCloseButton = parent.querySelector('.subtitleSync-closeButton');
- subtitleSyncContainer = parent.querySelector('.subtitleSyncContainer');
-
- if (layoutManager.tv) {
- subtitleSyncSlider.classList.add('focusable');
- // HACK: Delay to give time for registered element attach (Firefox)
- setTimeout(function () {
- subtitleSyncSlider.enableKeyboardDragging();
- }, 0);
- }
-
- subtitleSyncContainer.classList.add('hide');
-
- subtitleSyncTextField.updateOffset = function (offset) {
- this.textContent = offset + 's';
- };
-
- subtitleSyncTextField.addEventListener('click', function () {
- // keep focus to prevent fade with osd
- this.hasFocus = true;
- });
-
- subtitleSyncTextField.addEventListener('keydown', function (event) {
- if (event.key === 'Enter') {
- // if input key is enter search for float pattern
- let inputOffset = /[-+]?\d+\.?\d*/g.exec(this.textContent);
- if (inputOffset) {
- inputOffset = inputOffset[0];
- inputOffset = parseFloat(inputOffset);
-
- subtitleSyncSlider.updateOffset(inputOffset);
- } else {
- this.textContent = (playbackManager.getPlayerSubtitleOffset(player) || 0) + 's';
- }
- this.hasFocus = false;
- event.preventDefault();
- } else {
- // keep focus to prevent fade with osd
- this.hasFocus = true;
- if (event.key.match(/[+-\d.s]/) === null) {
- event.preventDefault();
- }
+// Constants
+const TIMELINE_RESOLUTION_SECONDS = 10.0;
+const DEFAULT_OFFSET = 0;
+const TIME_MARKER_INTERVAL = 1; // 1-second intervals for precise timing
+const PERCENT_MAX = 100.0;
+
+function formatTimeMarker(time) {
+ const minutes = Math.floor(time / 60);
+ const seconds = Math.floor(time % 60);
+ return `${minutes}:${seconds.toString().padStart(2, '0')}`;
+}
+
+function createSliderBubbleHtml(_, value) {
+ return '
'
+ + (value > 0 ? '+' : '') + parseFloat(value) + 's'
+ + '
';
+}
+
+class SubtitleTimeline extends PlaybackSubscriber {
+ constructor(timelineRuler, eventsContainer, timelineWrapper, player) {
+ super(playbackManager);
+ this.timelineRuler = timelineRuler;
+ this.subtitleEventsContainer = eventsContainer;
+ this.subtitleTimelineWrapper = timelineWrapper;
+ this.player = player;
+ this.currentSubtitles = null;
+ }
+
+ onPlayerTimeUpdate() {
+ this._updateTimelineVisualization();
+ }
+
+ _updateTimelineVisualization() {
+ if (!this.currentSubtitles || !this.currentSubtitles.length) {
+ this.hide();
+ return;
}
- // FIXME: TV layout will require special handling for navigation keys. But now field is not focusable
- event.stopPropagation();
- });
+ // Get the current player time in seconds
+ const currentTime = this._getCurrentPlayerTime();
+
+ // Render the timeline with current subtitles
+ this.render(currentTime);
+ }
+
+ _getCurrentPlayerTime() {
+ return (this.player ? (playbackManager.currentTime(this.player) || 0) : 0) / 1000.0;
+ }
+
+ _getSubtitleEvents() {
+ if (!this.player) return null;
+ return playbackManager.getCurrentSubtitleTrackEvents(this.player);
+ }
+
+ updateEvents() {
+ // Get subtitles in our unified seconds-based format
+ this.currentSubtitles = this._getSubtitleEvents();
+ this._updateTimelineVisualization();
+ }
+
+ getTimeWindow(currentTime) {
+ const startTime = Math.max(0, currentTime - (TIMELINE_RESOLUTION_SECONDS / 2));
+ const endTime = startTime + TIMELINE_RESOLUTION_SECONDS;
+ return { startTime, endTime };
+ }
- subtitleSyncTextField.blur = function () {
- // prevent textfield to blur while element has focus
- if (!this.hasFocus && this.prototype) {
- this.prototype.blur();
+ generateTimeMarkers(currentTime) {
+ // Clear existing markers
+ this.timelineRuler.innerHTML = '';
+
+ const timeWindow = this.getTimeWindow(currentTime);
+ this._createTimeMarkers(timeWindow.startTime, timeWindow.endTime);
+ }
+
+ _createTimeMarkers(startTime, endTime) {
+ for (let time = startTime; time <= endTime; time += TIME_MARKER_INTERVAL) {
+ const marker = this._createTimeMarker(time, startTime);
+ this.timelineRuler.appendChild(marker);
}
- };
+ }
- function updateSubtitleOffset() {
- const value = parseFloat(subtitleSyncSlider.value);
- // set new offset
- playbackManager.setSubtitleOffset(value, player);
- // synchronize with textField value
- subtitleSyncTextField.updateOffset(value);
+ _createTimeMarker(time, startTime) {
+ const marker = document.createElement('div');
+ marker.classList.add('timelineMarker');
+
+ // Calculate position as percentage
+ const position = (((time - startTime) / TIMELINE_RESOLUTION_SECONDS) * PERCENT_MAX);
+ marker.style.left = `${position}%`;
+
+ // Format time as MM:SS
+ marker.textContent = formatTimeMarker(time);
+
+ return marker;
}
- subtitleSyncSlider.updateOffset = function (sliderValue) {
- // default value is 0s = 0ms
- this.value = sliderValue === undefined ? 0 : sliderValue;
+ renderEvents(currentTime) {
+ // Clear existing events
+ this.subtitleEventsContainer.innerHTML = '';
- updateSubtitleOffset();
- };
+ if (!this.currentSubtitles || this.currentSubtitles.length === 0) {
+ // Hide the timeline wrapper when no events exist
+ this.subtitleTimelineWrapper.style.display = 'none';
+ return;
+ }
- subtitleSyncSlider.addEventListener('change', () => updateSubtitleOffset());
+ // Show the timeline wrapper when events exist
+ this.subtitleTimelineWrapper.style.display = '';
- subtitleSyncSlider.getBubbleHtml = function (_, value) {
- return ''
- + (value > 0 ? '+' : '') + parseFloat(value) + 's'
- + '
';
- };
+ const timeWindow = this.getTimeWindow(currentTime);
+ const { startTime, endTime } = timeWindow;
- subtitleSyncCloseButton.addEventListener('click', function () {
- playbackManager.disableShowingSubtitleOffset(player);
- SubtitleSync.prototype.toggle('forceToHide');
- });
+ this.currentSubtitles.forEach(subtitle => {
+ if (!this._isEventVisible(subtitle, startTime, endTime)) return;
- instance.element = parent;
-}
+ const eventElement = this._createEventElement(subtitle, startTime);
+ this.subtitleEventsContainer.appendChild(eventElement);
+ });
+ }
-class SubtitleSync {
- constructor(currentPlayer) {
- player = currentPlayer;
- init(this);
+ _isEventVisible(subtitle, startTime, endTime) {
+ return !(subtitle.endTime <= startTime || subtitle.startTime >= endTime);
+ }
+
+ _createEventElement(subtitle, startTime) {
+ // Calculate position and width as percentages exactly proportional to duration
+ // Clamp to visible area
+ const leftPos = Math.max(0, ((subtitle.startTime - startTime) / TIMELINE_RESOLUTION_SECONDS) * PERCENT_MAX);
+ const rightPos = Math.min(PERCENT_MAX, ((subtitle.endTime - startTime) / TIMELINE_RESOLUTION_SECONDS) * PERCENT_MAX);
+
+ const eventEl = document.createElement('div');
+ eventEl.classList.add('subtitleEvent');
+
+ // Apply position and width exactly as calculated
+ eventEl.style.left = `${leftPos}%`;
+ eventEl.style.width = `${rightPos - leftPos}%`;
+
+ // Clean and add the text
+ eventEl.textContent = subtitle.text;
+ // Add a title for the full text on hover
+ eventEl.title = subtitle.text;
+
+ return eventEl;
+ }
+
+ render(currentTime) {
+ // Generate time markers - the time markers don't shift with offset
+ this.generateTimeMarkers(currentTime);
+
+ // Render subtitle events with the current offset
+ this.renderEvents(currentTime);
+ }
+
+ hide() {
+ this.subtitleTimelineWrapper.style.display = 'none';
}
destroy() {
- SubtitleSync.prototype.toggle('forceToHide');
- if (player) {
- playbackManager.disableShowingSubtitleOffset(player);
- playbackManager.setSubtitleOffset(0, player);
- }
- const elem = this.element;
- if (elem) {
- elem.parentNode.removeChild(elem);
- this.element = null;
- }
+ this.currentSubtitles = null;
+
+ // Call parent class destroy to clean up event subscriptions
+ super.destroy?.();
}
+}
- toggle(action) {
- if (action && !['hide', 'forceToHide'].includes(action)) {
- console.warn('SubtitleSync.toggle called with invalid action', action);
- return;
+class OffsetController {
+ constructor(player, slider, textField, onOffsetChange) {
+ this.player = player;
+ this.slider = slider;
+ this.textField = textField;
+ this.currentOffset = DEFAULT_OFFSET;
+ this.onOffsetChange = onOffsetChange;
+
+ this._initSlider();
+ this._initTextField();
+ }
+
+ _initSlider() {
+ const slider = this.slider;
+
+ if (layoutManager.tv) {
+ slider.classList.add('focusable');
+ // eslint-disable-next-line no-warning-comments
+ // HACK: Delay to give time for registered element attach (Firefox)
+ setTimeout(() => slider.enableKeyboardDragging(), 0);
}
- if (player && playbackManager.supportSubtitleOffset(player)) {
- if (!action) {
- // if showing subtitle sync is enabled and if there is an external subtitle stream enabled
- if (playbackManager.isShowingSubtitleOffsetEnabled(player) && playbackManager.canHandleOffsetOnCurrentSubtitle(player)) {
- // if no subtitle offset is defined or element has focus (offset being defined)
- if (!(playbackManager.getPlayerSubtitleOffset(player) || subtitleSyncTextField.hasFocus)) {
- // set default offset to '0' = 0ms
- subtitleSyncSlider.value = '0';
- subtitleSyncTextField.textContent = '0s';
- playbackManager.setSubtitleOffset(0, player);
- }
- // show subtitle sync
- subtitleSyncContainer.classList.remove('hide');
- return;
+ slider.addEventListener('change', () => this.updateOffset());
+ slider.getBubbleHtml = createSliderBubbleHtml;
+
+ // Simplified slider update method
+ slider.updateOffset = (value) => {
+ this.slider.value = value === undefined ? DEFAULT_OFFSET : value;
+ this.updateOffset();
+ };
+ }
+
+ _initTextField() {
+ const textField = this.textField;
+
+ textField.updateOffset = (offset) => {
+ textField.textContent = offset + 's';
+ };
+
+ textField.addEventListener('click', () => {
+ // keep focus to prevent fade with osd
+ textField.hasFocus = true;
+ });
+
+ textField.addEventListener('keydown', (event) => {
+ if (event.key === 'Enter') {
+ // if input key is enter search for float pattern
+ let inputOffset = /[-+]?\d+\.?\d*/g.exec(textField.textContent);
+ if (inputOffset) {
+ inputOffset = parseFloat(inputOffset[0]);
+ this.slider.updateOffset(inputOffset);
+ } else {
+ textField.textContent = this.currentOffset + 's';
+ }
+ textField.hasFocus = false;
+ event.preventDefault();
+ } else {
+ // keep focus to prevent fade with osd
+ textField.hasFocus = true;
+ if (event.key.match(/[+-\d.s]/) === null) {
+ event.preventDefault();
}
- } else if (action === 'hide' && subtitleSyncTextField.hasFocus) {
- // do not hide if element has focus
- return;
}
+ event.stopPropagation();
+ });
+
+ textField.blur = function() {
+ // prevent textfield to blur while element has focus
+ if (!this.hasFocus && this.prototype) {
+ this.prototype.blur();
+ }
+ };
+ }
+
+ updateOffset() {
+ const value = parseFloat(this.slider.value);
- subtitleSyncContainer.classList.add('hide');
+ // set new offset
+ playbackManager.setSubtitleOffset(value, this.player);
+
+ // synchronize with textField value
+ this.textField.updateOffset(value);
+
+ // update current offset
+ this.currentOffset = value;
+
+ // notify listeners
+ if (this.onOffsetChange) {
+ this.onOffsetChange(value);
}
}
- update(offset) {
- this.toggle();
+ adjustOffset(delta) {
+ const value = parseFloat(this.slider.value) + delta;
+ this.slider.updateOffset(value);
+ }
+
+ setOffset(offset) {
+ this.currentOffset = offset;
+ this.slider.value = offset.toString();
+ this.textField.updateOffset(offset);
+ }
+
+ reset() {
+ this.setOffset(DEFAULT_OFFSET);
+ playbackManager.setSubtitleOffset(DEFAULT_OFFSET, this.player);
+ }
+}
+
+class SubtitleSync {
+ constructor(currentPlayer) {
+ this.player = currentPlayer;
+
+ this._initUI();
+
+ // Create the timeline controller
+ this.timeline = new SubtitleTimeline(
+ this.timelineRuler,
+ this.subtitleEventsContainer,
+ this.subtitleTimelineWrapper,
+ this.player
+ );
+
+ // Create the offset controller
+ this.offsetController = new OffsetController(
+ this.player,
+ this.subtitleSyncSlider,
+ this.subtitleSyncTextField,
+ () => this._handleOffsetChange()
+ );
+ }
+
+ _initUI() {
+ const parent = document.createElement('div');
+ document.body.appendChild(parent);
+ parent.innerHTML = template;
+
+ // Store DOM elements
+ this.element = parent;
+ this.subtitleSyncSlider = parent.querySelector('.subtitleSyncSlider');
+ this.subtitleSyncTextField = parent.querySelector('.subtitleSyncTextField');
+ this.subtitleSyncCloseButton = parent.querySelector('.subtitleSync-closeButton');
+ this.subtitleSyncContainer = parent.querySelector('.subtitleSyncContainer');
+ this.timelineRuler = parent.querySelector('.timelineRuler');
+ this.subtitleEventsContainer = parent.querySelector('.subtitleEventsContainer');
+ this.subtitleTimelineWrapper = parent.querySelector('.subtitleTimelineWrapper');
+
+ this._setupCloseButton();
+
+ // Initially hide the container
+ this.subtitleSyncContainer.classList.add('hide');
+ }
+
+ _setupCloseButton() {
+ this.subtitleSyncCloseButton.addEventListener('click', () => {
+ playbackManager.disableShowingSubtitleOffset(this.player);
+ this.toggle('forceToHide');
+ });
+ }
- const value = parseFloat(subtitleSyncSlider.value) + offset;
- subtitleSyncSlider.updateOffset(value);
+ _handleOffsetChange() {
+ // Wait a short time for the player to apply the offset to track events
+ setTimeout(() => {
+ // Update timeline with fresh events
+ this.timeline.updateEvents();
+ }, 150); // Small delay to ensure offset has been applied to events
+ }
+
+ _tryShowSubtitleSync() {
+ // if showing subtitle sync is enabled and if there is an external subtitle stream enabled
+ if (!this._canShowSubtitleSync()) {
+ this.subtitleSyncContainer.classList.add('hide');
+ return;
+ }
+
+ // Update current offset from player
+ const currentOffset = playbackManager.getPlayerSubtitleOffset(this.player) || DEFAULT_OFFSET;
+ this.offsetController.setOffset(currentOffset);
+
+ // show subtitle sync
+ this.subtitleSyncContainer.classList.remove('hide');
+
+ // Initialize the timeline visualization with the current offset
+ this.timeline.updateEvents();
+ }
+
+ _canShowSubtitleSync() {
+ return playbackManager.isShowingSubtitleOffsetEnabled(this.player)
+ && playbackManager.canHandleOffsetOnCurrentSubtitle(this.player);
}
incrementOffset() {
- this.update(+subtitleSyncSlider.step);
+ this.toggle();
+ this.offsetController.adjustOffset(+this.subtitleSyncSlider.step);
}
decrementOffset() {
- this.update(-subtitleSyncSlider.step);
+ this.toggle();
+ this.offsetController.adjustOffset(-this.subtitleSyncSlider.step);
+ }
+
+ destroy() {
+ if (this.timeline) {
+ this.timeline.destroy();
+ this.timeline = null;
+ }
+
+ this.toggle('forceToHide');
+ if (this.player) {
+ playbackManager.disableShowingSubtitleOffset(this.player);
+ this.offsetController.reset();
+ }
+
+ if (this.element) {
+ this.element.parentNode.removeChild(this.element);
+ this.element = null;
+ }
+
+ this.player = null;
+ }
+
+ toggle(action) {
+ if (action && !['hide', 'forceToHide'].includes(action)) {
+ console.warn('SubtitleSync.toggle called with invalid action', action);
+ return;
+ }
+
+ if (!this.player || !playbackManager.supportSubtitleOffset(this.player)) {
+ return;
+ }
+
+ if (!action) {
+ this._tryShowSubtitleSync();
+ } else if (action === 'hide' && this.subtitleSyncTextField.hasFocus) {
+ // do not hide if element has focus
+ return;
+ } else {
+ this.subtitleSyncContainer.classList.add('hide');
+ }
}
}
diff --git a/src/components/subtitlesync/subtitlesync.scss b/src/components/subtitlesync/subtitlesync.scss
index b0c3d6a759f..6db976d0f50 100644
--- a/src/components/subtitlesync/subtitlesync.scss
+++ b/src/components/subtitlesync/subtitlesync.scss
@@ -4,36 +4,32 @@
}
.subtitleSyncContainer {
- width: 40%;
- min-width: 18em;
- margin-left: auto;
- margin-right: auto;
- height: 4.2em;
- background: rgba(28, 28, 28, 0.8);
- border-radius: 0.3em;
- color: #fff;
+ width: 50%;
+ min-width: 24em;
+ margin: 0 auto;
+ background: rgba(32, 32, 32, 0.9);
+ border-radius: 0.2em;
+ color: rgba(255, 255, 255, 0.8);
position: relative;
+ padding: 1rem 1rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1rem;
}
.subtitleSync-closeButton {
position: absolute;
- top: 0;
- right: 0;
- color: #ccc;
+ top: 0.5rem;
+ right: 0.5rem;
+ color: rgba(255, 255, 255, 0.7);
z-index: 2;
}
.subtitleSyncTextField {
- position: absolute;
- left: 0;
- width: 40%;
- margin-left: 30%;
- margin-right: 30%;
- top: 0.1em;
text-align: center;
font-size: 20px;
- color: white;
- z-index: 2;
+ color: rgba(255, 255, 255, 0.8);
}
#prompt {
@@ -42,11 +38,101 @@
.subtitleSyncSliderContainer {
width: 98%;
- margin-left: 1%;
- margin-right: 1%;
- top: 2.5em;
- height: 1.4em;
- flex-grow: 1;
border-radius: 0.3em;
+ position: relative;
+}
+
+.subtitleTimelineWrapper {
+ width: 100%;
+ position: relative;
+}
+
+/* Timeline view styles */
+.subtitleSyncTimeline {
+ position: relative;
+ width: 100%;
+ height: 7em;
+ background: rgba(0, 0, 0, 0.2);
+ border-radius: 0.2em;
+ overflow: hidden;
+}
+
+.timelineRuler {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 1.8em;
+ background: rgba(0, 0, 0, 0.2);
z-index: 1;
+ padding: 0 0.5em;
+}
+
+.timelineMarker {
+ position: absolute;
+ font-size: 0.8em;
+ color: rgba(255, 255, 255, 0.7);
+ text-align: center;
+ width: 2.5em;
+ margin-left: -1.25em;
+}
+
+.timelineMarker::after {
+ content: '';
+ position: absolute;
+ bottom: -0.3em;
+ left: 50%;
+ width: 1px;
+ height: 0.3em;
+ background: rgba(255, 255, 255, 0.5);
+}
+
+.subtitleEventsContainer {
+ position: absolute;
+ top: 2em;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ overflow: hidden;
+ padding: 0.5em;
+}
+
+.subtitleEvent {
+ position: absolute;
+ background: rgba(41, 41, 41, 0.8);
+ border: 1px solid #00a4dc;
+ border-radius: 0.2em;
+ padding: 0.3em 0.5em;
+ color: rgba(255, 255, 255, 0.8);
+ font-size: 0.8em;
+ height: 3.2em;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ box-shadow: none;
+ transition: background-color 0.2s;
+ top: 0.9em;
+ text-align: left;
+ white-space: normal;
+ line-height: 1.2;
+ box-sizing: border-box;
+ display: -webkit-box;
+ line-clamp: 2;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+}
+
+.subtitleEvent:hover {
+ transform: none;
+ z-index: 10;
+ background: rgba(51, 51, 51, 0.9);
+}
+
+.currentTimeIndicator {
+ position: absolute;
+ top: 2em;
+ bottom: 0.5em;
+ width: 2px;
+ background: #00a4dc;
+ left: 50%;
+ z-index: 2;
}
diff --git a/src/components/subtitlesync/subtitlesync.template.html b/src/components/subtitlesync/subtitlesync.template.html
index eb36d582601..0f0f6f185f6 100644
--- a/src/components/subtitlesync/subtitlesync.template.html
+++ b/src/components/subtitlesync/subtitlesync.template.html
@@ -5,5 +5,18 @@
+
diff --git a/src/plugins/htmlVideoPlayer/plugin.js b/src/plugins/htmlVideoPlayer/plugin.js
index fcaff8f3235..136d9079136 100644
--- a/src/plugins/htmlVideoPlayer/plugin.js
+++ b/src/plugins/htmlVideoPlayer/plugin.js
@@ -728,6 +728,10 @@ export class HtmlVideoPlayer {
return this.#currentTrackOffset;
}
+ getCurrentTrackEvents() {
+ return this.#currentTrackEvents || [];
+ }
+
isPrimaryTrack(textTrackIndex) {
return textTrackIndex === PRIMARY_TEXT_TRACK_INDEX;
}