Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
664c169
feat: add responsive breakpoints
mizadi Feb 16, 2026
1ee5532
fix: add tests
mizadi Feb 16, 2026
75100fd
fix: test
mizadi Feb 16, 2026
ad4d44b
fix: tests
mizadi Feb 16, 2026
234bacb
fix: raw url test
mizadi Feb 16, 2026
328648b
fix: code styling
mizadi Feb 16, 2026
2b9f0d0
fix: revert package.json
mizadi Feb 16, 2026
d672c23
fix: e2e test
mizadi Feb 16, 2026
9370bf8
fix: unit test
mizadi Feb 16, 2026
ae86615
fix: dpr and redenition issues
mizadi Feb 16, 2026
a33cdef
fix: simpilify dpr vlaidation
mizadi Feb 16, 2026
d24764b
fix: breakpoints tests + getters and setters
mizadi Feb 16, 2026
31eb8e2
fix: code styling
mizadi Feb 17, 2026
814c32c
fix: add player width validation
mizadi Feb 17, 2026
93346f6
fix: remove dpr validation
mizadi Feb 17, 2026
28c8efa
fix: add breakpoints and dpr to validators
mizadi Feb 17, 2026
bc33ec2
fix: remove breakpoitns getters and setters
mizadi Feb 17, 2026
137b30c
fix: move logic to plugin
mizadi Feb 17, 2026
32262a2
fix: simplify code
mizadi Feb 17, 2026
885d2e3
fix: remove whitespace
mizadi Feb 17, 2026
eb8e80d
fix: remove whitespace
mizadi Feb 17, 2026
aaca893
fix: revert posterOptionsForCurrent
mizadi Feb 17, 2026
30477d3
fix: revert posterOptionsForCurrent
mizadi Feb 17, 2026
fc895c4
fix: lint error
mizadi Feb 17, 2026
948e09f
fix: remove redundant code
mizadi Feb 17, 2026
00671a4
fix: move player element into breakpoints
mizadi Feb 18, 2026
c7bd562
chore: add internal analytics
mizadi Feb 18, 2026
24f1263
fix: breakpoints sample page
mizadi Feb 18, 2026
afd1ad5
fix: breakpoints sample page
mizadi Feb 18, 2026
151884a
chore: add dpr to internal analytics
mizadi Feb 18, 2026
a0c37f0
fix: remove redundant code
mizadi Feb 18, 2026
fee54c4
feat: add 2560 rendition
mizadi Feb 22, 2026
6f7d0c6
feat: add 848 rendition
mizadi Feb 22, 2026
474832b
fix: breakpoints calculation logic
mizadi Feb 22, 2026
b2264c4
fix: remove redundant tests
mizadi Feb 22, 2026
3f32cfd
fix: remove redundant tests
mizadi Feb 22, 2026
0abd97b
fix: test description
mizadi Feb 22, 2026
67c2411
chore: add dpr tests
mizadi Feb 22, 2026
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
113 changes: 113 additions & 0 deletions docs/breakpoints.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Breakpoints - Cloudinary Video Player</title>
<link href="https://res.cloudinary.com/cloudinary-marketing/image/upload/f_auto,q_auto/c_scale,w_32/v1597183771/creative_staging/cloudinary_internal/Website/Brand%20Updates/Favicon/cloudinary_web_favicon_192x192.png" rel="icon" type="image/png">

<!-- Bootstrap -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

<script type="text/javascript" src="scripts.js"></script>

<style>
.player-container {
margin-bottom: 30px;
}
.video-url {
margin-top: 15px;
padding: 10px;
background: #f8f9fa;
border-radius: 5px;
word-break: break-all;
font-family: monospace;
font-size: 12px;
}
</style>

<script type="text/javascript">
window.addEventListener('load', function(){

const player = cloudinary.videoPlayer('player-with-breakpoints', {
cloudName: 'demo',
breakpoints: true,
dpr: 2.0
});
player.source('sea_turtle');

// Display video URL using multiple methods
function updateVideoUrl() {
// Try getting from video element
const videoElement = player.videojs?.el()?.querySelector('video') ||
document.querySelector('#player-with-breakpoints video') ||
document.querySelector('.vjs-tech');

const src = videoElement?.currentSrc || videoElement?.src || '';

if (src) {
document.getElementById('video-url').textContent = src;
return true;
}
return false;
}

// Update URL when video source is loaded
player.on('loadedmetadata', updateVideoUrl);

}, false);
</script>

