Skip to content

Commit 9dfac1b

Browse files
feat: cap-rendition-to-player-size (#1263)
Exposes hls.js's `capRenditionToPlayerSize` option with a Mux-optimized default. Closes #1238 (based off of initial effort from @spuppo-mux in #1251) ### Behavior | Value | Behavior | |-------|----------| | `undefined` (default) | Caps to player size, 720p minimum floor | | `true` | Standard hls.js capping (may go below 720p) | | `false` | No capping | ### Test plan - [ ] `npm run dev` → http://localhost:3000/MuxPlayer - [ ] Toggle "Cap Level To Player Size" between None/true/false - [ ] Verify no console errors - [ ] Verify config: `document.querySelector('mux-player').media.nativeEl._hls.config.capLevelToPlayerSize` - [ ] Trigger a rendition switch/re-selection via ABR - e.g. open Chrome Dev Tools, resize player size, seek backwards during playback to prompt segment reloading, and confirm that rendition does(n't) get capped under different permutations --------- Co-authored-by: Santiago Puppo <spuppo@mux.com>
1 parent 4efd2ec commit 9dfac1b

File tree

20 files changed

+442
-32
lines changed

20 files changed

+442
-32
lines changed

examples/nextjs-with-typescript/components/renderers.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,58 @@ export const BooleanRenderer = ({
4444
);
4545
};
4646

