Share playback state across views
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user