</head>
<body>

<div class="container p-4">
<h1 class="mb-4">Breakpoints - Responsive Video Resolution</h1>

<p class="lead">
Breakpoints automatically select the optimal video resolution based on container width and device pixel ratio (DPR).
</p>

<div class="player-container">
<h3>Breakpoints Example (DPR 2.0)</h3>
<p>The player automatically rounds container width to the nearest rendition [640, 1280, 1920, 3840] and lets Cloudinary handle DPR scaling.</p>

<video
id="player-with-breakpoints"
controls
muted
class="cld-video-player cld-video-player-skin-dark cld-fluid">
</video>

<div class="video-url mt-3">
<strong>Video URL:</strong><br>
<span id="video-url">Loading...</span>
</div>

<details class="mt-3">
<summary><strong>View Code</strong></summary>
<pre><code>const player = cloudinary.videoPlayer('player', {
cloudName: 'demo',
breakpoints: true,
dpr: 2.0
});

player.source('sea_turtle');</code></pre>
</details>
</div>

<h3 class="mt-5">Configuration Options</h3>
<ul>
<li><code>breakpoints</code> (boolean): Enable/disable breakpoints</li>
<li><code>dpr</code> (number): Device pixel ratio - 1.0, 1.5, or 2.0 (default: 2.0)</li>
<li><strong>Renditions:</strong> [640, 1280, 1920, 3840] - Container width is rounded to nearest value</li>
</ul>

<p class="mt-4">
<a href="./index.html">← Back to Examples</a>
</p>

</div>

