Skip to content

Commit fc6f3a7

Browse files
committed
feat(player): add chapter markers overlay and improve player wrapper structure
1 parent f314292 commit fc6f3a7

File tree

2 files changed

+128
-28
lines changed

2 files changed

+128
-28
lines changed

packages/visualizer/src/component/player/index.less

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,68 @@
4949
border-top-right-radius: 4px;
5050
border-top-left-radius: 4px;
5151

52-
// Remotion Player renders as a div, not a canvas
53-
> div {
52+
// Player wrapper positions chapter markers relative to the player
53+
.player-wrapper {
54+
position: relative;
5455
width: 100%;
5556
max-width: 100%;
5657
max-height: 100%;
57-
border: none;
58+
59+
> div:not(.chapter-markers) {
60+
width: 100%;
61+
max-width: 100%;
62+
max-height: 100%;
63+
border: none;
64+
}
5865
}
5966

6067
// Remove border/outline from Remotion control bar buttons
6168
button {
6269
border: none;
6370
outline: none;
6471
}
72+
73+
// Chapter markers overlaid on Remotion's seek bar
74+
// Remotion seek bar layout (from player bottom):
75+
// paddingBottom=10 + VERTICAL_PADDING=4 → bar bottom at 14px
76+
// BAR_HEIGHT=5 → bar top at 19px, center at 16.5px
77+
// X_PADDING=12 on each side
78+
.chapter-markers {
79+
position: absolute;
80+
bottom: 14px;
81+
left: 12px;
82+
right: 12px;
83+
height: 5px;
84+
pointer-events: none;
85+
z-index: 1;
86+
87+
.chapter-marker {
88+
position: absolute;
89+
top: 50%;
90+
width: 3px;
91+
height: 12px;
92+
background: rgba(255, 255, 255, 0.9);
93+
transform: translate(-50%, -50%);
94+
border-radius: 1.5px;
95+
pointer-events: auto;
96+
cursor: pointer;
97+
98+
// Larger hit area for easier hovering/clicking
99+
&::before {
100+
content: '';
101+
position: absolute;
102+
top: -6px;
103+
left: -6px;
104+
right: -6px;
105+
bottom: -6px;
106+
}
107+
108+
&:hover {
109+
background: #fff;
110+
height: 16px;
111+
}
112+
}
113+
}
65114
}
66115

67116
// Custom controls rendered inside Remotion Player's control bar

packages/visualizer/src/component/player/index.tsx

Lines changed: 76 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,37 @@ export function Player(props?: {
349349
mouseOverSettingsIcon,
350350
]);
351351

352+
// Compute chapter markers from step boundaries (each img/insight = new chapter)
353+
const chapterMarkers = useMemo(() => {
354+
if (!frameMap) return [];
355+
const { scriptFrames, totalDurationInFrames, openingDurationInFrames } =
356+
frameMap;
357+
if (totalDurationInFrames === 0) return [];
358+
359+
const markers: { percent: number; title: string; frame: number }[] = [];
360+
for (const sf of scriptFrames) {
361+
if (
362+
(sf.type !== 'img' && sf.type !== 'insight') ||
363+
sf.durationInFrames === 0
364+
)
365+
continue;
366+
const globalFrame = openingDurationInFrames + sf.startFrame;
367+
const percent = (globalFrame / totalDurationInFrames) * 100;
368+
if (percent > 1 && percent < 99) {
369+
const parts = [sf.title, sf.subTitle].filter(Boolean);
370+
markers.push({
371+
percent,
372+
title:
373+
parts.length > 0
374+
? parts.join(': ')
375+
: `Chapter ${markers.length + 1}`,
376+
frame: globalFrame,
377+
});
378+
}
379+
}
380+
return markers;
381+
}, [frameMap]);
382+
352383
// If no scripts, show empty
353384
if (!scripts || scripts.length === 0 || !frameMap) {
354385
return <div className="player-container" />;
@@ -360,31 +391,51 @@ export function Player(props?: {
360391
return (
361392
<div className="player-container" data-fit-mode={props?.fitMode}>
362393
<div className="canvas-container">
363-
<RemotionPlayer
364-
ref={playerRef}
365-
component={Composition}
366-
inputProps={{
367-
frameMap,
368-
effects: effectsEnabled,
369-
autoZoom,
370-
}}
371-
durationInFrames={Math.max(frameMap.totalDurationInFrames, 1)}
372-
compositionWidth={compositionWidth}
373-
compositionHeight={compositionHeight}
374-
fps={frameMap.fps}
375-
playbackRate={playbackSpeed}
376-
controls
377-
showVolumeControls={false}
378-
renderPlayPauseButton={renderPlayPauseButton}
379-
renderFullscreenButton={renderFullscreenButton}
380-
renderCustomControls={renderCustomControls}
381-
autoPlay
382-
loop={false}
383-
style={{
384-
width: '100%',
385-
aspectRatio: `${compositionWidth}/${compositionHeight}`,
386-
}}
387-
/>
394+
<div className="player-wrapper">
395+
<RemotionPlayer
396+
ref={playerRef}
397+
component={Composition}
398+
inputProps={{
399+
frameMap,
400+
effects: effectsEnabled,
401+
autoZoom,
402+
}}
403+
durationInFrames={Math.max(frameMap.totalDurationInFrames, 1)}
404+
compositionWidth={compositionWidth}
405+
compositionHeight={compositionHeight}
406+
fps={frameMap.fps}
407+
playbackRate={playbackSpeed}
408+
controls
409+
showVolumeControls={false}
410+
renderPlayPauseButton={renderPlayPauseButton}
411+
renderFullscreenButton={renderFullscreenButton}
412+
renderCustomControls={renderCustomControls}
413+
autoPlay
414+
loop={false}
415+
style={{
416+
width: '100%',
417+
aspectRatio: `${compositionWidth}/${compositionHeight}`,
418+
zIndex: 0,
419+
}}
420+
/>
421+
422+
{/* Chapter markers overlay on Remotion's seek bar */}
423+
{chapterMarkers.length > 0 && (
424+
<div className="chapter-markers">
425+
{chapterMarkers.map((marker) => (
426+
<Tooltip key={marker.percent} title={marker.title}>
427+
<div
428+
className="chapter-marker"
429+
style={{ left: `${marker.percent}%` }}
430+
onClick={() => {
431+
playerRef.current?.seekTo(marker.frame);
432+
}}
433+
/>
434+
</Tooltip>
435+
))}
436+
</div>
437+
)}
438+
</div>
388439
</div>
389440
</div>
390441
);

0 commit comments

Comments
 (0)