useRecordingPlayback.ts3.1 KBView on GitHub import { useQuery } from '@tanstack/react-query';
import { useCallback, useState } from 'react';
import { useTRPC } from '@/providers/query-provider';
/** How often to re-ask while a provider is still rendering its file. */
const PROCESSING_POLL_MS = 5_000;
/**
* A minted URL outlives a single view, so the answer is worth holding. Circleback signs for
* 24h, Fathom's render survives ~24h, Gong signs for 8h — ten minutes is comfortably inside
* all three and still short enough that a stale answer self-heals within one sitting.
*/
const STALE_TIME_MS = 10 * 60 * 1000;
export type RecordingPlaybackStatus =
| 'idle'
| 'loading'
| 'ready'
| 'processing'
| 'unavailable'
| 'permission_required';
export interface RecordingPlayback {
/** Playable media URL. Non-null only once `status` is `ready`. */
recordingUrl: string | null;
/** A page to open at the provider, if it has one. Available before the media resolves. */
externalUrl: string | null;
/** Canonical provider id, once known — drives the provider-shaped fallbacks. */
provider: string | null;
status: RecordingPlaybackStatus;
/** Ask the server for a URL. Idempotent; the first call is what starts the request. */
request: () => void;
}
/**
* Resolve a meeting's playable recording, ON DEMAND.
*
* WHY IT DOES NOT FETCH ON MOUNT. A coaching document routinely holds five to ten moments
* from different calls, and every one of them is a separate mint. Fathom's download endpoint
* allows 30 requests per 60s per API key and renders a cold recording in ~34 seconds, so a
* document that minted eagerly would spend a third of that minute's budget before the reader
* had pressed anything, and would do it again on every scroll-back. Nothing is requested
* until `request()` is called — the player calls it on the reader's first play or seek.
*
* `status` distinguishes four unhappy answers because they need four different words in the
* UI: still rendering, no recording, not allowed to fetch it (Gong's media scope), and not
* asked yet. Collapsing any pair of them tells the reader something untrue.
*/
export function useRecordingPlayback(externalId: string | null | undefined): RecordingPlayback {
const trpc = useTRPC();
const [requested, setRequested] = useState(false);
const enabled = requested && !!externalId;
const { data, isLoading, isError } = useQuery({
...trpc.crm.getMeetingRecordingUrl.queryOptions({ externalId: externalId || '' }),
enabled,
staleTime: STALE_TIME_MS,
// A provider that renders on demand answers "processing" first. Keep asking until it
// resolves one way or the other; every other status is final for this view.
refetchInterval: (query) =>
query.state.data?.status === 'processing' ? PROCESSING_POLL_MS : false,
});
const request = useCallback(() => setRequested(true), []);
const status: RecordingPlaybackStatus = !enabled
? 'idle'
: isError
? 'unavailable'
: isLoading || !data
? 'loading'
: data.status;
return {
recordingUrl: data?.recordingUrl ?? null,
externalUrl: data?.externalUrl ?? null,
provider: data?.provider ?? null,
status,
request,
};
}