diff --git a/docs/es-modules/fluid.html b/docs/es-modules/fluid.html index 464d03183..fbc70435d 100644 --- a/docs/es-modules/fluid.html +++ b/docs/es-modules/fluid.html @@ -39,7 +39,7 @@

Fluid Layouts

diff --git a/src/index.all.js b/src/index.all.js index 197791261..59dba7078 100644 --- a/src/index.all.js +++ b/src/index.all.js @@ -7,17 +7,18 @@ import cloudinary from './index.js'; -export * from './index.js'; -export * from './plugins/adaptive-streaming/adaptive-streaming.js'; -export * from './plugins/chapters/chapters.js'; -export * from './plugins/colors/colors.js'; -export * from './plugins/ima/ima.js'; -export * from './plugins/playlist/playlist.js'; -export * from './plugins/interaction-areas/interaction-areas.service.js'; -export * from './plugins/visual-search/visual-search.js'; -export * from './plugins/text-tracks-manager/index.js'; -export * from './plugins/share/share.js'; -export * from './components/shoppable-bar/shoppable-widget.js'; -export * from './components/recommendations-overlay/recommendations-overlay.js'; +// Import plugin implementations so webpack bundles them in /all.js +import './plugins/adaptive-streaming/adaptive-streaming.js'; +import './plugins/chapters/chapters.js'; +import './plugins/colors/colors.js'; +import './plugins/ima/ima.js'; +import './plugins/playlist/playlist.js'; +import './plugins/interaction-areas/interaction-areas.service.js'; +import './plugins/visual-search/visual-search.js'; +import './plugins/share/share.js'; +import './plugins/text-tracks-manager/text-tracks-manager.js'; +import './components/shoppable-bar/shoppable-widget.js'; +import './components/recommendations-overlay/recommendations-overlay.js'; +export * from './index.js'; export default cloudinary; diff --git a/src/index.es.js b/src/index.es.js deleted file mode 100644 index f19be2607..000000000 --- a/src/index.es.js +++ /dev/null @@ -1,19 +0,0 @@ -// This file is bundled as `cld-video-player.js` to be imported as a tree-shaken module. -// It is the default export of the Cloudinary Video Player. - -// Usage: -// import cloudinary from 'cloudinary-video-player'; -// Or: -// import { videoPlayer } from "cloudinary-video-player"; - -// Other modules can be imported like that: -// import dash from 'cloudinary-video-player/dash'; - -import cloudinary from './index.js'; - -export const videoPlayer = cloudinary.videoPlayer; -export const videoPlayers = cloudinary.videoPlayers; - -export const player = cloudinary.player; - -export default cloudinary; diff --git a/src/plugins/text-tracks-manager/index.js b/src/plugins/text-tracks-manager/index.js index b874eb24e..ca19fb938 100644 --- a/src/plugins/text-tracks-manager/index.js +++ b/src/plugins/text-tracks-manager/index.js @@ -1,194 +1,11 @@ -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) => - `${urlPrefix}/_applet_/video_service/transcription/${deliveryType}/${languageCode ? `${languageCode}/` : ''}${utf8ToBase64(publicId)}.transcript`; - -function textTracksManager() { +export default async function lazyTextTracksManagerPlugin() { const player = this; - const textTracksData = new WeakMap(); - let activeTrack = null; - - const removeAllTextTracks = () => { - const currentTracks = player.remoteTextTracks(); - if (currentTracks) { - for (let i = currentTracks.tracks_.length - 1; i >= 0; i--) { - player.removeRemoteTextTrack(currentTracks.tracks_[i]); - } - } - }; - - const createTextTrackData = (textTrack, loadMethod) => { - const controller = new AbortController(); - textTracksData.set(textTrack, { - status: 'idle', - load: async () => { - const { status } = textTracksData.get(textTrack); - if (status === 'idle') { - await loadMethod(controller.signal); - refreshTextTrack(textTrack); - } - }, - abortLoading: () => { - const { status } = textTracksData.get(textTrack); - if (status === 'pending') { - controller.abort(); - } - }, - }); - }; - - const updateTextTrackData = (textTrack, dataToUpdate) => { - const existingData = textTracksData.get(textTrack); - textTracksData.set(textTrack, { - ...existingData, - ...dataToUpdate, - }); - }; - const updateTextTrackStatusToPending = (textTrack) => updateTextTrackData(textTrack, { status: 'pending' }); - const updateTextTrackStatusToSuccess = (textTrack) => updateTextTrackData(textTrack, { status: 'success' }); - const updateTextTrackStatusToError = (textTrack, error) => updateTextTrackData(textTrack, { status: 'error', error }); - const updateTextTrackStatusToApplied = (textTrack) => updateTextTrackData(textTrack, { status: 'applied' }); - - const addTextTrack = (type, config) => { - const { - kind = type === 'transcript' ? 'captions' : 'subtitles', - label = type === 'transcript' ? 'Captions' : 'Subtitles', - default: isDefault, - srclang, - src, - } = config; - - if (type === 'transcript') { - player.textTrackDisplay.el().classList.add('cld-paced-text-tracks'); - } - - const { track } = player.addRemoteTextTrack({ - kind, - label, - srclang, - default: isDefault, - mode: isDefault ? 'showing' : 'disabled', - }); - - const createParser = () => { - if (type === 'srt') return srtParser; - if (type === 'vtt') return vttParser; - return (text) => transcriptParser(text, { - maxWords: config.maxWords, - wordHighlight: config.wordHighlight, - timeOffset: config.timeOffset ?? 0, - }); - }; - - const createSourceUrl = () => { - if (src) return src; - if (type !== 'transcript') return undefined; - - const source = player.cloudinary.source(); - const publicId = source.publicId(); - const deliveryType = source.resourceConfig().type; - const urlPrefix = getCloudinaryUrlPrefix(player.cloudinary.cloudinaryConfig()); - const baseUrl = getTranscriptionFileUrl(urlPrefix, deliveryType, publicId); - const localizedUrl = srclang ? getTranscriptionFileUrl(urlPrefix, deliveryType, publicId, srclang) : null; - - return localizedUrl ? localizedUrl : baseUrl; - }; - - createTextTrackData(track, async (signal) => { - updateTextTrackStatusToPending(track); - - const sourceUrl = createSourceUrl(); - const response = await fetchFileContent( - sourceUrl, - { - signal, - polling: type === 'transcript' && !src, - interval: 2000, - maxAttempts: 10, - responseStatusAsPending: 202, - onSuccess: () => updateTextTrackStatusToSuccess(track), - onError: (error) => { - updateTextTrackStatusToError(track, error); - console.warn(`[${track.label}] Text track could not be loaded`); - }, - - } - ); - - if (response) { - const parser = createParser(); - const data = await parser(response); - removeAllTextTrackCues(track); - addTextTrackCues(track, data); - updateTextTrackStatusToApplied(track); - } - }); - }; - - - const addTextTracks = (textTracks) => { - textTracks.forEach(textTrackConfig => { - if (textTrackConfig.src && textTrackConfig.src.endsWith('.vtt')) { - addTextTrack('vtt', textTrackConfig); - } else if (textTrackConfig.src && textTrackConfig.src.endsWith('.srt')) { - addTextTrack('srt', textTrackConfig); - } else if (!textTrackConfig.src || textTrackConfig.src.endsWith('.transcript')) { - addTextTrack('transcript', textTrackConfig); - } - }); - - const defaultTextTrack = Array.from(player.remoteTextTracks()).find((textTrack) => textTrack.default); - if (defaultTextTrack) { - onChangeActiveTrack(defaultTextTrack); - } - }; - - const onChangeActiveTrack = (textTrack) => { - const prevActiveTrack = activeTrack; - activeTrack = textTrack; - - const prevTextTrackData = textTracksData.get(prevActiveTrack); - if (prevTextTrackData) { - prevTextTrackData.abortLoading(); - } - - const selectedTextTrackData = textTracksData.get(activeTrack); - if (selectedTextTrackData) { - selectedTextTrackData.load(); - } - }; - - player.on('texttrackchange', () => { - const textTracks = player.textTracks(); - let newActiveTrack = null; - - for (let i = 0; i < textTracks.length; i++) { - const track = textTracks[i]; - - if (track.mode === 'showing') { - newActiveTrack = track; - break; - } - } - - if (activeTrack !== newActiveTrack) { - onChangeActiveTrack(newActiveTrack); - } - }); - - return { - removeAllTextTracks, - addTextTracks: (...args) => { - player.one('loadedmetadata', () => { - addTextTracks(...args); - }); - }, - }; + try { + const { default: textTracksManager } = await import( + /* webpackChunkName: "text-tracks" */ './text-tracks-manager' + ); + return textTracksManager.call(player); + } catch (error) { + console.error('Failed to load text tracks manager plugin:', error); + } } - -export default textTracksManager; diff --git a/src/plugins/text-tracks-manager/text-tracks-manager.js b/src/plugins/text-tracks-manager/text-tracks-manager.js new file mode 100644 index 000000000..b2b806039 --- /dev/null +++ b/src/plugins/text-tracks-manager/text-tracks-manager.js @@ -0,0 +1,195 @@ +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) => + `${urlPrefix}/_applet_/video_service/transcription/${deliveryType}/${languageCode ? `${languageCode}/` : ''}${utf8ToBase64(publicId)}.transcript`; + +function textTracksManager() { + const player = this; + const textTracksData = new WeakMap(); + let activeTrack = null; + + const removeAllTextTracks = () => { + const currentTracks = player.remoteTextTracks(); + if (currentTracks) { + for (let i = currentTracks.tracks_.length - 1; i >= 0; i--) { + player.removeRemoteTextTrack(currentTracks.tracks_[i]); + } + } + }; + + const createTextTrackData = (textTrack, loadMethod) => { + const controller = new AbortController(); + textTracksData.set(textTrack, { + status: 'idle', + load: async () => { + const { status } = textTracksData.get(textTrack); + if (status === 'idle') { + await loadMethod(controller.signal); + refreshTextTrack(textTrack); + } + }, + abortLoading: () => { + const { status } = textTracksData.get(textTrack); + if (status === 'pending') { + controller.abort(); + } + }, + }); + }; + + const updateTextTrackData = (textTrack, dataToUpdate) => { + const existingData = textTracksData.get(textTrack); + textTracksData.set(textTrack, { + ...existingData, + ...dataToUpdate, + }); + }; + const updateTextTrackStatusToPending = (textTrack) => updateTextTrackData(textTrack, { status: 'pending' }); + const updateTextTrackStatusToSuccess = (textTrack) => updateTextTrackData(textTrack, { status: 'success' }); + const updateTextTrackStatusToError = (textTrack, error) => updateTextTrackData(textTrack, { status: 'error', error }); + const updateTextTrackStatusToApplied = (textTrack) => updateTextTrackData(textTrack, { status: 'applied' }); + + const addTextTrack = (type, config) => { + const { + kind = type === 'transcript' ? 'captions' : 'subtitles', + label = type === 'transcript' ? 'Captions' : 'Subtitles', + default: isDefault, + srclang, + src, + } = config; + + if (type === 'transcript') { + player.textTrackDisplay.el().classList.add('cld-paced-text-tracks'); + } + + const { track } = player.addRemoteTextTrack({ + kind, + label, + srclang, + default: isDefault, + mode: isDefault ? 'showing' : 'disabled', + }); + + const createParser = () => { + if (type === 'srt') return srtParser; + if (type === 'vtt') return vttParser; + return (text) => transcriptParser(text, { + maxWords: config.maxWords, + wordHighlight: config.wordHighlight, + timeOffset: config.timeOffset ?? 0, + }); + }; + + const createSourceUrl = () => { + if (src) return src; + if (type !== 'transcript') return undefined; + + const source = player.cloudinary.source(); + const publicId = source.publicId(); + const deliveryType = source.resourceConfig().type; + const urlPrefix = getCloudinaryUrlPrefix(player.cloudinary.cloudinaryConfig()); + const baseUrl = getTranscriptionFileUrl(urlPrefix, deliveryType, publicId); + const localizedUrl = srclang ? getTranscriptionFileUrl(urlPrefix, deliveryType, publicId, srclang) : null; + + return localizedUrl ? localizedUrl : baseUrl; + }; + + createTextTrackData(track, async (signal) => { + updateTextTrackStatusToPending(track); + + const sourceUrl = createSourceUrl(); + const response = await fetchFileContent( + sourceUrl, + { + signal, + polling: type === 'transcript' && !src, + interval: 2000, + maxAttempts: 10, + responseStatusAsPending: 202, + onSuccess: () => updateTextTrackStatusToSuccess(track), + onError: (error) => { + updateTextTrackStatusToError(track, error); + console.warn(`[${track.label}] Text track could not be loaded`); + }, + + } + ); + + if (response) { + const parser = createParser(); + const data = await parser(response); + removeAllTextTrackCues(track); + addTextTrackCues(track, data); + updateTextTrackStatusToApplied(track); + } + }); + }; + + + const addTextTracks = (textTracks) => { + textTracks.forEach(textTrackConfig => { + if (textTrackConfig.src && textTrackConfig.src.endsWith('.vtt')) { + addTextTrack('vtt', textTrackConfig); + } else if (textTrackConfig.src && textTrackConfig.src.endsWith('.srt')) { + addTextTrack('srt', textTrackConfig); + } else if (!textTrackConfig.src || textTrackConfig.src.endsWith('.transcript')) { + addTextTrack('transcript', textTrackConfig); + } + }); + + const defaultTextTrack = Array.from(player.remoteTextTracks()).find((textTrack) => textTrack.default); + if (defaultTextTrack) { + onChangeActiveTrack(defaultTextTrack); + } + }; + + const onChangeActiveTrack = (textTrack) => { + const prevActiveTrack = activeTrack; + activeTrack = textTrack; + + const prevTextTrackData = textTracksData.get(prevActiveTrack); + if (prevTextTrackData) { + prevTextTrackData.abortLoading(); + } + + const selectedTextTrackData = textTracksData.get(activeTrack); + if (selectedTextTrackData) { + selectedTextTrackData.load(); + } + }; + + player.on('texttrackchange', () => { + const textTracks = player.textTracks(); + let newActiveTrack = null; + + for (let i = 0; i < textTracks.length; i++) { + const track = textTracks[i]; + + if (track.mode === 'showing') { + newActiveTrack = track; + break; + } + } + + if (activeTrack !== newActiveTrack) { + onChangeActiveTrack(newActiveTrack); + } + }); + + return { + removeAllTextTracks, + addTextTracks: (...args) => { + player.one('loadedmetadata', () => { + addTextTracks(...args); + }); + }, + }; +} + +export default textTracksManager; + diff --git a/src/video-player.js b/src/video-player.js index bdf045c37..5185fcfb2 100644 --- a/src/video-player.js +++ b/src/video-player.js @@ -209,6 +209,10 @@ class VideoPlayer extends Utils.mixin(Eventable) { } setTextTracks(conf) { + if (!this.textTracksManager) { + return; + } + this.textTracksManager.removeAllTextTracks(); if (conf) { @@ -376,9 +380,11 @@ class VideoPlayer extends Utils.mixin(Eventable) { } _initTextTracks () { - this.textTracksManager = this.videojs.textTracksManager(); - this.videojs.on(PLAYER_EVENT.CLD_SOURCE_CHANGED, (e, { source }) => { - if (source?._textTracks) { + this.videojs.on(PLAYER_EVENT.CLD_SOURCE_CHANGED, async (e, { source }) => { + if (source?._textTracks && this.videojs.textTracksManager) { + if (!this.textTracksManager) { + this.textTracksManager = await this.videojs.textTracksManager(); + } this.setTextTracks(source._textTracks); } }); diff --git a/webpack/es6.config.js b/webpack/es6.config.js index dbe253244..e84e1c74b 100644 --- a/webpack/es6.config.js +++ b/webpack/es6.config.js @@ -11,7 +11,7 @@ module.exports = merge(webpackCommon, { mode: 'production', entry: { - 'cld-video-player': './index.es.js', // default + 'cld-video-player': './index.js', // default 'videoPlayer': './index.videoPlayer.js', 'player': './index.player.js', 'all': './index.all.js'