Share playback state across views

This commit is contained in:
Codex
2026-04-23 11:13:34 -05:00
parent 226a50ea10
commit 0c24ba7111
2 changed files with 41 additions and 60 deletions

View File

@@ -7,17 +7,25 @@ type PlayClipArgs = {
url?: string | null;
startMs: number;
endMs: number;
title?: string;
subtitle?: string;
};
export type ClipPlaybackDetails = {
key: string;
title?: string;
subtitle?: string;
};
export function useClipPlayback() {
const audioRef = useRef<HTMLAudioElement | null>(null);
const audioContextRef = useRef<AudioContext | null>(null);
const mediaSourceRef = useRef<MediaElementAudioSourceNode | null>(null);
const gainNodeRef = useRef<GainNode | null>(null);
const playbackRangeRef = useRef<{ endSeconds: number } | null>(null);
const fadeOutTimerRef = useRef<number | null>(null);
const activeKeyRef = useRef<string | null>(null);
const [activeKey, setActiveKey] = useState<string | null>(null);
const [activeDetails, setActiveDetails] = useState<ClipPlaybackDetails | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTimeMs, setCurrentTimeMs] = useState<number | null>(null);
@@ -56,7 +64,6 @@ export function useClipPlayback() {
source.connect(gain);
gain.connect(context.destination);
audioContextRef.current = context;
mediaSourceRef.current = source;
gainNodeRef.current = gain;
}
}
@@ -100,6 +107,7 @@ export function useClipPlayback() {
activeKeyRef.current = null;
playbackRangeRef.current = null;
setActiveKey(null);
setActiveDetails(null);
setIsPlaying(false);
setCurrentTimeMs(null);
}
@@ -131,7 +139,7 @@ export function useClipPlayback() {
}, safeDuration);
}
async function playClip({ key, url, startMs, endMs }: PlayClipArgs) {
async function playClip({ key, url, startMs, endMs, title, subtitle }: PlayClipArgs) {
if (!url) {
return;
}
@@ -146,6 +154,11 @@ export function useClipPlayback() {
const nextAudio = ensureAudio();
activeKeyRef.current = key;
setActiveKey(key);
setActiveDetails({
key,
title,
subtitle,
});
setIsPlaying(false);
nextAudio.pause();
setPlaybackGain(1);
@@ -183,6 +196,7 @@ export function useClipPlayback() {
return {
activeKey,
activeDetails,
currentTimeMs,
fadeOutClip,
isPlaying,

View File

@@ -23,12 +23,6 @@ function clipKey(kind: "assignment" | "library", id: number | string): string {
return `${kind}:${id}`;
}
type NowPlaying = {
key: string;
title: string;
subtitle: string;
};
const DEFAULT_FADE_OUT_MS = 1000;
function getAvailabilityIconClass(statusCode: number | null | undefined): string {
@@ -65,12 +59,18 @@ export function GamedayPage() {
const [expandedPlayerId, setExpandedPlayerId] = useState("");
const [playerFilter, setPlayerFilter] = useState<"players" | "nonPlayers" | "all">("players");
const [playerFilterMenuOpen, setPlayerFilterMenuOpen] = useState(false);
const [nowPlaying, setNowPlaying] = useState<NowPlaying | null>(null);
const selectedPlayerWasManualRef = useRef(false);
const playerFilterMenuRef = useRef<HTMLDivElement | null>(null);
const hasInitializedExpandedPlayerRef = useRef(false);
const teamId = walkup.selectedTeamId;
const { activeKey: playingClipKey, fadeOutClip, isPlaying: isPlaybackPlaying, playClip: playClipPreview, stopClip } = useClipPlayback();
const {
activeKey: playingClipKey,
activeDetails: nowPlaying,
fadeOutClip,
isPlaying: isPlaybackPlaying,
playClip: playClipPreview,
stopClip,
} = useClipPlayback();
useEffect(() => {
const requestedGameId = searchParams.get("gameId");
@@ -84,14 +84,14 @@ export function GamedayPage() {
}, [searchParams, selectedGameId, walkup.nextGame]);
useEffect(() => {
stopPlayback();
stopClip();
setExpandedPlayerId("");
hasInitializedExpandedPlayerRef.current = false;
}, [selectedGameId]);
}, [selectedGameId, stopClip]);
useEffect(() => {
stopPlayback();
}, [selectedPlayerId]);
stopClip();
}, [selectedPlayerId, stopClip]);
useEffect(() => {
if (!playerFilterMenuOpen) {
@@ -214,56 +214,23 @@ export function GamedayPage() {
setSelectedGameId(gameId);
setSelectedPlayerId("");
setExpandedPlayerId("");
stopPlayback();
setSearchParams({ gameId });
}
function stopPlayback() {
stopClip();
setNowPlaying(null);
setSearchParams({ gameId });
}
function fadeOutPlayback(durationMs = DEFAULT_FADE_OUT_MS) {
fadeOutClip(durationMs);
}
async function playAudio(
url: string | null | undefined,
key: string,
playingItem: NowPlaying,
startMs: number,
endMs: number,
) {
if (!url) {
return;
}
if (playingClipKey === key && isPlaybackPlaying) {
stopPlayback();
return;
}
setNowPlaying(playingItem);
try {
await playClipPreview({ key, url, startMs, endMs });
} catch (error) {
stopPlayback();
throw error;
}
}
async function playClip(clip: AudioClip) {
await playAudio(
clip.normalized_url,
clipKey("library", clip.id),
{
key: clipKey("library", clip.id),
title: clip.label,
subtitle: formatMemberName(selectedPlayer),
},
clip.start_ms,
clip.end_ms,
);
await playClipPreview({
key: clipKey("library", clip.id),
url: clip.normalized_url,
startMs: clip.start_ms,
endMs: clip.end_ms,
title: clip.label,
subtitle: formatMemberName(selectedPlayer),
});
}
if (!walkup.isTeamSnap) {
@@ -280,11 +247,11 @@ export function GamedayPage() {
<div className="gameday-toolbar">
<div className="gameday-toolbar-copy">
<span className="gameday-toolbar-label">Now Playing</span>
<strong>{nowPlaying.title}</strong>
<span className="muted">{nowPlaying.subtitle}</span>
<strong>{nowPlaying.title ?? "Clip"}</strong>
{nowPlaying.subtitle ? <span className="muted">{nowPlaying.subtitle}</span> : null}
</div>
<div className="gameday-toolbar-actions">
<button type="button" className="btn btn-danger btn-sm" onClick={() => stopPlayback()}>
<button type="button" className="btn btn-danger btn-sm" onClick={() => stopClip()}>
Stop
</button>
<button