diff --git a/next-cloudinary/src/components/CldVideoPlayer/CldVideoPlayer.tsx b/next-cloudinary/src/components/CldVideoPlayer/CldVideoPlayer.tsx index d0701dfd..e4c3fd6c 100644 --- a/next-cloudinary/src/components/CldVideoPlayer/CldVideoPlayer.tsx +++ b/next-cloudinary/src/components/CldVideoPlayer/CldVideoPlayer.tsx @@ -6,6 +6,7 @@ import { CloudinaryVideoPlayer } from '@cloudinary-util/types'; import { getVideoPlayerOptions } from '@cloudinary-util/url-loader'; import { CldVideoPlayerProps } from './CldVideoPlayer.types'; +import { getPlayerTransformation } from './getPlayerTransformation'; import { getCloudinaryConfig } from "../../lib/cloudinary"; @@ -36,6 +37,8 @@ const CldVideoPlayer = (props: CldVideoPlayerProps) => { const cloudinaryConfig = getCloudinaryConfig(config); const playerOptions = getVideoPlayerOptions(props, cloudinaryConfig); + playerOptions.transformation = getPlayerTransformation(props, playerOptions.transformation); + const { publicId } = playerOptions; if ( typeof publicId === 'undefined' ) { throw new Error('Video Player requires a Public ID or Cloudinary URL - please specify a src prop'); diff --git a/next-cloudinary/src/components/CldVideoPlayer/getPlayerTransformation.ts b/next-cloudinary/src/components/CldVideoPlayer/getPlayerTransformation.ts new file mode 100644 index 00000000..61e74ad2 --- /dev/null +++ b/next-cloudinary/src/components/CldVideoPlayer/getPlayerTransformation.ts @@ -0,0 +1,23 @@ +import type { CloudinaryVideoPlayerOptions } from '@cloudinary-util/types'; + +import type { CldVideoPlayerProps } from './CldVideoPlayer.types'; + +/** + * getPlayerTransformation + * @description getVideoPlayerOptions passes the transformation as an array ([{ quality }, ...transformation]). + * The player turns each array item into its own URL step, which leaves an `aspectRatio` alone in + * a step and breaks poster URLs (cloudinary/cloudinary-video-player#1087). When an aspectRatio is + * set and the caller didn't pass an array themselves, combine everything into a single + * transformation object. Players without an aspectRatio keep their existing URLs. + */ + +export function getPlayerTransformation( + props: Pick, + transformation: CloudinaryVideoPlayerOptions['transformation'] +): CloudinaryVideoPlayerOptions['transformation'] { + if ( props.aspectRatio && !Array.isArray(props.transformation) && Array.isArray(transformation) ) { + return Object.assign({}, ...transformation.filter(Boolean)); + } + + return transformation; +} diff --git a/next-cloudinary/tests/components/getPlayerTransformation.spec.js b/next-cloudinary/tests/components/getPlayerTransformation.spec.js new file mode 100644 index 00000000..fbc71e00 --- /dev/null +++ b/next-cloudinary/tests/components/getPlayerTransformation.spec.js @@ -0,0 +1,61 @@ +import { describe, it, expect } from 'vitest'; +import { getVideoPlayerOptions } from '@cloudinary-util/url-loader'; + +import { getPlayerTransformation } from '../../src/components/CldVideoPlayer/getPlayerTransformation'; + +const config = { cloud: { cloudName: 'test' } }; + +function getTransformation(props) { + const playerOptions = getVideoPlayerOptions(props, config); + return getPlayerTransformation(props, playerOptions.transformation); +} + +describe('CldVideoPlayer', () => { + describe('getPlayerTransformation', () => { + it('should keep the transformation array when there is no aspectRatio', () => { + const props = { + src: 'turtle', + transformation: { effect: 'sepia' } + }; + + const { transformation } = getVideoPlayerOptions(props, config); + + expect(getTransformation(props)).toEqual(transformation); + }); + + it('should combine the transformation into one object when there is an aspectRatio', () => { + expect(getTransformation({ + src: 'turtle', + aspectRatio: '16:9', + transformation: { effect: 'sepia' } + })).toEqual({ quality: 'auto', effect: 'sepia' }); + }); + + it('should combine into one object when there is an aspectRatio and no transformation', () => { + expect(getTransformation({ + src: 'turtle', + aspectRatio: '16:9' + })).toEqual({ quality: 'auto' }); + }); + + // The crop mode is passed to the player separately, not as part of the transformation + it('should combine into one object with an aspectRatio, crop mode and quality', () => { + expect(getTransformation({ + src: 'turtle', + aspectRatio: '16:9', + cropMode: 'fill', + quality: 50 + })).toEqual({ quality: 50 }); + }); + + it('should keep the transformation array when the caller passes an array', () => { + const props = { + src: 'turtle', + aspectRatio: '16:9', + transformation: [{ effect: 'sepia' }, { width: 300 }] + }; + + expect(getTransformation(props)).toEqual([{ quality: 'auto' }, { effect: 'sepia' }, { width: 300 }]); + }); + }); +});