</body>
</html>
110 changes: 110 additions & 0 deletions docs/es-modules/breakpoints.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Breakpoints - Cloudinary Video Player</title>
<link
href="https://res.cloudinary.com/cloudinary-marketing/image/upload/f_auto,q_auto/c_scale,w_32,e_hue:290/creative_staging/cloudinary_internal/Website/Brand%20Updates/Favicon/cloudinary_web_favicon_192x192.png"
rel="icon"
type="image/png"
/>
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
.player-container {
margin-bottom: 30px;
}
.video-url {
margin-top: 15px;
padding: 10px;
background: #f8f9fa;
border-radius: 5px;
word-break: break-all;
font-family: monospace;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container p-4 col-12 col-md-9 col-xl-6">
<nav class="nav mb-2">
<a href="/index.html">&#60;&#60; Back to examples index</a>
</nav>
<h1>Cloudinary Video Player</h1>
<h3 class="mb-4">Breakpoints - Responsive Video Resolution</h3>

<p>
Breakpoints automatically select the optimal video resolution based on player width and device pixel ratio (DPR).
Container width is rounded to the nearest rendition [640, 1280, 1920, 3840] and Cloudinary handles DPR scaling.
</p>

<div class="player-container">
<h4 class="mt-4">Breakpoints Example (DPR 2.0)</h4>
<video
id="player-with-breakpoints"
class="cld-video-player"
controls
muted
width="500"
></video>

<div class="video-url">
<strong>Video URL:</strong><br>
<span id="video-url">Loading...</span>
</div>
</div>

<h4 class="mt-4">Configuration</h4>
<ul>
<li><code>breakpoints</code> (boolean): Enable/disable breakpoints</li>
<li><code>dpr</code> (number): Device pixel ratio - 1.0, 1.5, or 2.0 (default: 2.0)</li>
<li><strong>Renditions:</strong> [640, 1280, 1920, 3840]</li>
</ul>

<p class="mt-4">
<a href="https://cloudinary.com/documentation/cloudinary_video_player"
>Full documentation</a
>
</p>
</div>

<script type="module">
import videoPlayer from 'cloudinary-video-player/videoPlayer';
import 'cloudinary-video-player/cld-video-player.min.css';

const player = videoPlayer('player-with-breakpoints', {
cloudName: 'demo',
breakpoints: true,
dpr: 2.0
});
player.source('sea_turtle');

// Display video URL using multiple methods
function updateVideoUrl() {
// Try getting from video element
const videoElement = player.videojs?.el()?.querySelector('video') ||
document.querySelector('#player-with-breakpoints video') ||
document.querySelector('.vjs-tech');

const src = videoElement?.currentSrc || videoElement?.src || '';

if (src) {
document.getElementById('video-url').textContent = src;
return true;
}
return false;
}

// Update URL when video source is loaded
player.on('loadedmetadata', updateVideoUrl);
</script>

<!-- Bootstrap -->
<link
href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
crossorigin="anonymous"
/>
</body>
</html>
1 change: 1 addition & 0 deletions docs/es-modules/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ <h3 class="mt-4">Code examples:</h3>
<li><a href="./api.html">API and Events</a></li>
<li><a href="./audio.html">Audio Player</a></li>
<li><a href="./autoplay-on-scroll.html">Autoplay on Scroll</a></li>
<li><a href="./breakpoints.html">Breakpoints</a></li>
<li><a href="./chapters.html">Chapters</a></li>
<li><a href="./cloudinary-analytics.html">Cloudinary Analytics</a></li>
<li><a href="./codec-formats.html">Codecs and formats</a></li>
Expand Down
2 changes: 1 addition & 1 deletion docs/es-modules/raw-url.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ <h3 class="mb-4">Video with raw URL - ABR</h3>

const player = videoPlayer('player', config);

player.source('https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4');
player.source('https://res.cloudinary.com/prod/video/upload/video/examples/big_buck_bunny_trailer_720p.mp4');

const adpPlayer = videoPlayer('adpPlayer', config);

Expand Down
1 change: 1 addition & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ <h3 class="mt-4">Some code examples:</h3>
<li><a href="./api.html">API and Events</a></li>
<li><a href="./audio.html">Audio Player</a></li>
<li><a href="./autoplay-on-scroll.html">Autoplay on Scroll</a></li>
<li><a href="./breakpoints.html">Breakpoints</a></li>
<li><a href="./chapters.html">Chapters</a></li>
<li><a href="./cloudinary-analytics.html">Cloudinary Analytics</a></li>
<li><a href="./codec-formats.html">Codecs and formats</a></li>
Expand Down
4 changes: 2 additions & 2 deletions docs/raw-url.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

player = cloudinary.videoPlayer('player', config);

player.source('https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4');
player.source('https://res.cloudinary.com/prod/video/upload/video/examples/big_buck_bunny_trailer_720p.mp4');

adpPlayer = cloudinary.videoPlayer('adpPlayer',config);

Expand Down Expand Up @@ -106,7 +106,7 @@ <h3 class="mt-4">Example Code:</h3>

player = cloudinary.videoPlayer('player', config);

player.source('https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4');
player.source('https://res.cloudinary.com/prod/video/upload/video/examples/big_buck_bunny_trailer_720p.mp4');

adpPlayer = cloudinary.videoPlayer('adpPlayer',config);

Expand Down
12 changes: 12 additions & 0 deletions src/config/configSchema.json
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,18 @@
},
"default": ["auto"]
},
"breakpoints": {
"type": "boolean",
"default": false,
"description": "Enable responsive video resolution based on player width and device pixel ratio"
},
"dpr": {
"type": "number",
"minimum": 1.0,
"maximum": 2.0,
"default": 2.0,
"description": "Device pixel ratio for responsive video (1.0, 1.5, or 2.0)"
},
"resourceType": {
"type": "string",
"default": "video"
Expand Down
1 change: 1 addition & 0 deletions src/config/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default {
muted: false,
posterOptions: {},
sourceTypes: ['auto'],
breakpoints: false,
contextMenu: {
content: contextMenuContent
},
Expand Down
24 changes: 23 additions & 1 deletion src/plugins/cloudinary/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,16 @@ import {
import VideoSource from './models/video-source/video-source';
import EventHandlerRegistry from './event-handler-registry';
import AudioSource from './models/audio-source/audio-source';
import { DEFAULT_DPR, RENDITIONS } from './models/video-source/video-source.const';

import recommendationsOverlay from 'components/recommendations-overlay';

/**
* Effective DPR for breakpoints: min(user value, device DPR, cap of DEFAULT_DPR).
*/
export const getEffectiveDpr = (userDpr, deviceDpr) =>
Math.min(userDpr ?? DEFAULT_DPR, deviceDpr ?? DEFAULT_DPR, DEFAULT_DPR);

const DEFAULT_PARAMS = {
transformation: {},
sourceTypes: [],
Expand Down Expand Up @@ -126,8 +133,8 @@ class CloudinaryContext {
options.sourceTransformation = options.sourceTransformation || this.sourceTransformation();
options.sourceTypes = options.sourceTypes || this.sourceTypes();


const posterOptions = posterOptionsForCurrent();

const hasUserPosterOptions = !isEmpty(options.posterOptions);

if (options.poster === undefined) {
Expand All @@ -146,6 +153,21 @@ class CloudinaryContext {
{ hasUserPosterOptions: hasUserPosterOptions || null }
);

// Calculate breakpoint transformation: requiredWidth = playerWidth * dpr, then closest breakpoint as width (no dpr in transformation).
if (options.breakpoints) {
const playerEl = this.player.el();
const playerWidth = playerEl?.clientWidth;
const win = playerEl?.ownerDocument?.defaultView;
const deviceDpr = win?.devicePixelRatio ?? DEFAULT_DPR;
const dpr = getEffectiveDpr(options.dpr, deviceDpr);
const requiredWidth = playerWidth * dpr;
const width = RENDITIONS.find(rendition => rendition >= requiredWidth) || RENDITIONS[RENDITIONS.length - 1];
options.breakpointTransformation = {
width,
crop: 'limit'
};
}

options.queryParams = Object.assign(options.queryParams || {}, options.allowUsageReport ? { _s: `vp-${VERSION}` } : {});

if (options.sourceTypes.indexOf('audio') > -1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,7 @@ export const FORMAT_MAPPINGS = {
hls: 'm3u8',
dash: 'mpd'
};

// Breakpoints constants
export const DEFAULT_DPR = 2.0;
export const RENDITIONS = [640, 848, 1280, 1920, 2560, 3840];
6 changes: 6 additions & 0 deletions src/plugins/cloudinary/models/video-source/video-source.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class VideoSource extends BaseSource {
this.isLiveStream = options.type === 'live';
this.withCredentials = !!options.withCredentials;
this.getInitOptions = () => initOptions;
this._breakpointTransformation = options.breakpointTransformation;

// Get properties that need simple getter/setter methods (exclude special cases)
const EXCLUDED_PROPERTIES = [
Expand Down Expand Up @@ -178,6 +179,11 @@ class VideoSource extends BaseSource {
opts.transformation = castArray(srcTransformation);
}

// Merge breakpoint transformation if available
if (this._breakpointTransformation) {
opts.transformation = mergeTransformations(opts.transformation, this._breakpointTransformation);
}

Object.assign(opts, { format });

const [type, codecTrans] = formatToMimeTypeAndTransformation(sourceType);
Expand Down
2 changes: 2 additions & 0 deletions src/utils/get-analytics-player-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ const getSourceOptions = (sourceOptions = {}) => ({
...(hasConfig(sourceOptions.textTracks) ? getTextTracksOptions(sourceOptions.textTracks) : {}),
interactionAreas: hasConfig(sourceOptions.interactionAreas),
videoSources: hasConfig(sourceOptions.videoSources),
breakpoints: sourceOptions.breakpoints,
dpr: sourceOptions.dpr,
});

const getTextTracksOptions = (textTracks = {}) => {
Expand Down
2 changes: 2 additions & 0 deletions src/validators/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ export const sourceValidators = {
download: validator.isBoolean,
title: validator.or(validator.isString, validator.isBoolean),
description: validator.or(validator.isString, validator.isBoolean),
breakpoints: validator.isBoolean,
dpr: validator.isNumber,
interactionAreas: {
enable: validator.isBoolean,
template: validator.or(validator.isString(INTERACTION_AREAS_TEMPLATE), validator.isArray),
Expand Down
Loading