diff --git a/docs/pages/cldvideoplayer/configuration.mdx b/docs/pages/cldvideoplayer/configuration.mdx index 6c305253..f3502287 100644 --- a/docs/pages/cldvideoplayer/configuration.mdx +++ b/docs/pages/cldvideoplayer/configuration.mdx @@ -39,7 +39,7 @@ import OgImage from '../../components/OgImage'; | showLogo | boolea | `true` | Show the Cloudinary logo on Player | `false` | | src | string | - | **Required**: Video public ID | `"videos/my-video"` | | transformation | object/array | - | Transformations to apply to the video | `{ width: 200, height: 200, crop: 'fill' }` | -| version | string | `"1.9.4"` | Cloudinary Video Player version | `"1.9.4"` | +| version | string | `"1.9.4"` | **Removed** | `"1.9.4"` | | videoRef | Ref | - | React ref to access video element | See Refs Below | | width | string/number | - | **Required**: Player width | `1920` | diff --git a/next-cloudinary/package.json b/next-cloudinary/package.json index 9320df62..68de181b 100644 --- a/next-cloudinary/package.json +++ b/next-cloudinary/package.json @@ -15,7 +15,8 @@ }, "dependencies": { "@cloudinary-util/url-loader": "^3.10.0", - "@cloudinary-util/util": "^2.2.1" + "@cloudinary-util/util": "^2.2.1", + "cloudinary-video-player": "^1.9.14" }, "devDependencies": { "@babel/core": "^7.19.6", @@ -27,6 +28,7 @@ "dotenv": "^16.0.3", "jest": "^29.2.2", "jest-environment-jsdom": "^29.2.2", + "mkdirp": "^3.0.1", "ts-jest": "^29.0.3", "tsup": "^6.6.3", "typescript": "^4.9.4" diff --git a/next-cloudinary/plugins/copy-assets.ts b/next-cloudinary/plugins/copy-assets.ts new file mode 100644 index 00000000..5cb6ae22 --- /dev/null +++ b/next-cloudinary/plugins/copy-assets.ts @@ -0,0 +1,106 @@ +import { Plugin } from 'esbuild' +import path from 'path'; +import { readdir, copyFile, readFile, lstat } from 'fs/promises'; +import { mkdirp } from 'mkdirp'; + +let hasWrittenAssets = false; + +const assets = [ + 'cloudinary-video-player/dist/cld-video-player.min.css', + 'cloudinary-video-player/dist/fonts' +]; + +export const plugin: Plugin = { + name: 'copy-assets', + setup: async () => { + const rootPath = path.join(__dirname, '../'); + const distPath = path.join(rootPath, 'dist'); + + if ( hasWrittenAssets ) return; + + await mkdirp(distPath); + + for ( const asset of assets ) { + const assetPath = await resolveAssetPath(asset); + + if ( typeof assetPath === 'string' ) { + const info = await lstat(assetPath); + const isDirectory = info.isDirectory(); + let files; + + if ( isDirectory ) { + const dirFiles = await readdir(assetPath); + const dirName = path.basename(assetPath); + + files = dirFiles.map(dirFile => { + return { + path: path.join(assetPath, dirFile), + name: path.join(dirName, dirFile) + } + }); + + await mkdirp(path.join(distPath, dirName)); + } else { + files = [{ + path: assetPath, + name: path.basename(assetPath) + }]; + } + + for ( const file of files ) { + await copyFile(file.path, path.join(distPath, file.name)); + } + } + } + + hasWrittenAssets = true; + } +} + +async function resolveAssetPath(assetPath: string) { + let filePath; + let dirPath; + + // Check if it's a file in the active project root node_modules + + try { + filePath = path.join('node_modules', assetPath); + await readFile(filePath); + } catch(e) { + filePath = undefined; + } + + // Check if it's a file in the workspace node_modules + + try { + filePath = path.join('../node_modules', assetPath) + await readFile(filePath); + } catch(e) { + filePath = undefined; + } + + // If we've determined its a file, return early + + if ( filePath ) return filePath; + + // If it's not a file, maybe its a directory + // First check in active project root + + try { + dirPath = path.join('node_modules', assetPath) + await readdir(dirPath); + } catch(e) { + dirPath = undefined; + } + + // Then again in the workspace root + + try { + dirPath = path.join('../node_modules', assetPath) + await readdir(dirPath); + } catch(e) { + dirPath = undefined; + } + + return dirPath; +} \ No newline at end of file diff --git a/next-cloudinary/src/components/CldVideoPlayer/CldVideoPlayer.tsx b/next-cloudinary/src/components/CldVideoPlayer/CldVideoPlayer.tsx index e26883e7..c15f7eeb 100644 --- a/next-cloudinary/src/components/CldVideoPlayer/CldVideoPlayer.tsx +++ b/next-cloudinary/src/components/CldVideoPlayer/CldVideoPlayer.tsx @@ -1,23 +1,21 @@ -import React, { useRef, MutableRefObject } from 'react'; -import Script from 'next/script'; -import Head from 'next/head'; +import React, { useRef, useEffect, useState, MutableRefObject, } from 'react'; import { parseUrl } from '@cloudinary-util/util'; +import Head from 'next/head'; +import pkg from '../../../package.json' import { CldVideoPlayerProps } from './CldVideoPlayer.types'; import { CloudinaryVideoPlayer, CloudinaryVideoPlayerOptions, CloudinaryVideoPlayerOptionsLogo } from '../../types/player'; -const CldVideoPlayer = (props: CldVideoPlayerProps) => { - // If no ID is passed in - we want to be able to ensure that we are using - // unique IDs for each player. We can do this by generating a random number - // and using that as the ID. We use a ref here so that we can ensure that - // the ID is only generated once. - const idRef = useRef(Math.ceil(Math.random() * 100000)); +// @ts-ignore +const version: string = pkg.dependencies['cloudinary-video-player']; +const CldVideoPlayer = (props: CldVideoPlayerProps) => { const { autoPlay = 'never', className, colors, controls = true, + excludeExternalStylesheet = false, fontFace, height, id, @@ -32,11 +30,14 @@ const CldVideoPlayer = (props: CldVideoPlayerProps) => { onEnded, src, transformation, - version = '1.9.4', quality = 'auto', width, } = props as CldVideoPlayerProps; + if ( typeof props.version !== 'undefined' ) { + console.warn('The version prop no longer controls the video player version and thus is no longer available for use.'); + } + const playerTransformations = Array.isArray(transformation) ? transformation : [transformation]; let publicId = src; @@ -61,13 +62,11 @@ const CldVideoPlayer = (props: CldVideoPlayerProps) => { // Setup the refs and allow for the caller to pass through their // own ref instance - const cloudinaryRef = useRef(); const defaultVideoRef = useRef() as MutableRefObject; const videoRef = props.videoRef || defaultVideoRef; const defaultPlayerRef = useRef()as MutableRefObject; const playerRef = props.playerRef || defaultPlayerRef; - const playerId = id || `player-${publicId.replace('/', '-')}-${idRef.current}`; let playerClassName = 'cld-video-player cld-fluid'; if ( className ) { @@ -83,71 +82,92 @@ const CldVideoPlayer = (props: CldVideoPlayerProps) => { ended: onEnded }; - /** - * handleEvent - * @description Event handler for all player events - */ - - function handleEvent(event: { type: 'string' }) { - const activeEvent = events[event.type]; + let logoOptions: CloudinaryVideoPlayerOptionsLogo = {}; - if ( typeof activeEvent === 'function' ) { - activeEvent(getPlayerRefs()); + if ( typeof logo === 'boolean' ) { + logoOptions.showLogo = logo; + } else if ( typeof logo === 'object' ) { + logoOptions = { + ...logoOptions, + showLogo: true, + logoImageUrl: logo.imageUrl, + logoOnclickUrl: logo.onClickUrl } } - /** - * handleOnLoad - * @description Stores the Cloudinary window instance to a ref when the widget script loads - */ + let playerOptions: CloudinaryVideoPlayerOptions = { + autoplayMode: autoPlay, + cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME, + controls, + fontFace: fontFace || '', + loop, + muted, + publicId: src, + secure: true, + transformation: playerTransformations, + ...logoOptions + }; + + if ( typeof colors === 'object' ) { + playerOptions.colors = colors; + } - function handleOnLoad() { - if ( 'cloudinary' in window ) { - cloudinaryRef.current = window.cloudinary; - - let logoOptions: CloudinaryVideoPlayerOptionsLogo = {}; - - if ( typeof logo === 'boolean' ) { - logoOptions.showLogo = logo; - } else if ( typeof logo === 'object' ) { - logoOptions = { - ...logoOptions, - showLogo: true, - logoImageUrl: logo.imageUrl, - logoOnclickUrl: logo.onClickUrl - } + // If no ID is passed in - we want to be able to ensure that we are using + // unique IDs for each player to avoid conflicts. We can do this by generating + // a random number and using that as the ID. We use a ref here so that we can + // ensure that the ID is only generated once. + + const idRef = useRef(Math.ceil(Math.random() * 100000)); + const [playerId, setPlayerId] = useState(id); + + useEffect(() => { + if ( typeof id !== 'undefined' ) return; + setPlayerId(`player-${src.replace('/', '-')}-${idRef.current}`); + }, []) + + // Initialize the player + + useEffect(() => { + if ( !playerId || playerRef.current ) return; + + (async function run() { + // @ts-ignore + const { videoPlayer } = await import('cloudinary-video-player'); + + if ( !playerRef.current ) { + playerRef.current = videoPlayer(videoRef.current, playerOptions); + + Object.keys(events).forEach((key) => { + if ( typeof events[key] === 'function' ) { + playerRef.current?.on(key, handleEvent); + } + }); } + })(); - let playerOptions: CloudinaryVideoPlayerOptions = { - autoplayMode: autoPlay, - cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME, - controls, - fontFace: fontFace || '', - loop, - muted, - publicId, - secure: true, - transformation: playerTransformations, - ...logoOptions - }; - - if ( typeof colors === 'object' ) { - playerOptions.colors = colors; + return () => { + if ( playerRef.current ) { + playerRef.current.dispose(); } + } + }, [playerId]) - playerRef.current = cloudinaryRef.current.videoPlayer(videoRef.current, playerOptions); + /** + * handleEvent + * @description Event handler for all player events + */ + + function handleEvent(event: { type: 'string' }) { + const activeEvent = events[event.type]; - Object.keys(events).forEach((key) => { - if ( typeof events[key] === 'function' ) { - playerRef.current?.on(key, handleEvent); - } - }); + if ( typeof activeEvent === 'function' ) { + activeEvent(getPlayerRefs()); } } /** *getPlayerRefs - */ + */ function getPlayerRefs() { return { @@ -158,9 +178,17 @@ const CldVideoPlayer = (props: CldVideoPlayerProps) => { return ( <> - - - + {/** + * There's not a reliable way (?) to include the stylesheet without impacting the rest + * of the components and not requirin the developer to include it themselves, so add + * it to head by default. If using Next.js 13 App, where Head is not supported, they + * would likely need to still add it themselves + */} + {!excludeExternalStylesheet && ( + + + + )}