Skip to content

Commit

Permalink
feat: support srt subtitle format (#743)
Browse files Browse the repository at this point in the history
* feat: support srt subtitle format

* chore: add example and usage monitoring

* chore: esm examples

* chore: esm example

* chore: examples

* feat: support srt subtitle format
  • Loading branch information
tsi authored Dec 5, 2024
1 parent 7787c36 commit 3461a76
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 53 deletions.
59 changes: 35 additions & 24 deletions docs/es-modules/subtitles-and-captions.html
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,17 @@ <h4 class="mt-4 mb-2">Karaoke player</h4>
width="500"
></video>

<h4 class="mt-4 mb-2">Translated Transcript</h4>

<video
id="translated-transcript"
playsinline
controls
class="cld-video-player cld-fluid"
crossorigin="anonymous"
width="500"
></video>

<p class="mt-4">
<a href="https://cloudinary.com/documentation/cloudinary_video_player"
>Full documentation</a
Expand All @@ -117,31 +128,31 @@ <h4 class="mt-4 mb-2">Karaoke player</h4>
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', {
Expand Down Expand Up @@ -202,7 +213,7 @@ <h4 class="mt-4 mb-2">Karaoke player</h4>
playlist.playlist(playlistSources, playlistOptions);

// Paced
const pacedPlayer = cloudinary.videoPlayer('paced', {
const pacedPlayer = videoPlayer('paced', {
cloudName: 'prod'
});

Expand Down Expand Up @@ -250,7 +261,7 @@ <h4 class="mt-4 mb-2">Karaoke player</h4>
});

// Karaoke
const karaokePlayer = cloudinary.videoPlayer('karaoke', {
const karaokePlayer = videoPlayer('karaoke', {
cloudName: 'prod'
});

Expand Down Expand Up @@ -282,7 +293,7 @@ <h4 class="mt-4 mb-2">Karaoke player</h4>
});

// Auto-translated transcript
const translatedTranscriptPlayer = cloudinary.videoPlayer('translated-transcript', {
const translatedTranscriptPlayer = videoPlayer('translated-transcript', {
cloudName: 'prod'
});

Expand Down
46 changes: 20 additions & 26 deletions docs/subtitles-and-captions.html
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
]
}
Expand Down Expand Up @@ -353,29 +350,26 @@ <h3 class="mt-4">Example Code:</h3>

// 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'
}
]
}
Expand Down
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -40,6 +41,7 @@ const plugins = {
imaPlugin,
playlist,
shoppable,
srtTextTracks,
styledTextTracks,
interactionAreas
};
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/paced-transcript/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
10 changes: 10 additions & 0 deletions src/plugins/srt-text-tracks/index.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
60 changes: 60 additions & 0 deletions src/plugins/srt-text-tracks/srt-text-tracks.js
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions src/utils/cloudinary.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
7 changes: 5 additions & 2 deletions src/utils/get-analytics-player-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
};

Expand Down

0 comments on commit 3461a76

Please sign in to comment.