diff --git a/docs/es-modules/subtitles-and-captions.html b/docs/es-modules/subtitles-and-captions.html index c1e0f7c0..9e2e048e 100644 --- a/docs/es-modules/subtitles-and-captions.html +++ b/docs/es-modules/subtitles-and-captions.html @@ -104,6 +104,17 @@

Karaoke player

width="500" > +

Translated Transcript

+ + +

Full documentationKaraoke player import 'cloudinary-video-player/playlist'; const player = videoPlayer('player', { - cloudName: 'demo' + cloudName: 'prod' }); - player.source('video-player/stubhub', { - textTracks: { - captions: { - label: 'English captions', - language: 'en', - default: true, - url: 'https://res.cloudinary.com/demo/raw/upload/v1636972013/video-player/vtt/Meetup_english.vtt' - }, - subtitles: [ - { - label: 'German subtitles', - language: 'de', - url: 'https://res.cloudinary.com/demo/raw/upload/v1636970250/video-player/vtt/Meetup_german.vtt' + player.source( + 'video/examples/big_buck_bunny_trailer_720p', + { + info: { title: 'SRT & VTT from URL' }, + textTracks: { + options: { + theme: "videojs-default" }, - { - label: 'Russian subtitles', - language: 'ru', - url: 'https://res.cloudinary.com/demo/raw/upload/v1636970275/video-player/vtt/Meetup_russian.vtt' - } - ] + captions: { + label: 'VTT from URL', + default: true, + url: 'https://res.cloudinary.com/prod/raw/upload/video/examples/big_buck_bunny_trailer_720p.vtt' + }, + subtitles: [ + { + label: 'SRT from URL', + url: 'https://res.cloudinary.com/prod/raw/upload/video/examples/big_buck_bunny_trailer_720p.srt' + } + ] + } } - }); + ); // Playlist const playlist = videoPlayer('playlist', { @@ -202,7 +213,7 @@

Karaoke player

playlist.playlist(playlistSources, playlistOptions); // Paced - const pacedPlayer = cloudinary.videoPlayer('paced', { + const pacedPlayer = videoPlayer('paced', { cloudName: 'prod' }); @@ -250,7 +261,7 @@

Karaoke player

}); // Karaoke - const karaokePlayer = cloudinary.videoPlayer('karaoke', { + const karaokePlayer = videoPlayer('karaoke', { cloudName: 'prod' }); @@ -282,7 +293,7 @@

Karaoke player

}); // Auto-translated transcript - const translatedTranscriptPlayer = cloudinary.videoPlayer('translated-transcript', { + const translatedTranscriptPlayer = videoPlayer('translated-transcript', { cloudName: 'prod' }); diff --git a/docs/subtitles-and-captions.html b/docs/subtitles-and-captions.html index cde74bb7..c115377d 100644 --- a/docs/subtitles-and-captions.html +++ b/docs/subtitles-and-captions.html @@ -28,29 +28,26 @@ window.addEventListener('load', function(){ var player = cloudinary.videoPlayer('player', { - cloud_name: 'demo' + cloud_name: 'prod' }); player.source( - 'video-player/stubhub', + 'video/examples/big_buck_bunny_trailer_720p', { + info: { title: 'SRT & VTT from URL' }, textTracks: { + options: { + theme: "videojs-default" + }, captions: { - label: 'English captions', - language: 'en', + label: 'VTT from URL', default: true, - url: 'https://res.cloudinary.com/demo/raw/upload/v1636972013/video-player/vtt/Meetup_english.vtt' + url: 'https://res.cloudinary.com/prod/raw/upload/video/examples/big_buck_bunny_trailer_720p.vtt' }, subtitles: [ { - label: 'German subtitles', - language: 'de', - url: 'https://res.cloudinary.com/demo/raw/upload/v1636970250/video-player/vtt/Meetup_german.vtt' - }, - { - label: 'Russian subtitles', - language: 'ru', - url: 'https://res.cloudinary.com/demo/raw/upload/v1636970275/video-player/vtt/Meetup_russian.vtt' + label: 'SRT from URL', + url: 'https://res.cloudinary.com/prod/raw/upload/video/examples/big_buck_bunny_trailer_720p.srt' } ] } @@ -353,29 +350,26 @@

