Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(playback-core, mux-video, mux-player): addChapters interface #909

Merged
merged 31 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e0ca84f
interface for setting and getting chapters
AdamJaggard Apr 23, 2024
061d853
fix method name
AdamJaggard Apr 23, 2024
8d31ba6
track kind "chapters"
AdamJaggard Apr 23, 2024
4b6a48d
align track attributes with how media store locates text tracks for c…
AdamJaggard Apr 24, 2024
0016ae6
undo space change
AdamJaggard Apr 24, 2024
6c4af58
chapters example in vanilla
AdamJaggard Apr 24, 2024
f03d9c3
cuepoints -> chapters
AdamJaggard Apr 24, 2024
c2480ac
easier to read
AdamJaggard Apr 24, 2024
efc0bd2
undoing this, it wasn't needed
AdamJaggard Apr 24, 2024
f881819
force mode change to cause a change event
AdamJaggard Apr 25, 2024
0ab2660
forced mode flipping to use "showing" instead of "disabled"
AdamJaggard Apr 25, 2024
e88926b
tests
AdamJaggard Apr 25, 2024
bb362f6
remove unimplemented event
AdamJaggard Apr 25, 2024
43c7ff1
nextjs example
AdamJaggard Apr 25, 2024
7007542
optional end time for chapters
AdamJaggard Apr 26, 2024
5f24490
cuepoints with similar shape to chapters, supporting legacy type
AdamJaggard Apr 26, 2024
bdf3a59
updated types
AdamJaggard May 1, 2024
6127b49
update reference
AdamJaggard May 1, 2024
e6ba018
use audio UI for second example
AdamJaggard May 1, 2024
b278b59
remove else
AdamJaggard May 1, 2024
99f1712
fix broken links after rebase onto nextjs upgrade
AdamJaggard May 2, 2024
f4cb9a9
remove mode flipping hack, manually dispatch change event to update s…
AdamJaggard May 16, 2024
571e792
shared getTextTrack
AdamJaggard May 16, 2024
b4ee78c
addChapters and addCuePoints to use one shared function
AdamJaggard May 17, 2024
a00de2f
oops, don't stringify strings!
AdamJaggard May 17, 2024
3aafd8a
demos more readable
AdamJaggard May 17, 2024
6561194
fix tests for webkit, don't skip cuepoints tests
AdamJaggard May 17, 2024
a3d9aa1
account for cuepoint values being strings and needing to be stringified
AdamJaggard May 17, 2024
5db3f62
fix(playback-core): Fix chapters and cuepoints edge case for active cue.
cjpillsbury May 17, 2024
79117f1
test(mux-player): Cleanup async and event logic in tests (particularl…
cjpillsbury May 17, 2024
0d7077f
Merge branch 'main' into feat/chapters
AdamJaggard May 21, 2024
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
55 changes: 55 additions & 0 deletions examples/nextjs-with-typescript/pages/MuxPlayerChapters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Link from "next/link";
import Head from 'next/head';
import { useState } from "react";
import MuxPlayer from "@mux/mux-player-react";
import type MuxPlayerElement from "@mux/mux-player";

type Chapter = { startTime: number; endTime: number; value: string; };
const chapters: Chapter[] = [
{ startTime: 0, endTime: 3, value: 'Chapter 1' },
{ startTime: 3, endTime: 6, value: 'Chapter 2' },
{ startTime: 6, endTime: 9, value: 'Chapter 3' }
];


function MuxPlayerPage() {
const [activeChapter, setActiveChapter] = useState<Chapter>(undefined);

function addChaptersToPlayer(playerEl: MuxPlayerElement) {
playerEl.addChapters(chapters);
}

return (
<>
<Head>
<title>&lt;MuxPlayer/&gt; (CuePoints) Demo</title>
</Head>

<MuxPlayer
playbackId="23s11nz72DsoN657h4314PjKKjsF2JG33eBQQt6B95I"
streamType="on-demand"
preload="auto"
onLoadedMetadata={({ target }) => {
addChaptersToPlayer(target as MuxPlayerElement);
}}
onChapterChange={({ target }) => {
setActiveChapter((target as MuxPlayerElement).activeChapter);
}}
/>
<div>
<b>Active chapter:</b>
<pre style={{
backgroundColor: '#888',
padding: '10px',
color: '#fff'
}}>
{activeChapter?.value ?? 'Unset'}
</pre>
</div>
<br />
<Link href="/">Browse Elements</Link>
</>
);
}

export default MuxPlayerPage;
39 changes: 20 additions & 19 deletions examples/nextjs-with-typescript/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,26 @@ function HomePage() {
return (
<nav>
<ul>
<li><Link legacyBehavior href="/MuxVideo"><a className="video">&lt;MuxVideo&gt;</a></Link></li>
<li><Link legacyBehavior href="/MuxAudio"><a className="audio">&lt;MuxAudio&gt;</a></Link></li>
<li><Link legacyBehavior href="/MuxPlayer"><a className="player">&lt;MuxPlayer&gt;</a></Link></li>
<li><Link legacyBehavior href="/MuxPlayerPosterSlot"><a className="player">&lt;MuxPlayer&gt;<br/>(poster slot)</a></Link></li>
<li><Link legacyBehavior href="/MuxPlayerTheme"><a className="player">&lt;MuxPlayer&gt;<br/>(theme)</a></Link></li>
<li><Link legacyBehavior href="/MuxPlayerCuePoints"><a className="player">&lt;MuxPlayer&gt;<br/>(CuePoints)</a></Link></li>
<li><Link legacyBehavior href="/MuxPlayerCuePointsMeditate"><a className="player">&lt;MuxPlayer&gt;<br/>(CuePoints + Audio Only)</a></Link></li>
<li><Link legacyBehavior href="/MuxPlayerLazy"><a className="player">&lt;MuxPlayer&gt;<br/>(lazy)</a></Link></li>
<li><Link legacyBehavior href="/MuxPlayerDynamic"><a className="player">&lt;MuxPlayer&gt;<br/>(Next.js dynamic)</a></Link></li>
<li><Link legacyBehavior href="/MuxPlayerLazyDynamic"><a className="player">&lt;MuxPlayer&gt;<br/>(lazy + dynamic)</a></Link></li>
<li><Link legacyBehavior href="/MuxPlayerIframe"><a className="player">&lt;MuxPlayer&gt;<br/>(w/o fullscreen)</a></Link></li>
<li><Link legacyBehavior href="/MuxPlayerLazyIframe"><a className="player">&lt;MuxPlayer&gt;<br/>(lazy + w/o fullscreen)</a></Link></li>
<li><Link legacyBehavior href="/MuxPlayerLazyBlurUp"><a className="player">&lt;MuxPlayer&gt;<br/>(lazy + @mux/blurup)</a></Link></li>
<li><Link legacyBehavior href="/MuxUploader"><a className="uploader">&lt;MuxUploader&gt;</a></Link></li>
<li><Link legacyBehavior href="/MuxUploaderSeparateComponents"><a className="uploader">&lt;MuxUploader&gt; (Separate Components)</a></Link></li>
<li><Link legacyBehavior href="/mux-video"><a className="video">&lt;mux-video&gt;<br/>(Web Component)</a></Link></li>
<li><Link legacyBehavior href="/mux-audio"><a className="audio">&lt;mux-audio&gt;<br/>(Web Component)</a></Link></li>
<li><Link legacyBehavior href="/mux-player"><a className="player">&lt;mux-player&gt;<br/>(Web Component)</a></Link></li>
<li><Link legacyBehavior href="/app-router"><a className="react">App Router</a></Link></li>
<li><Link href="/MuxVideo" className="video">&lt;MuxVideo&gt;</Link></li>
<li><Link href="/MuxAudio"className="audio">&lt;MuxAudio&gt;</Link></li>
<li><Link href="/MuxPlayer" className="player">&lt;MuxPlayer&gt;</Link></li>
<li><Link href="/MuxPlayerPosterSlot" className="player"><>&lt;MuxPlayer&gt;<br/>(poster slot)</></Link></li>
<li><Link href="/MuxPlayerTheme" className="player"><>&lt;MuxPlayer&gt;<br/>(theme)</></Link></li>
<li><Link href="/MuxPlayerCuePoints" className="player"><>&lt;MuxPlayer&gt;<br/>(CuePoints)</></Link></li>
<li><Link href="/MuxPlayerCuePointsMeditate" className="player"><>&lt;MuxPlayer&gt;<br/>(CuePoints + Audio Only)</></Link></li>
<li><Link href="/MuxPlayerChapters" className="player"><>&lt;MuxPlayer&gt;<br/>(Chapters)</></Link></li>
<li><Link href="/MuxPlayerLazy" className="player"><>&lt;MuxPlayer&gt;<br/>(lazy)</></Link></li>
<li><Link href="/MuxPlayerDynamic" className="player"><>&lt;MuxPlayer&gt;<br/>(Next.js dynamic)</></Link></li>
<li><Link href="/MuxPlayerLazyDynamic" className="player"><>&lt;MuxPlayer&gt;<br/>(lazy + dynamic)</></Link></li>
<li><Link href="/MuxPlayerIframe" className="player"><>&lt;MuxPlayer&gt;<br/>(w/o fullscreen)</></Link></li>
<li><Link href="/MuxPlayerLazyIframe" className="player"><>&lt;MuxPlayer&gt;<br/>(lazy + w/o fullscreen)</></Link></li>
<li><Link href="/MuxPlayerLazyBlurUp" className="player"><>&lt;MuxPlayer&gt;<br/>(lazy + @mux/blurup)</></Link></li>
<li><Link href="/MuxUploader" className="uploader"><>&lt;MuxUploader&gt;</></Link></li>
<li><Link href="/MuxUploaderSeparateComponents" className="uploader"><>&lt;MuxUploader&gt; (Separate Components)</></Link></li>
<li><Link href="/mux-video" className="video"><>&lt;mux-video&gt;<br/>(Web Component)</></Link></li>
<li><Link href="/mux-audio" className="audio"><>&lt;mux-audio&gt;<br/>(Web Component)</></Link></li>
<li><Link href="/mux-player" className="player"><>&lt;mux-player&gt;<br/>(Web Component)</></Link></li>
<li><Link href="/app-router" className="react"><>App Router</></Link></li>
</ul>
</nav>
);
Expand Down
1 change: 1 addition & 0 deletions examples/vanilla-ts-esm/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ <h1><a href="/">Elements</a></h1>
<li><a href="./mux-player.html" class="player">&lt;mux-player&gt;</a></li>
<li><a href="./mux-player-audio.html" class="player">&lt;mux-player audio&gt;</a></li>
<li><a href="./mux-player-cuepoints.html" class="player">&lt;mux-player&gt; (cuePoints)</a></li>
<li><a href="./mux-player-chapters.html" class="player">&lt;mux-player&gt; (chapters)</a></li>
<li><a href="./mux-player-renditions.html" class="player">&lt;mux-player&gt; (renditions)</a></li>
<li><a href="./mux-player-poster-slot.html" class="player">&lt;mux-player&gt; (poster slot)</a></li>
<li><a href="./mux-player-audio-tracks.html" class="player">&lt;mux-player&gt; (audio tracks)</a></li>
Expand Down
127 changes: 127 additions & 0 deletions examples/vanilla-ts-esm/public/mux-player-chapters.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>&lt;mux-player&gt; | Chapters</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css">
<link rel="stylesheet" href="./styles.css">
<script type="module" src="./dist/mux-player.js"></script>
<style>
mux-player {
display: block;
width: 100%;
margin: 1rem 0 2rem;
background-color: #000;
}

mux-player:not([audio]) {
aspect-ratio: 16 / 9;
}

#chapter-renderer {
background-color: #888;
color: #fff;
padding: 10px;
}
</style>
</head>
<body>
<header>
<div class="left-header">
<a class="mux-logo" href="https://www.mux.com/player" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/360826/233653989-11cd8603-c20f-4008-8bf7-dc15b743c52b.svg">
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/360826/233653583-50dda726-cbe7-4182-a113-059a91ae83e6.svg">
<img alt="Mux Logo" src="https://user-images.githubusercontent.com/360826/233653583-50dda726-cbe7-4182-a113-059a91ae83e6.svg">
</picture>
</a>
<h1><a href="/">Elements</a></h1>
</div>
<div class="right-header">
<a class="github-logo" href="https://github.com/muxinc/elements" target="_blank">
<img width="32" height="32" src="./images/github-logo.svg" alt="Github logo">
</a>
</div>
</header>

<h2>Chapters with explicit end times (with gaps)</h2>

<mux-player
id="playerOne"
stream-type="on-demand"
playback-id="23s11nz72DsoN657h4314PjKKjsF2JG33eBQQt6B95I"
muted
preload="auto"
></mux-player>
<p>
<b>Active Chapter:</b> <pre id="chapter-renderer">Unset</pre>
</p>
<p>
<button onclick="window.playerOne.addChapters([{startTime: 15, endTime: 18, value: 'Extra chapter'}]); this.disabled = true;">Add additional fourth chapter</button>
</p>


<h2>Chapters with implicit end times (no gaps, audio UI)</h2>

<mux-player
id="playerTwo"
stream-type="on-demand"
playback-id="23s11nz72DsoN657h4314PjKKjsF2JG33eBQQt6B95I"
muted
preload="auto"
audio
></mux-player>

<p>
<button onclick="window.playerTwo.addChapters([{startTime: 6, value: 'Chapter 2.5'}]); this.disabled = true;">Add chapter between 2 and 3</button>
</p>

<script>
const playerOneEl = document.querySelector('#playerOne');
const playerTwoEl = document.querySelector('#playerTwo');

function addChapterstoPlayerOne() {
const chaptersOne = [
{ startTime: 1, endTime: 3, value: 'Chapter 1' },
{ startTime: 3, endTime: 6, value: 'Chapter 2 (joined)' },
{ startTime: 10, endTime: 15, value: 'Chapter 3 (after gap)' }
];
playerOneEl.addChapters(chaptersOne);

console.log('Player one chapters added');
}

function addChapterstoPlayerTwo() {
const chaptersTwo = [
{ startTime: 1, value: 'Chapter 1' },
{ startTime: 3, value: 'Chapter 2' },
{ startTime: 10, value: 'Chapter 3 (to the end...)' }
];
playerTwoEl.addChapters(chaptersTwo);

console.log('Player 2 chapters added');
}

function chapterChangeListener() {
console.log('Active Chapter', playerOneEl.activeChapter);
document.getElementById('chapter-renderer').innerText = playerOneEl.activeChapter.value;
}

playerOneEl.addEventListener('chapterchange', chapterChangeListener);

// NOTE: Need to wait until the player has loaded metadata before adding chapters!
if (playerOneEl.readyState >= 1) {
addChapterstoPlayerOne();
} else {
playerOneEl.addEventListener('loadedmetadata', addChapterstoPlayerOne, { once: true });
}

if (playerTwoEl.readyState >= 1) {
addChapterstoPlayerTwo();
} else {
playerTwoEl.addEventListener('loadedmetadata', addChapterstoPlayerTwo, { once: true });
}
</script>
<a href="../">Browse Elements</a>
</body>
</html>
3 changes: 2 additions & 1 deletion examples/vanilla-ts-esm/public/mux-player-cuepoints.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
}

