From aaf8ee7e35029805a5a2394b6d3c5f7467156c26 Mon Sep 17 00:00:00 2001 From: Tsachi Shlidor Date: Tue, 4 Nov 2025 16:25:41 +0200 Subject: [PATCH 1/2] fix: Parsing VTT with cue attributes like 'align' & 'line' --- src/plugins/text-tracks-manager/index.js | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/plugins/text-tracks-manager/index.js b/src/plugins/text-tracks-manager/index.js index b874eb24..3c598179 100644 --- a/src/plugins/text-tracks-manager/index.js +++ b/src/plugins/text-tracks-manager/index.js @@ -2,7 +2,6 @@ import { utf8ToBase64 } from '../../utils/utf8Base64'; import { getCloudinaryUrlPrefix } from '../cloudinary/common'; import { transcriptParser } from './parsers/transcriptParser'; import { srtParser } from './parsers/srtParser'; -import { vttParser } from './parsers/vttParser'; import { addTextTrackCues, fetchFileContent, refreshTextTrack, removeAllTextTrackCues } from './utils'; const getTranscriptionFileUrl = (urlPrefix, deliveryType, publicId, languageCode = null) => @@ -54,6 +53,25 @@ function textTracksManager() { const updateTextTrackStatusToError = (textTrack, error) => updateTextTrackData(textTrack, { status: 'error', error }); const updateTextTrackStatusToApplied = (textTrack) => updateTextTrackData(textTrack, { status: 'applied' }); + const addNativeVttTrack = (config) => { + const { + kind = 'subtitles', + label = 'Subtitles', + default: isDefault, + srclang, + src, + } = config; + + player.addRemoteTextTrack({ + kind, + label, + srclang, + src, + default: isDefault, + mode: isDefault ? 'showing' : 'disabled', + }); + }; + const addTextTrack = (type, config) => { const { kind = type === 'transcript' ? 'captions' : 'subtitles', @@ -77,7 +95,6 @@ function textTracksManager() { const createParser = () => { if (type === 'srt') return srtParser; - if (type === 'vtt') return vttParser; return (text) => transcriptParser(text, { maxWords: config.maxWords, wordHighlight: config.wordHighlight, @@ -134,7 +151,7 @@ function textTracksManager() { const addTextTracks = (textTracks) => { textTracks.forEach(textTrackConfig => { if (textTrackConfig.src && textTrackConfig.src.endsWith('.vtt')) { - addTextTrack('vtt', textTrackConfig); + addNativeVttTrack(textTrackConfig); } else if (textTrackConfig.src && textTrackConfig.src.endsWith('.srt')) { addTextTrack('srt', textTrackConfig); } else if (!textTrackConfig.src || textTrackConfig.src.endsWith('.transcript')) { From 7d9ecc09a88ef1b70fc60ab2e6098419764beb17 Mon Sep 17 00:00:00 2001 From: Tsachi Shlidor Date: Tue, 4 Nov 2025 16:26:20 +0200 Subject: [PATCH 2/2] chore: cleanup --- .../text-tracks-manager/parsers/vttParser.js | 65 ------------------- src/plugins/text-tracks-manager/utils.js | 16 ----- 2 files changed, 81 deletions(-) delete mode 100644 src/plugins/text-tracks-manager/parsers/vttParser.js diff --git a/src/plugins/text-tracks-manager/parsers/vttParser.js b/src/plugins/text-tracks-manager/parsers/vttParser.js deleted file mode 100644 index 33675dd1..00000000 --- a/src/plugins/text-tracks-manager/parsers/vttParser.js +++ /dev/null @@ -1,65 +0,0 @@ -export const vttParser = async (fileContent) => { - const lines = fileContent.trim().split(/\r?\n/); - const cues = []; - let buffer = []; - - const parseTime = (timeStr) => { - const [hms, ms] = timeStr.split('.'); - const [h, m, s] = hms.split(':').map(Number); - return h * 3600 + m * 60 + s + (parseFloat(`0.${ms}`) || 0); - }; - - const parseCueBlock = (blockLines) => { - const cueLines = blockLines.filter(Boolean); - if (cueLines.length < 2) return null; - - const timeLineIdx = cueLines[0].includes('-->') ? 0 : 1; - const timeLine = cueLines[timeLineIdx]; - - const [startPart, endPartWithSettings] = timeLine.split('-->'); - const start = startPart.trim(); - - const [end, ...cueSettingsParts] = endPartWithSettings.trim().split(/\s+/); - const cueSettings = cueSettingsParts.join(' '); // like: line:90% position:50% align:start - - const text = cueLines.slice(timeLineIdx + 1).join('\n').trim(); - - return { - startTime: parseTime(start), - endTime: parseTime(end), - text, - settings: cueSettings || null, - }; - }; - - let inSkipBlock = false; - - for (const line of lines) { - if (/^(NOTE|STYLE|REGION)/.test(line.trim())) { - inSkipBlock = true; - continue; - } - - if (inSkipBlock && line.trim() === '') { - inSkipBlock = false; - continue; - } - - if (inSkipBlock) continue; - - if (line.trim() === '') { - const cue = parseCueBlock(buffer); - if (cue) cues.push(cue); - buffer = []; - } else { - buffer.push(line); - } - } - - if (buffer.length > 0) { - const cue = parseCueBlock(buffer); - if (cue) cues.push(cue); - } - - return cues; -}; diff --git a/src/plugins/text-tracks-manager/utils.js b/src/plugins/text-tracks-manager/utils.js index a0a9d35e..cd5bc640 100644 --- a/src/plugins/text-tracks-manager/utils.js +++ b/src/plugins/text-tracks-manager/utils.js @@ -106,22 +106,6 @@ export const removeAllTextTrackCues = (textTrack) => { }); }; -export const addNotificationCue = (videoDuration, textTrack, text) => { - if (!textTrack || !text) return; - - removeAllTextTrackCues(textTrack); - - const fallbackDuration = 3600; - const endTime = ( - typeof videoDuration === 'number' && - isFinite(videoDuration) && - videoDuration > 0 - ) ? videoDuration : fallbackDuration; - - const cue = new VTTCue(0, endTime, text); - textTrack.addCue(cue); -}; - export const refreshTextTrack = (textTrack) => { if (!textTrack || typeof textTrack.mode !== 'string') return;