Skip to content

Commit 0ee2bb2

Browse files
committed
Refactor: PhantomStreamPlayer — fold ChapterStrip in, drop event bus
1 parent 1abd794 commit 0ee2bb2

6 files changed

Lines changed: 97 additions & 100 deletions

File tree

www/src/pages/docs/client-applications/pages/mac/index/index.en.mdx

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import PhantomStreamPlayer from '@shared/components/visualization/PhantomStreamPlayer';
2-
import ChapterStrip from '@shared/components/visualization/ChapterStrip';
32
import ContentTabs from '@shared/components/content/ContentTabs';
43
import DinoGame from '@shared/components/games/DinoGame';
54
import Installation from './data/installation/index.en.mdx';
@@ -12,17 +11,15 @@ import Customizing from './data/customizing/index.en.mdx';
1211
<PhantomStreamPlayer
1312
uuid="1904ae937d556f65359938a3721c5367"
1413
title="Phantom-WG Mac Quickstart"
15-
/>
16-
17-
<ChapterStrip
18-
duration={149.45}
19-
chapters={[
20-
{ slug: 'intro', label: 'Intro', start: 0 },
21-
{ slug: 'installation', label: 'Installation', start: 11.7 },
22-
{ slug: 'setting-up', label: 'Setting Up', start: 28.9 },
23-
{ slug: 'connecting', label: 'Connecting', start: 74.6 },
24-
{ slug: 'customizing', label: 'Customizing', start: 111.1 },
25-
]}
14+
chapterStrip={{
15+
chapters: [
16+
{ slug: 'intro', label: 'Intro', start: 0 },
17+
{ slug: 'installation', label: 'Installation', start: 11.7 },
18+
{ slug: 'setting-up', label: 'Setting Up', start: 28.9 },
19+
{ slug: 'connecting', label: 'Connecting', start: 74.6 },
20+
{ slug: 'customizing', label: 'Customizing', start: 111.1 },
21+
]
22+
}}
2623
/>
2724

2825
---

www/src/pages/docs/client-applications/pages/mac/index/index.mdx

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import PhantomStreamPlayer from '@shared/components/visualization/PhantomStreamPlayer';
2-
import ChapterStrip from '@shared/components/visualization/ChapterStrip';
32
import ContentTabs from '@shared/components/content/ContentTabs';
43
import DinoGame from '@shared/components/games/DinoGame';
54
import Installation from './data/installation/index.mdx';
@@ -12,17 +11,15 @@ import Customizing from './data/customizing/index.mdx';
1211
<PhantomStreamPlayer
1312
uuid="1904ae937d556f65359938a3721c5367"
1413
title="Phantom-WG Mac Quickstart"
15-
/>
16-
17-
<ChapterStrip
18-
duration={149.45}
19-
chapters={[
20-
{ slug: 'intro', label: 'Intro', start: 0 },
21-
{ slug: 'installation', label: 'Installation', start: 11.7 },
22-
{ slug: 'setting-up', label: 'Setting Up', start: 28.9 },
23-
{ slug: 'connecting', label: 'Connecting', start: 74.6 },
24-
{ slug: 'customizing', label: 'Customizing', start: 111.1 },
25-
]}
14+
chapterStrip={{
15+
chapters: [
16+
{ slug: 'intro', label: 'Intro', start: 0 },
17+
{ slug: 'installation', label: 'Installation', start: 11.7 },
18+
{ slug: 'setting-up', label: 'Setting Up', start: 28.9 },
19+
{ slug: 'connecting', label: 'Connecting', start: 74.6 },
20+
{ slug: 'customizing', label: 'Customizing', start: 111.1 },
21+
]
22+
}}
2623
/>
2724

2825
---

www/src/shared/components/visualization/styles/ChapterStrip.scss renamed to www/src/shared/components/visualization/PhantomStreamPlayer/ChapterStrip.scss

File renamed without changes.

www/src/shared/components/visualization/ChapterStrip.tsx renamed to www/src/shared/components/visualization/PhantomStreamPlayer/ChapterStrip.tsx

Lines changed: 9 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import React, { useEffect, useState } from 'react';
1+
import React from 'react';
22
import type { Chapter } from '@shared/types/chapters';
3-
import './styles/ChapterStrip.scss';
4-
5-
const CUE_EVENT = 'phantom-stream:cue';
6-
const TIME_EVENT = 'phantom-stream:time';
3+
import './ChapterStrip.scss';
74

85
interface ChapterStripProps {
96
chapters: Chapter[];
107
/** Total video duration in seconds. */
118
duration: number;
12-
/** Player wrapper selector for smooth scroll-into-view. */
13-
playerSelector?: string;
9+
/** Playhead position used to highlight the active segment. */
10+
currentTime: number;
11+
/** Called with the chapter start time when a segment is clicked. */
12+
onCue: (seconds: number) => void;
1413
}
1514