#cuepoint-renderer {
background-color: lightgray;
background-color: #888;
color: #fff;
padding: 10px;
}
</style>
Expand Down
3 changes: 3 additions & 0 deletions packages/mux-player-react/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export type MuxPlayerProps = {
onError?: GenericEventListener<MuxPlayerElementEventMap['error']>;
onCuePointChange?: GenericEventListener<MuxPlayerElementEventMap['cuepointchange']>;
onCuePointsChange?: GenericEventListener<MuxPlayerElementEventMap['cuepointschange']>;
onChapterChange?: GenericEventListener<MuxPlayerElementEventMap['chapterchange']>;
} & Partial<MuxMediaPropTypes> &
Partial<VideoApiAttributes>;

Expand Down Expand Up @@ -178,6 +179,7 @@ const usePlayer = (
onError,
onCuePointChange,
onCuePointsChange,
onChapterChange,
metadata,
tokens,
paused,
Expand Down Expand Up @@ -244,6 +246,7 @@ const usePlayer = (
useEventCallbackEffect('error', ref, onError);
useEventCallbackEffect('cuepointchange', ref, onCuePointChange);
useEventCallbackEffect('cuepointschange', ref, onCuePointsChange);
useEventCallbackEffect('chapterchange', ref, onChapterChange);
return [remainingProps];
};

Expand Down
10 changes: 7 additions & 3 deletions packages/mux-player/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@
| --------- | -------------------------------------------------------------------------------------------------------------------- |
| `play()` | Identical to the [native `play()` method](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play). |
| `pause()` | Identical to the [native `pause()` method](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/pause). |
| `addCuePoints()` | Add an array of metadata CuePoints of "shape" `{ time: number; value: any; }` to the Mux Player instance for the current media |
| `addCuePoints()` | Add an array of CuePoints with the shape `{ startTime: number; endTime?: number, value: any; }` to the Mux Player instance |
| `addChapters()` | Add an array of chapters with the shape `{ startTime: number; endTime?: number, value: string; }` to the Mux Player instance |
| `getStartDate()` | Will return a Date that matches the earliest PDT in your stream. Identical to [native `getStartDate()` method](https://html.spec.whatwg.org/multipage/media.html#dom-media-getstartdate), if exists. |
|

Expand Down Expand Up @@ -138,8 +139,10 @@
| `storyboardSrc` | `string` (URL) | Full URL string for the storyboard asset. Setting this will override the storyboard URL derived from the playback ID. | `undefined` |
| `tokens` | `object`\* | An object for setting all signed URL tokens with the signature `{ playback: string; thumbnail: string; storyboard: string; }`. If any `*token` properties or `*-token` attributes are set, they will take precedence. | `undefined` |
| `textTracks` | [`TextTrackList`](https://developer.mozilla.org/en-US/docs/Web/API/TextTrackList) | Identical to the [native `textTracks` property](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/textTracks) | (empty `TextTrackList` instance) |
| `cuePoints` <sub><sup>Read only</sup></sub> | `Array<{ time: number; value: any; }>` | The array of CuePoints for the current media, added via `addCuePoints(cuePoints)`. | `[]` |
| `activeCuePoint` <sub><sup>Read only</sup></sub> | `{ time: number; value: any; }` | The current active CuePoint, determined based on the player's `currentTime`. | `undefined` |
| `cuePoints` <sub><sup>Read only</sup></sub> | `Array<{ startTime: number; endTime?: number, value: any; }>` | The array of CuePoints for the current media, added via `addCuePoints(cuePoints)`. | `[]` |
| `chapters` <sub><sup>Read only</sup></sub> | `Array<{ startTime: number; endTime?: number, value: string; }>` | The array of Chapters for the current media, added via `addChapters(chapters)`. | `[]` |
| `activeCuePoint` <sub><sup>Read only</sup></sub> | `{ startTime: number; endTime?: number, value: any; }` | The current active CuePoint, determined based on the player's `currentTime`. | `undefined` |
| `activeChapter` <sub><sup>Read only</sup></sub> | `{ startTime: number; endTime?: number, value: string; }` | The current active Chapter, determined based on the player's `currentTime`. | `undefined` |

<!-- UNDOCUMENTED
// NEW STREAM TYPE VALUES
Expand Down Expand Up @@ -181,6 +184,7 @@
| `volumechange` | Identical to the native [`volumechange` event](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/volumechange_event) |
| `waiting` | Identical to the native [`waiting` event](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/waiting_event) |
| `cuepointchange` | Similar to the native `TextTrack` [`cuechange` event](https://developer.mozilla.org/en-US/docs/Web/API/TextTrack/cuechange_event), only the event's `detail` will be the `activeCuePoint` |
| `chapterchange` | Similar to the native `TextTrack` [`cuechange` event](https://developer.mozilla.org/en-US/docs/Web/API/TextTrack/cuechange_event), only the event's `detail` will be the `activeChapter` |

# CSS Variables

Expand Down
21 changes: 21 additions & 0 deletions packages/mux-player/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ const DEFAULT_EXTRA_PLAYLIST_PARAMS = { redundant_streams: true };
export interface MuxPlayerElementEventMap extends HTMLVideoElementEventMap {
cuepointchange: CustomEvent<{ time: number; value: any }>;
cuepointschange: CustomEvent<Array<{ time: number; value: any }>>;
chapterchange: CustomEvent<{ startTime: number; endTime: number; value: string }>;
}

interface MuxPlayerElement
Expand Down Expand Up @@ -1507,6 +1508,26 @@ class MuxPlayerElement extends VideoApiElement implements MuxPlayerElement {
return this.media?.cuePoints ?? [];
}

addChapters(chapters: { startTime: number; endTime: number; value: string }[]) {
this.#init();

// NOTE: This condition should never be met. If it is, there is a bug (CJP)
if (!this.media) {
logger.error('underlying media element missing when trying to addChapters. chapters will not be added.');
return;
}

return this.media?.addChapters(chapters);
}

get activeChapter() {
return this.media?.activeChapter;
}

get chapters() {
return this.media?.chapters ?? [];
}

getStartDate() {
return this.media?.getStartDate();
}
Expand Down
5 changes: 4 additions & 1 deletion packages/mux-player/src/themes/gerwig/gerwig.html
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
--media-preview-border-radius: 5px;
--media-text-color: var(--_text-color);
--media-control-hover-background: transparent;
--media-preview-chapter-text-shadow: none;
color: var(--_accent-color);
padding: 0 6px;
}
Expand Down Expand Up @@ -272,7 +273,9 @@
paint-order: stroke;
stroke: rgba(102, 102, 102, 1);
stroke-width: 0.3px;
text-shadow: 0 0 2px rgb(0 0 0 / 0.25), 0 0 6px rgb(0 0 0 / 0.25);
text-shadow:
0 0 2px rgb(0 0 0 / 0.25),
0 0 6px rgb(0 0 0 / 0.25);
}

.center-controls media-play-button {
Expand Down
Loading
Loading