Example Code:

// Initialize players var player = cloudinary.videoPlayer('player', { - cloud_name: 'demo' + cloud_name: 'prod' }); player.source( - 'video-player/stubhub', + 'video/examples/big_buck_bunny_trailer_720p', { + info: { title: 'SRT & VTT from URL' }, textTracks: { + options: { + theme: "videojs-default" + }, captions: { - label: 'English captions', - language: 'en', + label: 'VTT from URL', default: true, - url: 'https://res.cloudinary.com/demo/raw/upload/v1636972013/video-player/vtt/Meetup_english.vtt' + url: 'https://res.cloudinary.com/prod/raw/upload/video/examples/big_buck_bunny_trailer_720p.vtt' }, subtitles: [ { - label: 'German subtitles', - language: 'de', - url: 'https://res.cloudinary.com/demo/raw/upload/v1636970250/video-player/vtt/Meetup_german.vtt' - }, - { - label: 'Russian subtitles', - language: 'ru', - url: 'https://res.cloudinary.com/demo/raw/upload/v1636970275/video-player/vtt/Meetup_russian.vtt' + label: 'SRT from URL', + url: 'https://res.cloudinary.com/prod/raw/upload/video/examples/big_buck_bunny_trailer_720p.srt' } ] } diff --git a/package-lock.json b/package-lock.json index 5fa186f3..1cc66c93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "cloudinary-video-analytics": "1.7.1", "cloudinary-video-player-profiles": "1.1.0", "lodash": "^4.17.21", + "srt-parser-2": "^1.2.3", "uuid": "^10.0.0", "video.js": "^8.17.1", "videojs-contrib-ads": "^7.5.2", @@ -17014,6 +17015,18 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, + "node_modules/srt-parser-2": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/srt-parser-2/-/srt-parser-2-1.2.3.tgz", + "integrity": "sha512-dANP1AyJTI503H0/kXwRza+7QxDB3BqeFvEKTF4MI9lQcBe8JbRUQTKVIGzGABJCwBovEYavZ2Qsdm/s8XKz8A==", + "license": "MIT", + "bin": { + "srt-parser-2": "bin/index.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", diff --git a/package.json b/package.json index 5dc4012e..cbce58f7 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "cloudinary-video-analytics": "1.7.1", "cloudinary-video-player-profiles": "1.1.0", "lodash": "^4.17.21", + "srt-parser-2": "^1.2.3", "uuid": "^10.0.0", "video.js": "^8.17.1", "videojs-contrib-ads": "^7.5.2", diff --git a/src/plugins/index.js b/src/plugins/index.js index 684421a3..90e26c1c 100644 --- a/src/plugins/index.js +++ b/src/plugins/index.js @@ -20,6 +20,7 @@ import chapters from './chapters'; import imaPlugin from './ima'; import playlist from './playlist'; import shoppable from './shoppable-plugin'; +import srtTextTracks from './srt-text-tracks'; import styledTextTracks from './styled-text-tracks'; import interactionAreas from './interaction-areas'; @@ -40,6 +41,7 @@ const plugins = { imaPlugin, playlist, shoppable, + srtTextTracks, styledTextTracks, interactionAreas }; diff --git a/src/plugins/paced-transcript/index.js b/src/plugins/paced-transcript/index.js index 6be26564..ab4f6c61 100644 --- a/src/plugins/paced-transcript/index.js +++ b/src/plugins/paced-transcript/index.js @@ -52,7 +52,7 @@ function pacedTranscript(config) { transcriptResponse = await fallbackFetch(`${basePath}.transcript`); } } - if (!transcriptResponse.ok) return; + if (!transcriptResponse?.ok) return; const transcriptData = await transcriptResponse.json(); const captions = parseTranscript(transcriptData); diff --git a/src/plugins/srt-text-tracks/index.js b/src/plugins/srt-text-tracks/index.js new file mode 100644 index 00000000..19dc9c79 --- /dev/null +++ b/src/plugins/srt-text-tracks/index.js @@ -0,0 +1,10 @@ +import srtTextTracks from './srt-text-tracks'; + +export default async function srtTextTracksPlugin(config) { + const player = this; + try { + player.ready(() => srtTextTracks(config, player)); + } catch (error) { + console.error('Failed to load plugin:', error); + } +} diff --git a/src/plugins/srt-text-tracks/srt-text-tracks.js b/src/plugins/srt-text-tracks/srt-text-tracks.js new file mode 100644 index 00000000..4859a733 --- /dev/null +++ b/src/plugins/srt-text-tracks/srt-text-tracks.js @@ -0,0 +1,60 @@ +import srtParser2 from 'srt-parser-2'; + +function srtTextTracks(config, player) { + // Load the SRT file and convert it to WebVTT + const initSRT = async () => { + let srtResponse; + if (config.src) { + try { + srtResponse = await fetch(config.src); + if (!srtResponse.ok) { + throw new Error(`Failed fetching from ${config.src} with status code ${srtResponse.status}`); + } + } catch (error) { + console.error(error); + } + } + if (!srtResponse.ok) return; + + const srtData = await srtResponse.text(); + const webvttCues = srt2webvtt(srtData); // Get the array of cues + + const srtTrack = player.addRemoteTextTrack({ + kind: config.kind || 'subtitles', + label: config.label || 'Subtitles', + srclang: config.srclang, + default: config.default, + mode: config.default ? 'showing' : 'disabled' + }); + + // required for Safari to display the captions + // https://github.com/videojs/video.js/issues/8519 + await new Promise(resolve => setTimeout(resolve, 100)); + + // Add the WebVTT data to the track + webvttCues.forEach(cue => { + if (cue) { + srtTrack.track.addCue(new VTTCue(cue.startTime, cue.endTime, cue.text)); + } + }); + }; + + player.one('loadedmetadata', () => { + initSRT(); + }); +} + +// SRT parser +const srt2webvtt = data => { + const SRTParser = new srtParser2(); + + const cues = SRTParser.fromSrt(data); + + return cues.map(cue => ({ + startTime: cue.startSeconds, + endTime: cue.endSeconds, + text: cue.text + })); +}; + +export default srtTextTracks; diff --git a/src/utils/cloudinary.js b/src/utils/cloudinary.js index cf7e83a1..4ecf9108 100644 --- a/src/utils/cloudinary.js +++ b/src/utils/cloudinary.js @@ -107,6 +107,8 @@ const addTextTracks = (tracks, videojs) => { videojs.addRemoteTextTrack(track, true); } }); + } else if (track.src && track.src.endsWith('.srt')) { + videojs.srtTextTracks(track); } else if (videojs.pacedTranscript && (!track.src || track.src.endsWith('.transcript'))) { videojs.pacedTranscript(track); } diff --git a/src/utils/get-analytics-player-options.js b/src/utils/get-analytics-player-options.js index ffb33eec..7c9ab48f 100644 --- a/src/utils/get-analytics-player-options.js +++ b/src/utils/get-analytics-player-options.js @@ -23,12 +23,15 @@ const getTranscriptOptions = (textTracks = {}) => { const tracksArr = [textTracks.captions, ...textTracks.subtitles]; return { textTracks: hasConfig(textTracks), + textTracksLength: tracksArr.length, + textTracksOptions: hasConfig(textTracks.options) || Object.keys(textTracks.options).join(','), pacedTextTracks: hasConfig(textTracks) && JSON.stringify(textTracks || {}).includes('"maxWords":') || null, wordHighlight: hasConfig(textTracks) && JSON.stringify(textTracks || {}).includes('"wordHighlight":') || null, + transcriptLanguages: tracksArr.filter((track) => !track.url).map((track) => track.language || '').join(',') || null, transcriptAutoLoaded: tracksArr.some((track) => !track.url) || null, transcriptFromURl: tracksArr.some((track) => track.url?.endsWith('.transcript')) || null, - transcriptLanguages: tracksArr.filter((track) => !track.url).map((track) => track.language || '').join(',') || null, - vttFromUrl: tracksArr.some((track) => track.url?.endsWith('.vtt')) || null + vttFromUrl: tracksArr.some((track) => track.url?.endsWith('.vtt')) || null, + srtFromUrl: tracksArr.some((track) => track.url?.endsWith('.srt')) || null }; };