1615
function formatTime(sec: number): string {
@@ -22,32 +21,9 @@ function formatTime(sec: number): string {
2221
const ChapterStrip: React.FC<ChapterStripProps> = ({
2322
chapters,
2423
duration,
25-
playerSelector = '.phantom-stream-player',
24+
currentTime,
25+
onCue,
2626
}) => {
27-
const [currentTime, setCurrentTime] = useState(0);
28-
29-
useEffect(() => {
30-
const handler = (e: Event) => {
31-
const detail = (e as CustomEvent).detail;
32-
if (typeof detail?.currentTime === 'number') {
33-
setCurrentTime(detail.currentTime);
34-
}
35-
};
36-
window.addEventListener(TIME_EVENT, handler);
37-
return () => window.removeEventListener(TIME_EVENT, handler);
38-
}, []);
39-
40-
const handleClick = (start: number) => {
41-
window.dispatchEvent(
42-
new CustomEvent(CUE_EVENT, { detail: { seconds: start } }),
43-
);
44-
if (playerSelector) {
45-
document
46-
.querySelector(playerSelector)
47-
?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
48-
}
49-
};
50-
5127
return (
5228
<div className="chapter-strip" role="list">
5329
{chapters.map((ch, i) => {
@@ -61,7 +37,7 @@ const ChapterStrip: React.FC<ChapterStripProps> = ({
6137
key={ch.slug}
6238
className={`chapter-strip__segment${active ? ' chapter-strip__segment--active' : ''}`}
6339
style={{ flexBasis: `${widthPct}%` }}
64-
onClick={() => handleClick(start)}
40+
onClick={() => onCue(start)}
6541
role="listitem"
6642
aria-label={`${ch.label}${formatTime(start)}`}
6743
>

www/src/shared/components/visualization/styles/PhantomStreamPlayer.scss renamed to www/src/shared/components/visualization/PhantomStreamPlayer/PhantomStreamPlayer.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
margin: sp.$spacing-07 0;
88
border-radius: 8px;
99
overflow: hidden;
10-
background: #000;
1110

1211
&__aspect {
1312
position: relative;
1413
width: 100%;
1514
overflow: hidden;
15+
background: #000;
1616
/* aspect-ratio is applied inline via the `style` prop. */
1717
}
1818

www/src/shared/components/visualization/PhantomStreamPlayer.tsx renamed to www/src/shared/components/visualization/PhantomStreamPlayer/index.tsx

Lines changed: 69 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,17 @@ import { SkeletonPlaceholder } from '@carbon/react';
33
import { useTheme } from '@shared/hooks/useTheme';
44
import { useIsClient } from '@shared/hooks/useIsClient';
55
import { useLocale } from '@shared/hooks';
6-
import './styles/PhantomStreamPlayer.scss';
6+
import type { Chapter } from '@shared/types/chapters';
7+
import ChapterStrip from './ChapterStrip';
8+
import './PhantomStreamPlayer.scss';
79

810
// ── Types ────────────────────────────────────────────────────────
911

12+
interface ChapterStripConfig {
13+
/** Ordered list of chapters covering the timeline. */
14+
chapters: Chapter[];
15+
}
16+
1017
interface PhantomStreamPlayerProps {
1118
/** Cloudflare Stream video UUID (phantom-stream-tool upload output). */
1219
uuid: string;
@@ -39,6 +46,14 @@ interface PhantomStreamPlayerProps {
3946
title?: string;
4047
/** Additional class name for the outer wrapper. */
4148
className?: string;
49+
/**
50+
* If present, a chapter strip is rendered under the player; clicking a
51+
* segment seeks the embed (auto-playing on the first cue so the poster
52+
* lifts). Each player owns its own strip — multiple players on a page
53+
* stay independent. Total duration is pulled from the player itself
54+
* once metadata loads.
55+
*/
56+
chapterStrip?: ChapterStripConfig;
4257
}
4358

4459
// ── Theme-aware defaults ─────────────────────────────────────────
@@ -84,6 +99,7 @@ const PhantomStreamPlayerCore: React.FC<PhantomStreamPlayerProps> = ({
8499
aspectRatio = '16 / 9',
85100
title,
86101
className = '',
102+
chapterStrip,
87103
}) => {
88104
const { theme } = useTheme();
89105
const { locale } = useLocale();
@@ -99,19 +115,14 @@ const PhantomStreamPlayerCore: React.FC<PhantomStreamPlayerProps> = ({
99115
// play — otherwise the user stares at a black letterbox.
100116
const [videoReady, setVideoReady] = useState(false);
101117

102-
useEffect(() => {
103-
let cancelled = false;
104-
import('@cloudflare/stream-react').then((mod) => {
105-
if (!cancelled) setStreamComp(() => mod.Stream);
106-
});
107-
return () => {
108-
cancelled = true;
109-
};
110-
}, []);
118+
// Drives the chapter strip's "currently playing" highlight.
119+
const [currentTime, setCurrentTime] = useState(0);
120+
121+
// Pulled from the player once metadata loads; the chapter strip
122+
// waits for this before rendering segment widths.
123+
const [duration, setDuration] = useState(0);
111124

112125
// Imperative handle from @cloudflare/stream-react (HTMLVideoElement-shaped).
113-
// MDX modules can't share refs across pages, so chapter cues travel via
114-
// window events — any CueLink / ChapterStrip on the page can drive us.
115126
// eslint-disable-next-line @typescript-eslint/no-explicit-any
116127
const streamRef = useRef<any>(null);
117128

@@ -122,40 +133,48 @@ const PhantomStreamPlayerCore: React.FC<PhantomStreamPlayerProps> = ({
122133
const hasStartedRef = useRef(false);
123134

124135
useEffect(() => {
125-
const handler = (e: Event) => {
126-
const seconds = (e as CustomEvent).detail?.seconds;
127-
if (typeof seconds === 'number' && streamRef.current) {
128-
try {
129-
streamRef.current.currentTime = seconds;
130-
if (!hasStartedRef.current) {
131-
const result = streamRef.current.play?.();
132-
if (result?.catch) {
133-
result.catch(() => {
134-
// Sound autoplay refused — retry muted so the poster
135-
// at least clears and frames appear.
136-
try {
137-
streamRef.current.muted = true;
138-
streamRef.current.play?.();
139-
} catch {
140-
/* give up silently */
141-
}
142-
});
136+
let cancelled = false;
137+
import('@cloudflare/stream-react').then((mod) => {
138+
if (!cancelled) setStreamComp(() => mod.Stream);
139+
});
140+
return () => {
141+
cancelled = true;
142+
};
143+
}, []);
144+
145+
const cueTo = (seconds: number) => {
146+
if (!Number.isFinite(seconds) || !streamRef.current) return;
147+
try {
148+
streamRef.current.currentTime = seconds;
149+
if (!hasStartedRef.current) {
150+
const result = streamRef.current.play?.();
151+
if (result?.catch) {
152+
result.catch(() => {
153+
// Sound autoplay refused — retry muted so the poster
154+
// at least clears and frames appear.
155+
try {
156+
streamRef.current.muted = true;
157+
streamRef.current.play?.();
158+
} catch {
159+
/* give up silently */
143160
}
144-
}
145-
} catch {
146-
/* player not ready yet */
161+
});
147162
}
148163
}
149-
};
150-
window.addEventListener('phantom-stream:cue', handler);
151-
return () => window.removeEventListener('phantom-stream:cue', handler);
152-
}, []);
164+
} catch {
165+
/* player not ready yet */
166+
}
167+
};
153168

154169
const handleTimeUpdate = () => {
155-
const t = streamRef.current?.currentTime ?? 0;
156-
window.dispatchEvent(
157-
new CustomEvent('phantom-stream:time', { detail: { currentTime: t } }),
158-
);
170+
setCurrentTime(streamRef.current?.currentTime ?? 0);
171+
};
172+
173+
const handleCanPlay = () => {
174+
setVideoReady(true);
175+
// canPlay guarantees metadata is loaded, so the imperative handle's
176+
// duration is now valid.
177+
setDuration(streamRef.current?.duration ?? 0);
159178
};
160179

161180
const themeColors = THEME_COLORS[theme] ?? THEME_COLORS.white;
@@ -190,7 +209,7 @@ const PhantomStreamPlayerCore: React.FC<PhantomStreamPlayerProps> = ({
190209
width="100%"
191210
title={title}
192211
streamRef={streamRef}
193-
onCanPlay={() => setVideoReady(true)}
212+
onCanPlay={handleCanPlay}
194213
onPlay={() => { hasStartedRef.current = true; }}
195214
onTimeUpdate={handleTimeUpdate}
196215
/>
@@ -200,6 +219,14 @@ const PhantomStreamPlayerCore: React.FC<PhantomStreamPlayerProps> = ({
200219
<SkeletonPlaceholder className="phantom-stream-player__skeleton" />
201220
)}
202221
</div>
222+
{chapterStrip && duration > 0 && (
223+
<ChapterStrip
224+
chapters={chapterStrip.chapters}
225+
duration={duration}
226+
currentTime={currentTime}
227+
onCue={cueTo}
228+
/>
229+
)}
203230
</div>
204231
);
205232
};

0 commit comments

Comments
 (0)