Skip to content

Commit

Permalink
feat(playback-core): custom cap level controller (muxinc#1010)
Browse files Browse the repository at this point in the history
Implements a custom `CapLevelController` that ensures we never cap to
480p (portrait or landscape)

**To test:**
1. Go to:
https://elements-demo-nextjs-git-fork-cjpillsbury-feat-custo-7d5867-mux.vercel.app/MuxPlayer
2. In the config UI, use the `width` input to set `MuxPlayer`'s `width`
to something smaller than 480p (keep in mind dpp, so you may want to
choose something small like `200`
3. In the config UI, use the `playbackId` input to set the desired
`playbackId` (or choose one from the dropdown)
4. Confirm that quality doesn't look sad
- **NOTE:** For advanced/programmatic confirmation, you can e.g. open
the browser's dev tools beforehand, go to the network tab, filter by
.m3u8, and make sure the video media playlists being requested match a
resolution > 480p from the response content of the multivariant playlist
  • Loading branch information
cjpillsbury authored Nov 22, 2024
1 parent 192aa79 commit e49e231
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 0 deletions.
8 changes: 8 additions & 0 deletions examples/nextjs-with-typescript/pages/MuxPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,14 @@ function MuxPlayerPage({ location }: Props) {
}}
values={['extra-small', 'small', 'large']}
/>
<NumberRenderer
value={stylesState.width}
name="width"
label="Explicit Width"
onChange={({ width }) => {
dispatchStyles(updateProps({ width }));
}}
/>
<BooleanRenderer value={state.audio} name="audio" onChange={genericOnChange} />
<EnumRenderer
value={state.theme}
Expand Down
2 changes: 2 additions & 0 deletions packages/playback-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
import { StreamTypes, PlaybackTypes, ExtensionMimeTypeMap, CmcdTypes, HlsPlaylistTypes, MediaTypes } from './types';
import { ErrorDetails, ErrorTypes, type ErrorData, type HlsConfig } from 'hls.js';
import { getErrorFromResponse, MuxJWTAud } from './request-errors';
import MinCapLevelController from './min-cap-level-controller';
// import { MediaKeySessionContext } from 'hls.js';
export {
mux,
Expand Down Expand Up @@ -636,6 +637,7 @@ export const setupHls = (

xhr.open('GET', urlObj);
},
capLevelController: MinCapLevelController,
...defaultConfig,
...streamTypeConfig,
...drmConfig,
Expand Down
59 changes: 59 additions & 0 deletions packages/playback-core/src/min-cap-level-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type Hls from 'hls.js';
import type { Level } from 'hls.js';
import { CapLevelController } from 'hls.js';

/**
* A custom HLS.js CapLevelController that behaves like the default one, except
* it enforces a "minimum maximum" to avoid forced capping to lower quality at small sizes
*/
class MinCapLevelController extends CapLevelController {
// Never cap below this level.
static minMaxResolution = 720;

constructor(hls: Hls) {
super(hls);
}

get levels() {
// NOTE: hls is a TS-private member in CapLevelController. Should be TS-protected (CJP)
// @ts-ignore
return (this.hls.levels ?? []) as Level[];
}

getValidLevels(capLevelIndex: number) {
return this.levels.filter(
// NOTE: isLevelAllowed is a TS-private member in CapLevelController. Should be TS-protected (CJP)
// @ts-ignore
(level, index) => this.isLevelAllowed(level) && index <= capLevelIndex
);
}

getMaxLevel(capLevelIndex: number) {
const baseMaxLevel = super.getMaxLevel(capLevelIndex);
const validLevels = this.getValidLevels(capLevelIndex);

// Default maxLevel selection ended up out of bounds to indicate e.g. no capping/no levels available (yet), so use it
if (!validLevels[baseMaxLevel]) return baseMaxLevel;

const baseMaxLevelResolution = Math.min(validLevels[baseMaxLevel].width, validLevels[baseMaxLevel].height);
const preferredMinMaxResolution = MinCapLevelController.minMaxResolution;

// Default maxLevel selection already meets our conditions, so use it
if (baseMaxLevelResolution >= preferredMinMaxResolution) return baseMaxLevel;

// Default maxLevel selection is below the preferred "min max", so find the lowest level
// that is >= the preference. We can simply repurpose CapLevelController:getMaxLevelByMediaSize()
// for this, "lying" about the element's size.
// NOTE: Since CapLevelController:getMaxLevelByMediaSize() uses "max square size" under the hood
// already, we don't need to duplicate that logic here.
const maxLevel = CapLevelController.getMaxLevelByMediaSize(
validLevels,
preferredMinMaxResolution * (16 / 9),
preferredMinMaxResolution
);

return maxLevel;
}
}

export default MinCapLevelController;

0 comments on commit e49e231

Please sign in to comment.