Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic Imports for Video Player #181

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
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.
Jump to
Jump to file
Failed to load files.
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