diff --git a/docs/aspect-ratio-crop.html b/docs/aspect-ratio-crop.html
new file mode 100644
index 000000000..485bb08ad
--- /dev/null
+++ b/docs/aspect-ratio-crop.html
@@ -0,0 +1,118 @@
+
+
+
+
+ Cloudinary Video Player
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Cloudinary Video Player
+
Aspect ratio & crop mode
+
+
fill - crops to fit aspect ratio
+
+
+
pad - letterboxes to fit
+
+
+
smart with 9:16 (portrait)
+
+
+
+ Full documentation
+
+
+
Example Code:
+
+
+// Fill - crops to fit aspect ratio (no distortion)
+player.source({
+ publicId: 'sea_turtle',
+ aspectRatio: '1:1',
+ cropMode: 'fill'
+});
+
+// Pad - letterboxes to fit (no distortion)
+player.source({
+ publicId: 'sea_turtle',
+ aspectRatio: '4:3',
+ cropMode: 'pad',
+ cropPadColor: 'blue'
+});
+
+// Portrait (9:16) - smart crop
+player.source({
+ publicId: 'sea_turtle',
+ aspectRatio: '9:16',
+ cropMode: 'smart'
+});
+
+
+
+
+
diff --git a/docs/audio.html b/docs/audio.html
index f1f72de10..98238bcd0 100644
--- a/docs/audio.html
+++ b/docs/audio.html
@@ -71,7 +71,7 @@ Audio Player - with transformations
id="player-t"
playsinline
controls
- class="cld-video-player"
+ class="cld-video-player cld-video-player-skin-light"
width="500"
>
diff --git a/docs/es-modules/aspect-ratio-crop.html b/docs/es-modules/aspect-ratio-crop.html
new file mode 100644
index 000000000..b6cb5c34d
--- /dev/null
+++ b/docs/es-modules/aspect-ratio-crop.html
@@ -0,0 +1,121 @@
+
+
+
+
+ Cloudinary Video Player
+
+
+
+
+
+
+
+
Cloudinary Video Player
+
Aspect ratio & crop mode
+
+
fill - crops to fit aspect ratio
+
+
+
pad - letterboxes to fit
+
+
+
smart with 9:16 (portrait)
+
+
+
+ Full documentation
+
+
+
Example Code:
+
+// Fill - crops to fit aspect ratio
+player.source({
+ publicId: 'sea_turtle',
+ aspectRatio: '1:1',
+ cropMode: 'fill'
+});
+
+// Pad - letterboxes to fit
+player.source({
+ publicId: 'sea_turtle',
+ aspectRatio: '4:3',
+ cropMode: 'pad',
+ cropPadColor: 'blue'
+});
+
+// Portrait (9:16) - smart crop
+player.source({
+ publicId: 'sea_turtle',
+ aspectRatio: '9:16',
+ cropMode: 'smart'
+});
+
+
+
+
+
+
+
+
diff --git a/docs/es-modules/index.html b/docs/es-modules/index.html
index a027813b1..c7d438175 100644
--- a/docs/es-modules/index.html
+++ b/docs/es-modules/index.html
@@ -45,6 +45,7 @@ Code examples:
- Adaptive Streaming
- AI Highlights Graph
+ - Aspect Ratio & Crop
- Analytics
- API and Events
- Audio Player
diff --git a/docs/index.html b/docs/index.html
index a0a4a2251..aaf8ecbc1 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -49,6 +49,7 @@ Some code examples:
- Adaptive Streaming
- AI Highlights Graph
+ - Aspect Ratio & Crop
- Analytics
- API and Events
- Audio Player
diff --git a/package-lock.json b/package-lock.json
index 85278fabf..3de38bdba 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5124,14 +5124,11 @@
}
},
"node_modules/@simple-libs/stream-utils": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.1.0.tgz",
- "integrity": "sha512-6rsHTjodIn/t90lv5snQjRPVtOosM7Vp0AKdrObymq45ojlgVwnpAqdc+0OBBrpEiy31zZ6/TKeIVqV1HwvnuQ==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz",
+ "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "@types/node": "^22.0.0"
- },
"engines": {
"node": ">=18"
},
@@ -10821,6 +10818,25 @@
}
}
},
+ "node_modules/git-semver-tags/node_modules/conventional-commits-parser": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.3.0.tgz",
+ "integrity": "sha512-RfOq/Cqy9xV9bOA8N+ZH6DlrDR+5S3Mi0B5kACEjESpE+AviIpAptx9a9cFpWCCvgRtWT+0BbUw+e1BZfts9jg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@simple-libs/stream-utils": "^1.2.0",
+ "meow": "^13.0.0"
+ },
+ "bin": {
+ "conventional-commits-parser": "dist/cli/index.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
diff --git a/src/config/configSchema.json b/src/config/configSchema.json
index a4e6b4bd9..345d43b6c 100644
--- a/src/config/configSchema.json
+++ b/src/config/configSchema.json
@@ -290,6 +290,19 @@
"default": 2.0,
"description": "Maximum device pixel ratio cap for breakpoint rendition selection (1.0, 1.5, or 2.0)"
},
+ "aspectRatio": {
+ "type": "string",
+ "description": "Aspect ratio for video resize (e.g. 16:9, 9:16, 1:1). Merged into transformation."
+ },
+ "cropMode": {
+ "type": "string",
+ "enum": ["fill", "pad", "smart"],
+ "description": "Resize mode: fill, pad, or smart. Merged into transformation."
+ },
+ "cropPadColor": {
+ "type": "string",
+ "description": "Background color for pad mode, e.g. #000000. Merged into transformation.background."
+ },
"resourceType": {
"type": "string",
"default": "video"
diff --git a/src/plugins/cloudinary/index.js b/src/plugins/cloudinary/index.js
index 3a6755ae3..faa02ef03 100644
--- a/src/plugins/cloudinary/index.js
+++ b/src/plugins/cloudinary/index.js
@@ -10,6 +10,7 @@ import {
setupCloudinaryMiddleware,
isRawUrl
} from './common';
+import { CROP_MODE } from 'video-player.const';
import VideoSource from './models/video-source/video-source';
import EventHandlerRegistry from './event-handler-registry';
import AudioSource from './models/audio-source/audio-source';
@@ -30,6 +31,29 @@ const DEFAULT_PARAMS = {
export const CONSTRUCTOR_PARAMS = ['cloudinaryConfig', 'transformation',
'sourceTypes', 'sourceTransformation', 'posterOptions', 'autoShowRecommendations'];
+const normalizeAspectCrop = (options) => {
+ const { aspectRatio, cropMode, cropPadColor, transformation, ...rest } = options;
+ if (!aspectRatio && !cropMode) return options;
+
+ const tx = {};
+ if (aspectRatio) tx.aspect_ratio = aspectRatio;
+ if (cropMode) {
+ if (cropMode === CROP_MODE.SMART) {
+ tx.crop = CROP_MODE.FILL;
+ tx.gravity = 'auto';
+ } else {
+ tx.crop = cropMode;
+ if (cropMode === CROP_MODE.PAD && cropPadColor) tx.background = cropPadColor;
+ }
+ }
+ return {
+ ...rest,
+ transformation: Array.isArray(transformation)
+ ? [tx, ...transformation]
+ : mergeTransformations(transformation || {}, tx)
+ };
+};
+
class CloudinaryContext {
constructor(player, options = {}) {
setupCloudinaryMiddleware();
@@ -124,6 +148,7 @@ class CloudinaryContext {
this.buildSource = (publicId, options = {}) => {
let builtSrc = null;
({ publicId, options } = normalizeOptions(publicId, options));
+ options = normalizeAspectCrop(options);
options.cloudinaryConfig = extendCloudinaryConfig(this.cloudinaryConfig(), options.cloudinaryConfig || {});
options.transformation = mergeTransformations(this.transformation(), options.transformation || {});
@@ -161,7 +186,7 @@ class CloudinaryContext {
const width = RENDITIONS.find(rendition => rendition >= requiredWidth) || RENDITIONS[RENDITIONS.length - 1];
options.breakpointTransformation = {
width,
- crop: 'limit'
+ ...(!isKeyInTransformation(options.transformation, 'crop') && { crop: 'limit' })
};
}
diff --git a/src/plugins/cloudinary/models/video-source/video-source.js b/src/plugins/cloudinary/models/video-source/video-source.js
index 2ce16ce9b..6756f9842 100644
--- a/src/plugins/cloudinary/models/video-source/video-source.js
+++ b/src/plugins/cloudinary/models/video-source/video-source.js
@@ -181,7 +181,9 @@ class VideoSource extends BaseSource {
// Merge breakpoint transformation if available
if (this._breakpointTransformation) {
- opts.transformation = mergeTransformations(opts.transformation, this._breakpointTransformation);
+ opts.transformation = Array.isArray(opts.transformation)
+ ? [this._breakpointTransformation, ...opts.transformation]
+ : mergeTransformations(opts.transformation || {}, this._breakpointTransformation);
}
Object.assign(opts, { format });
@@ -213,7 +215,7 @@ class VideoSource extends BaseSource {
// dr (dynamic range) is not yet exposed by @cloudinary/url-gen, so we use raw_transformation
if (this.hdr() === true && window.matchMedia && window.matchMedia('(dynamic-range: high)').matches) {
opts.transformation = mergeTransformations(opts.transformation, {
- fetch_format:'mp4',
+ fetch_format: 'mp4',
video_codec: 'h265',
raw_transformation: 'dr_high'
});
diff --git a/src/utils/get-analytics-player-options.js b/src/utils/get-analytics-player-options.js
index 8fa781c46..0a9e12061 100644
--- a/src/utils/get-analytics-player-options.js
+++ b/src/utils/get-analytics-player-options.js
@@ -47,6 +47,9 @@ const getSourceOptions = (sourceOptions = {}) => ({
videoSources: hasConfig(sourceOptions.videoSources),
breakpoints: sourceOptions.breakpoints,
maxDpr: sourceOptions.maxDpr,
+ aspectRatio: sourceOptions.aspectRatio,
+ cropMode: sourceOptions.cropMode,
+ cropPadColor: hasConfig(sourceOptions.cropPadColor) ? sourceOptions.cropPadColor : undefined
});
const getTextTracksOptions = (textTracks = {}) => {
diff --git a/src/validators/validators.js b/src/validators/validators.js
index 4260d8f3d..52102546f 100644
--- a/src/validators/validators.js
+++ b/src/validators/validators.js
@@ -1,4 +1,4 @@
-import { ADS_IN_PLAYLIST, AUTO_PLAY_MODE, FLOATING_TO } from '../video-player.const';
+import { ADS_IN_PLAYLIST, AUTO_PLAY_MODE, CROP_MODE, FLOATING_TO } from '../video-player.const';
import {
INTERACTION_AREAS_TEMPLATE,
INTERACTION_AREAS_THEME
@@ -85,6 +85,9 @@ export const playerValidators = {
export const sourceValidators = {
raw_transformation: validator.isString,
+ aspectRatio: validator.isString,
+ cropMode: validator.isString(CROP_MODE),
+ cropPadColor: validator.isString,
hdr: validator.isBoolean,
resourceType: validator.isString,
shoppable: validator.isPlainObject,
diff --git a/src/video-player.const.js b/src/video-player.const.js
index bdd036d97..e1dcaeac9 100644
--- a/src/video-player.const.js
+++ b/src/video-player.const.js
@@ -2,9 +2,12 @@
export const SOURCE_PARAMS = [
'adaptiveStreaming',
'allowUsageReport',
+ 'aspectRatio',
'autoShowRecommendations',
'breakpoints',
'chapters',
+ 'cropMode',
+ 'cropPadColor',
'cloudinaryConfig',
'description',
'download',
@@ -101,3 +104,9 @@ export const PRELOAD = {
METADATA: 'metadata',
NONE: 'none'
};
+
+export const CROP_MODE = {
+ FILL: 'fill',
+ PAD: 'pad',
+ SMART: 'smart'
+};
diff --git a/src/video-player.js b/src/video-player.js
index 013ea2bd1..f02996086 100644
--- a/src/video-player.js
+++ b/src/video-player.js
@@ -264,11 +264,11 @@ class VideoPlayer {
delete transformation.video_codec;
}
- transformation.flags = transformation.flags || [];
- transformation.flags.push('sprite');
+ // fl_sprite must be in a separate URL component when transformation has params
+ const spriteTx = [...(Array.isArray(transformation) ? transformation : [transformation]), { flags: ['sprite'] }];
const vttUrl = source.config()
- .url(`${publicId}.vtt`, { transformation })
+ .url(`${publicId}.vtt`, { transformation: spriteTx })
.replace(/\.json$/, ''); // Handle playlist by tag
const vttSrc = appendQueryParams(vttUrl, source.queryParams());
diff --git a/test/e2e/specs/ESM/esmAspectRatioCropPage.spec.ts b/test/e2e/specs/ESM/esmAspectRatioCropPage.spec.ts
new file mode 100644
index 000000000..1e060aa40
--- /dev/null
+++ b/test/e2e/specs/ESM/esmAspectRatioCropPage.spec.ts
@@ -0,0 +1,16 @@
+import { vpTest } from '../../fixtures/vpTest';
+import { ExampleLinkName } from '../../testData/ExampleLinkNames';
+import { getEsmLinkByName } from '../../testData/esmPageLinksData';
+import { test } from '@playwright/test';
+import { waitForPageToLoadWithTimeout } from '../../src/helpers/waitForPageToLoadWithTimeout';
+import { ESM_URL } from '../../testData/esmUrl';
+import { testAspectRatioCropPageVideoIsPlaying } from '../commonSpecs/aspectRatioCropPageVideoPlaying';
+
+const link = getEsmLinkByName(ExampleLinkName.AspectRatioCrop);
+
+vpTest('Test if video on ESM aspect ratio & crop page is playing as expected', async ({ page, pomPages }) => {
+ await test.step('Navigate to ESM', async () => {
+ await page.goto(ESM_URL);
+ });
+ await testAspectRatioCropPageVideoIsPlaying(page, pomPages, link);
+});
diff --git a/test/e2e/specs/ESM/linksConsoleErrorsEsmPage.spec.ts b/test/e2e/specs/ESM/linksConsoleErrorsEsmPage.spec.ts
index ce8aca05b..dc83b2758 100644
--- a/test/e2e/specs/ESM/linksConsoleErrorsEsmPage.spec.ts
+++ b/test/e2e/specs/ESM/linksConsoleErrorsEsmPage.spec.ts
@@ -25,7 +25,7 @@ for (const link of ESM_LINKS) {
*/
vpTest('ESM page Link count test', async ({ page }) => {
await page.goto(ESM_URL);
- const expectedNumberOfLinks = 37;
+ const expectedNumberOfLinks = 38;
const numberOfLinks = await page.getByRole('link').count();
expect(numberOfLinks).toBe(expectedNumberOfLinks);
});
diff --git a/test/e2e/specs/NonESM/aspectRatioCropPage.spec.ts b/test/e2e/specs/NonESM/aspectRatioCropPage.spec.ts
new file mode 100644
index 000000000..861d39ae6
--- /dev/null
+++ b/test/e2e/specs/NonESM/aspectRatioCropPage.spec.ts
@@ -0,0 +1,10 @@
+import { vpTest } from '../../fixtures/vpTest';
+import { getLinkByName } from '../../testData/pageLinksData';
+import { ExampleLinkName } from '../../testData/ExampleLinkNames';
+import { testAspectRatioCropPageVideoIsPlaying } from '../commonSpecs/aspectRatioCropPageVideoPlaying';
+
+const link = getLinkByName(ExampleLinkName.AspectRatioCrop);
+
+vpTest('Test if video on aspect ratio & crop page is playing as expected', async ({ page, pomPages }) => {
+ await testAspectRatioCropPageVideoIsPlaying(page, pomPages, link);
+});
diff --git a/test/e2e/specs/NonESM/linksConsolErros.spec.ts b/test/e2e/specs/NonESM/linksConsolErros.spec.ts
index 0989cadfa..7df30862c 100644
--- a/test/e2e/specs/NonESM/linksConsolErros.spec.ts
+++ b/test/e2e/specs/NonESM/linksConsolErros.spec.ts
@@ -24,7 +24,7 @@ for (const link of LINKS) {
* Testing number of links in page.
*/
vpTest('Link count test', async ({ page }) => {
- const expectedNumberOfLinks = 41;
+ const expectedNumberOfLinks = 42;
const numberOfLinks = await page.getByRole('link').count();
expect(numberOfLinks).toBe(expectedNumberOfLinks);
});
diff --git a/test/e2e/specs/commonSpecs/aspectRatioCropPageVideoPlaying.ts b/test/e2e/specs/commonSpecs/aspectRatioCropPageVideoPlaying.ts
new file mode 100644
index 000000000..55e26bd80
--- /dev/null
+++ b/test/e2e/specs/commonSpecs/aspectRatioCropPageVideoPlaying.ts
@@ -0,0 +1,21 @@
+import { Page, test } from '@playwright/test';
+import { waitForPageToLoadWithTimeout } from '../../src/helpers/waitForPageToLoadWithTimeout';
+import PageManager from '../../src/pom/PageManager';
+import { ExampleLinkType } from '../../types/exampleLinkType';
+
+export async function testAspectRatioCropPageVideoIsPlaying(
+ page: Page,
+ pomPages: PageManager,
+ link: ExampleLinkType
+) {
+ await test.step('Navigate to aspect ratio & crop page by clicking on link', async () => {
+ await pomPages.mainPage.clickLinkByName(link.name);
+ await waitForPageToLoadWithTimeout(page, 5000);
+ });
+ await test.step('Click play button', async () => {
+ await pomPages.aspectRatioCropPage.videoComponent.clickPlay();
+ });
+ await test.step('Validating that video is playing', async () => {
+ await pomPages.aspectRatioCropPage.videoComponent.validateVideoIsPlaying(true);
+ });
+}
diff --git a/test/e2e/src/pom/PageManager.ts b/test/e2e/src/pom/PageManager.ts
index 0bc42eb7f..7f2bcea76 100644
--- a/test/e2e/src/pom/PageManager.ts
+++ b/test/e2e/src/pom/PageManager.ts
@@ -4,6 +4,7 @@ import { BasePage } from './BasePage';
import { MainPage } from './mainPage';
import { AnalyticsPage } from './analyticsPage';
import { ApiAndEventsPage } from './apiAndEventsPage';
+import { AspectRatioCropPage } from './aspectRatioCropPage';
import { AudioPlayerPage } from './audioPlayerPage';
import { AutoplayOnScrollPage } from './autoplayOnScrollPage';
import { ChaptersPage } from './chaptersPage';
@@ -84,6 +85,10 @@ export class PageManager {
return this.getPage(ApiAndEventsPage);
}
+ public get aspectRatioCropPage(): AspectRatioCropPage {
+ return this.getPage(AspectRatioCropPage);
+ }
+
/**
* Returns audio player page object
*/
diff --git a/test/e2e/src/pom/aspectRatioCropPage.ts b/test/e2e/src/pom/aspectRatioCropPage.ts
new file mode 100644
index 000000000..e9b4840cf
--- /dev/null
+++ b/test/e2e/src/pom/aspectRatioCropPage.ts
@@ -0,0 +1,17 @@
+import { Page } from '@playwright/test';
+import { VideoComponent } from '../../components/videoComponent';
+import { BasePage } from './BasePage';
+
+const ASPECT_RATIO_CROP_VIDEO_SELECTOR = '//*[@id="player-1_html5_api"]';
+
+/**
+ * Video player examples aspect ratio & crop page object
+ */
+export class AspectRatioCropPage extends BasePage {
+ public videoComponent: VideoComponent;
+
+ constructor(page: Page) {
+ super(page);
+ this.videoComponent = new VideoComponent(page, ASPECT_RATIO_CROP_VIDEO_SELECTOR);
+ }
+}
diff --git a/test/e2e/testData/ExampleLinkNames.ts b/test/e2e/testData/ExampleLinkNames.ts
index b7429544c..7283d21a4 100644
--- a/test/e2e/testData/ExampleLinkNames.ts
+++ b/test/e2e/testData/ExampleLinkNames.ts
@@ -6,6 +6,7 @@ export enum ExampleLinkName {
AIHighlightsGraph = 'AI Highlights Graph',
Analytics = 'Analytics',
APIAndEvents = 'API and Events',
+ AspectRatioCrop = 'Aspect Ratio & Crop',
AudioPlayer = 'Audio Player',
AutoplayOnScroll = 'Autoplay on Scroll',
Breakpoints = 'Breakpoints',
diff --git a/test/e2e/testData/esmPageLinksData.ts b/test/e2e/testData/esmPageLinksData.ts
index 204432d2d..656abfff1 100644
--- a/test/e2e/testData/esmPageLinksData.ts
+++ b/test/e2e/testData/esmPageLinksData.ts
@@ -9,6 +9,7 @@ export const ESM_LINKS: ExampleLinkType[] = [
{ name: ExampleLinkName.AIHighlightsGraph, endpoint: 'highlights-graph' },
{ name: ExampleLinkName.Analytics, endpoint: 'analytics' },
{ name: ExampleLinkName.APIAndEvents, endpoint: 'api' },
+ { name: ExampleLinkName.AspectRatioCrop, endpoint: 'aspect-ratio-crop' },
{ name: ExampleLinkName.AudioPlayer, endpoint: 'audio' },
{ name: ExampleLinkName.AutoplayOnScroll, endpoint: 'autoplay-on-scroll' },
{ name: ExampleLinkName.Breakpoints, endpoint: 'breakpoints' },
diff --git a/test/e2e/testData/pageLinksData.ts b/test/e2e/testData/pageLinksData.ts
index 33677701d..a4d6e505e 100644
--- a/test/e2e/testData/pageLinksData.ts
+++ b/test/e2e/testData/pageLinksData.ts
@@ -9,6 +9,7 @@ export const LINKS: ExampleLinkType[] = [
{ name: ExampleLinkName.AIHighlightsGraph, endpoint: 'highlights-graph.html' },
{ name: ExampleLinkName.Analytics, endpoint: 'analytics.html' },
{ name: ExampleLinkName.APIAndEvents, endpoint: 'api.html' },
+ { name: ExampleLinkName.AspectRatioCrop, endpoint: 'aspect-ratio-crop.html' },
{ name: ExampleLinkName.AudioPlayer, endpoint: 'audio.html' },
{ name: ExampleLinkName.AutoplayOnScroll, endpoint: 'autoplay-on-scroll.html' },
{ name: ExampleLinkName.Breakpoints, endpoint: 'breakpoints.html' },
diff --git a/test/unit/breakpoints.test.js b/test/unit/breakpoints.test.js
index 338fe96d8..06146f16f 100644
--- a/test/unit/breakpoints.test.js
+++ b/test/unit/breakpoints.test.js
@@ -56,6 +56,19 @@ describe('Breakpoints - Unit Tests', () => {
expect(source.generateSources()[0].src).not.toContain('c_limit');
});
+ it('should not add crop: limit when transformation already has crop (crop-mode set)', () => {
+ const source = new VideoSource('sea_turtle', {
+ cloudinaryConfig: cld,
+ transformation: { crop: 'fill' },
+ breakpointTransformation: { width: 640 }
+ });
+
+ const srcs = source.generateSources();
+ expect(srcs[0].src).toContain('w_640');
+ expect(srcs[0].src).toContain('c_fill');
+ expect(srcs[0].src).not.toContain('c_limit');
+ });
+
it('maxDpr should not appear in the URL — SDK does not recognize it as a transformation param', () => {
// maxDpr is an internal hint for rendition selection only.
// Unlike the old 'dpr' name, the SDK does not recognize 'maxDpr' so it never leaks into the URL.
diff --git a/test/unit/videoSource.test.js b/test/unit/videoSource.test.js
index feeec3c75..2e4638f90 100644
--- a/test/unit/videoSource.test.js
+++ b/test/unit/videoSource.test.js
@@ -635,4 +635,20 @@ describe('sourceOptions extraction and inheritance', () => {
expect(playerOptions.sourceOptions.transformation).toEqual({ width: 640 });
expect(playerOptions.colors).toEqual({ base: '#FF0000' });
});
+
+ it('should extract aspectRatio, cropMode, cropPadColor into sourceOptions', () => {
+ let elem = document.createElement('video');
+ let options = {
+ cloudName: 'demo',
+ aspectRatio: '16:9',
+ cropMode: 'fill',
+ cropPadColor: '#000000'
+ };
+ let flatOptions = extractOptions(elem, options);
+ let { playerOptions } = splitOptions(flatOptions);
+
+ expect(playerOptions.sourceOptions.aspectRatio).toBe('16:9');
+ expect(playerOptions.sourceOptions.cropMode).toBe('fill');
+ expect(playerOptions.sourceOptions.cropPadColor).toBe('#000000');
+ });
});