Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/pages/cldvideoplayer/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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` |

Expand Down
4 changes: 3 additions & 1 deletion next-cloudinary/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.11"
},
"devDependencies": {
"@babel/core": "^7.19.6",
Expand All @@ -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"
Expand Down
106 changes: 106 additions & 0 deletions next-cloudinary/plugins/copy-assets.ts
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont love this solution but couldn't figure out a way to resolve the node_modules reliably with being in a yarn workspace


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;
}
164 changes: 93 additions & 71 deletions next-cloudinary/src/components/CldVideoPlayer/CldVideoPlayer.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;

Expand All @@ -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<any>();
const defaultVideoRef = useRef() as MutableRefObject<HTMLVideoElement | null>;
const videoRef = props.videoRef || defaultVideoRef;
const defaultPlayerRef = useRef()as MutableRefObject<CloudinaryVideoPlayer | null>;
const playerRef = props.playerRef || defaultPlayerRef;

const playerId = id || `player-${publicId.replace('/', '-')}-${idRef.current}`;
let playerClassName = 'cld-video-player cld-fluid';

if ( className ) {
Expand All @@ -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 {
Expand All @@ -158,9 +178,17 @@ const CldVideoPlayer = (props: CldVideoPlayerProps) => {

return (
<>
<Head>
<link href={`https://unpkg.com/cloudinary-video-player@${version}/dist/cld-video-player.min.css`} rel="stylesheet" />
</Head>
{/**
* 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 && (
<Head>
<link href={`https://unpkg.com/cloudinary-video-player@${version}/dist/cld-video-player.min.css`} rel="stylesheet" />
</Head>
)}
<div style={{ width: '100%', aspectRatio: `${props.width} / ${props.height}`}}>
<video
ref={videoRef}
Expand All @@ -169,12 +197,6 @@ const CldVideoPlayer = (props: CldVideoPlayerProps) => {
width={width}
height={height}
/>
<Script
id={`cloudinary-videoplayer-${Math.floor(Math.random() * 100)}`}
src={`https://unpkg.com/cloudinary-video-player@${version}/dist/cld-video-player.min.js`}
onLoad={handleOnLoad}
onError={(e) => console.error(`Failed to load Cloudinary Video Player: ${e.message}`)}
/>
</div>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CloudinaryVideoPlayer, CloudinaryVideoPlayerOptions, CloudinaryVideoPla

export type CldVideoPlayerProps = Pick<CloudinaryVideoPlayerOptions, "colors" | "controls" | "fontFace" | "loop" | "muted" | "transformation"> & {
autoPlay?: string;
excludeExternalStylesheet?: boolean;
className?: string;
height: string | number;
id?: string;
Expand Down
3 changes: 2 additions & 1 deletion next-cloudinary/src/types/player.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export interface CloudinaryVideoPlayer {
on: Function
dispose: Function;
on: Function;
}

export interface CloudinaryVideoPlayerOptions {
Expand Down
Loading