47+
export const OptionalBooleanRenderer = ({
48+
name,
49+
value,
50+
label,
51+
onChange,
52+
formatter = DefaultEnumFormatter
53+
}: {
54+
name: string;
55+
value: boolean | undefined;
56+
label?: string;
57+
removeFalse?: boolean;
58+
onChange: (obj: any) => void;
59+
formatter?: (enumValue: boolean) => ReactNode;
60+
}) => {
61+
const labelStr = label ?? toWordsFromKeyName(name);
62+
const values = [true, false];
63+
return (
64+
<div>
65+
<label htmlFor={`${name}-control`}>
66+
{labelStr} (<code>{name}</code>)
67+
</label>
68+
<div>
69+
<input
70+
id={`${name}-none-control`}
71+
type="radio"
72+
onChange={() => {
73+
onChange(toChangeObject(name, undefined))}}
74+
value=""
75+
checked={value === undefined}
76+
/>
77+
<label htmlFor={`${name}-none-control`}>None</label>
78+
{values.map((enumValue, i) => {
79+
return (
80+
<Fragment key={`${name}-${enumValue}`}>
81+
<input
82+
id={`${name}-${enumValue}-control`}
83+
type="radio"
84+
onChange={() => {
85+
onChange(toChangeObject(name, enumValue));
86+
}}
87+
value={enumValue.toString()}
88+
checked={value === enumValue}
89+
/>
90+
<label htmlFor={`${name}-${enumValue}-control`}>{formatter(enumValue)}</label>
91+
</Fragment>
92+
);
93+
})}
94+
</div>
95+
</div>
96+
);
97+
};
98+
4799
export const NumberRenderer = ({
48100
name,
49101
value,

examples/nextjs-with-typescript/pages/MuxPlayer.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
EnumMultiSelectRenderer,
1414
EnumRenderer,
1515
NumberRenderer,
16+
OptionalBooleanRenderer,
1617
TextRenderer,
1718
URLRenderer,
1819
} from '../components/renderers';
@@ -127,6 +128,7 @@ const DEFAULT_INITIAL_STATE: Partial<MuxPlayerProps> = Object.freeze({
127128
fullscreenElement: undefined,
128129
proudlyDisplayMuxBadge: undefined,
129130
disablePseudoEnded: undefined,
131+
capRenditionToPlayerSize: undefined,
130132
});
131133

132134
const SMALL_BREAKPOINT = 700;
@@ -282,6 +284,7 @@ function MuxPlayerPage({ location }: Props) {
282284
// debug: true,
283285
// }}
284286
maxAutoResolution="720p"
287+
capRenditionToPlayerSize={state.capRenditionToPlayerSize}
285288
title={state.title}
286289
videoTitle={state.videoTitle}
287290
startTime={state.startTime}
@@ -654,6 +657,11 @@ function MuxPlayerPage({ location }: Props) {
654657
min={0}
655658
step={1}
656659
/>
660+
<OptionalBooleanRenderer
661+
value={state.capRenditionToPlayerSize}
662+
name="capRenditionToPlayerSize"
663+
onChange={genericOnChange}
664+
/>
657665
</div>
658666
</main>
659667
</>

examples/nextjs-with-typescript/pages/mux-player.tsx

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,34 @@
1-
// @ts-nocheck
2-
import Link from "next/link";
31
import Head from "next/head";
42
import "@mux/mux-player";
5-
import { useState } from "react";
3+
import { useEffect, useRef, useState } from "react";
4+
import MuxPlayerElement from "@mux/mux-player";
5+
import { OptionalBooleanRenderer } from "../components/renderers";
6+
import { Autoplay, MuxMediaPropTypes } from "../../../packages/playback-core/dist/types/types";
67

78
const INITIAL_DEBUG = false;
8-
const INITIAL_MUTED = false;
9-
const INITIAL_AUTOPLAY = false;
9+
const INITIAL_MUTED = true;
10+
const INITIAL_AUTOPLAY: Autoplay = false;
1011
const INITIAL_PLAYBACK_ID = "g65IqSFtWdpGR100c2W8VUHrfIVWTNRen";
12+
const INITIAL_CAP_LEVEL_TO_PLAYER_SIZE : boolean | undefined = undefined;
1113

1214
function MuxPlayerWCPage() {
13-
// const mediaElRef = useRef(null);
15+
const mediaElRef = useRef<MuxPlayerElement>(null);
1416
const [playbackId, setPlaybackId] = useState(INITIAL_PLAYBACK_ID);
1517
const [muted, setMuted] = useState(INITIAL_MUTED);
1618
const [debug, setDebug] = useState(INITIAL_DEBUG);
17-
const [autoplay, setAutoplay] = useState(INITIAL_AUTOPLAY);
18-
const debugObj = debug ? { debug: "" } : {};
19-
const mutedObj = muted ? { muted: "" } : {};
20-
const autoplayObj = autoplay ? { autoplay } : {};
19+
const [autoplay, setAutoplay] = useState<MuxMediaPropTypes["autoplay"]>(INITIAL_AUTOPLAY);
20+
const [capRenditionToPlayerSize, setCapRenditionToPlayerSize] = useState<boolean | undefined>(INITIAL_CAP_LEVEL_TO_PLAYER_SIZE);
21+
const debugObj : {debug?: boolean}= debug ? { debug: true } : {};
22+
const mutedObj : {muted?: boolean} = muted ? { muted: true } : {};
23+
const autoplayObj : {autoplay?: Autoplay} = autoplay ? { autoplay: autoplay } : {};
24+
25+
// Set capRenditionToPlayerSize via JavaScript property (supports undefined, true, and false)
26+
useEffect(() => {
27+
if (mediaElRef.current) {
28+
mediaElRef.current.capRenditionToPlayerSize = capRenditionToPlayerSize;
29+
}
30+
}, [capRenditionToPlayerSize]);
31+
2132
return (
2233
<>
2334
<Head>
@@ -26,7 +37,7 @@ function MuxPlayerWCPage() {
2637

2738
<div>
2839
<mux-player
29-
// style={{ aspectRatio: "16 / 9" }}
40+
ref={mediaElRef}
3041
playback-id={playbackId}
3142
forward-seek-offset={10}
3243
backward-seek-offset={10}
@@ -43,13 +54,17 @@ function MuxPlayerWCPage() {
4354
></mux-player>
4455
</div>
4556
<div className="options">
57+
<button onClick={() => {
58+
if (!mediaElRef.current) return;
59+
mediaElRef.current.load();
60+
}}>Reload</button>
4661
<div>
4762
<label htmlFor="autoplay-control">Muted Autoplay</label>
4863
<input
4964
id="autoplay-control"
5065
type="checkbox"
5166
onChange={() => setAutoplay(!autoplay ? "muted" : false)}
52-
checked={autoplay}
67+
checked={!!autoplay}
5368
/>
5469
</div>
5570
<div>
@@ -78,6 +93,11 @@ function MuxPlayerWCPage() {
7893
defaultValue={playbackId}
7994
/>
8095
</div>
96+
<OptionalBooleanRenderer
97+
value={capRenditionToPlayerSize}
98+
name="capRenditionToPlayerSize"
99+
onChange={({ capRenditionToPlayerSize }) => setCapRenditionToPlayerSize(capRenditionToPlayerSize)}
100+
/>
81101
</div>
82102
</>
83103
);

examples/nextjs-with-typescript/pages/mux-video-react.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import Head from 'next/head';
22
import { useRef, useState } from "react";
33
import MuxVideo from "@mux/mux-video/react";
4+
import { EnumRenderer, OptionalBooleanRenderer } from '../components/renderers';
5+
import MuxVideoElement from '@mux/mux-video';
46

57
const INITIAL_AUTOPLAY = false;
68
const INITIAL_MUTED = false;
9+
const INITIAL_CAP_LEVEL_TO_PLAYER_SIZE : boolean | undefined = undefined;
10+
const INITIAL_PREFER_PLAYBACK = undefined;
711

812
function MuxVideoPage() {
9-
const mediaElRef = useRef(null);
13+
const mediaElRef = useRef<MuxVideoElement>(null);
1014
const [autoplay, setAutoplay] = useState<"muted" | boolean>(INITIAL_AUTOPLAY);
1115
const [muted, setMuted] = useState(INITIAL_MUTED);
16+
const [preferPlayback, setPreferPlayback] = useState<MuxVideoElement["preferPlayback"]>(INITIAL_PREFER_PLAYBACK);
17+
const [capRenditionToPlayerSize, setCapRenditionToPlayerSize] = useState<boolean | undefined>(INITIAL_CAP_LEVEL_TO_PLAYER_SIZE);
1218
const [paused, setPaused] = useState<boolean | undefined>(true);
1319

1420
return (
@@ -32,12 +38,13 @@ function MuxVideoPage() {
3238
// }}
3339
// envKey="mux-data-env-key"
3440
controls
41+
capRenditionToPlayerSize={capRenditionToPlayerSize}
3542
autoplay={autoplay}
3643
muted={muted}
3744
maxResolution="2160p"
3845
minResolution="540p"
3946
renditionOrder="desc"
40-
preferPlayback="native"
47+
preferPlayback={preferPlayback}
4148
onPlay={() => {
4249
setPaused(false);
4350
}}
@@ -47,6 +54,10 @@ function MuxVideoPage() {
4754
/>
4855

4956
<div className="options">
57+
<button onClick={() => {
58+
if (!mediaElRef.current) return;
59+
mediaElRef.current.load();
60+
}}>Reload</button>
5061
<div>
5162
<label htmlFor="paused-control">Paused</label>
5263
<input
@@ -74,6 +85,17 @@ function MuxVideoPage() {
7485
checked={muted}
7586
/>
7687
</div>
88+
<EnumRenderer
89+
value={preferPlayback}
90+
name="preferPlayback"
91+
onChange={({ preferPlayback }) => setPreferPlayback(preferPlayback as MuxVideoElement["preferPlayback"])}
92+
values={['mse', 'native']}
93+
/>
94+
<OptionalBooleanRenderer
95+
value={capRenditionToPlayerSize}
96+
name="capRenditionToPlayerSize"
97+
onChange={({ capRenditionToPlayerSize }) => setCapRenditionToPlayerSize(capRenditionToPlayerSize)}
98+
/>
7799
</div>
78100
</>
79101
);

packages/mux-player-react/REFERENCE.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,11 @@
4343
| `targetLiveWindow` | `number` | An offset representing the seekable range for live content, where `Infinity` means the entire live content is seekable (aka "standard DVR"). Used along with `streamType` to determine what UI/controls to show. | (inferred from `playbackId` and/or `streamType`, otherwise `NaN`) |
4444
| `startTime` | `number` (seconds) | Specify where in the media's timeline you want playback to start. | `0` |
4545
| `preferPlayback` | `"mse" \| "native"` | Specify if Mux Player should try to use Media Source Extension or native playback (if available). If no value is provided, Mux Player will choose based on what's deemed optimal for content and playback environment. | Varies |
46-
| `maxResolution` | `"720p" \| "1080p" \| "1440p" \| "2160p"` | Specify the maximum resolution you want delivered for this video. | N/A |
47-
| `minResolution` | `"480p" \| "540p" \| "720p" \| "1080p" \| "1440p" \| "2160p"` | Specify the minimum resolution you want delivered for this video. | N/A |
48-
| `maxAutoResolution` | `string` (`"720p"`, `"1080p"`, `"1440p"`, or `"2160p"`) | Cap the default resolution selection based on total pixels (width × height) to match Mux Video pricing tiers. Values align with [Mux Video resolution-based pricing](https://www.mux.com/docs/pricing/video#resolution-based-pricing). If there's an exact match, it will be used. Otherwise, selects the highest quality rendition that doesn't exceed the cap. Only accepts: `"720p"`, `"1080p"`, `"1440p"`, or `"2160p"`. Other values are ignored. | N/A |
46+
| `maxResolution` | `"720p" \| "1080p" \| "1440p" \| "2160p"` | Limits the highest resolution rendition requested from the server. Renditions above this are excluded from the playlist entirely. For client-side dimension-based capping, see `capRenditionToPlayerSize`. | N/A |
47+
| `minResolution` | `"480p" \| "540p" \| "720p" \| "1080p" \| "1440p" \| "2160p"` | Limits the lowest resolution rendition requested from the server. Renditions below this are excluded from the playlist entirely. | N/A |
48+
| `maxAutoResolution` | `string` (`"720p"`, `"1080p"`, `"1440p"`, or `"2160p"`) | Caps automatic resolution selection to align with [Mux Video pricing tiers](https://www.mux.com/docs/pricing/video#resolution-based-pricing). The player won't automatically select resolutions above this cap. Unlike `maxResolution`, all renditions remain available. Unlike `capRenditionToPlayerSize`, this is independent of player dimensions. | N/A |
4949
| `renditionOrder` | `"desc"` | Change the order in which renditions are provided in the src playlist. Can impact initial segment loads. Currently only support `"desc"` for descending order | N/A |
50+
| `capRenditionToPlayerSize` | `boolean` | Caps video resolution based on the player's display dimensions to avoid downloading video larger than can be displayed. When `undefined` (default), caps to player size with a 720p minimum floor. Set to `true` to cap strictly to player dimensions (may use lower resolutions for small players). Set to `false` to disable dimension-based capping. Independent of `maxResolution` (server-side) and `maxAutoResolution` (pricing cap). | `undefined` (Mux optimized) |
5051
| `programStartTime` | `number` | Apply PDT-based [instant clips](https://docs.mux.com/guides/create-instant-clips) to the beginning of the media stream. | N/A |
5152
| `programEndTime` | `number` | Apply PDT-based [instant clips](https://docs.mux.com/guides/create-instant-clips) to the end of the media stream. | N/A |
5253
| `assetStartTime` | `number` | Apply media timeline-based [instant clips](https://docs.mux.com/guides/create-instant-clips) to the beginning of the media stream. | N/A |

packages/mux-player-react/src/lazy.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ import useIsIntersecting from './useIsIntersecting';
1010
import type { MuxPlayerProps, MuxPlayerRefAttributes, MuxCSSProperties } from './index';
1111
import type MuxPlayerElement from '@mux/mux-player';
1212

13-
interface MuxPlayerElementReact extends Partial<Omit<MuxPlayerElement, 'style' | 'children'>> {
13+
interface MuxPlayerElementReact
14+
extends Partial<Omit<MuxPlayerElement, 'style' | 'children' | 'autoplay' | 'capRenditionToPlayerSize'>> {
1415
ref: React.MutableRefObject<MuxPlayerElement | null> | null | undefined;
15-
style: React.CSSProperties;
16+
style?: React.CSSProperties;
1617
children?: React.ReactNode;
18+
autoplay?: MuxPlayerProps['autoPlay'];
19+
'cap-rendition-to-player-size'?: boolean;
1720
}
1821

1922
declare global {

packages/mux-player-react/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export type MuxPlayerProps = {
118118
theme?: string;
119119
themeProps?: { [k: string]: any };
120120
fullscreenElement?: string;
121+
capRenditionToPlayerSize?: boolean;
121122
onAbort?: GenericEventListener<MuxPlayerElementEventMap['abort']>;
122123
onCanPlay?: GenericEventListener<MuxPlayerElementEventMap['canplay']>;
123124
onCanPlayThrough?: GenericEventListener<MuxPlayerElementEventMap['canplaythrough']>;

0 commit comments

Comments
 (0)