diff --git a/src/utils/timerange.js b/src/utils/timerange.js index 48dfd07..9e374c1 100644 --- a/src/utils/timerange.js +++ b/src/utils/timerange.js @@ -226,3 +226,31 @@ export const parseTimerangeDateTime = (timerange) => { }; } }; + +/** + * Whether a timerange covers any media at all. + * + * A growing flow reports an open-ended range (`[start_`), so it has segments + * but no computable duration -- duration is therefore not a usable proxy for + * "has media". The bounds are read from the string rather than from + * parseTimerange, which collapses "empty", "inverted" and "did not parse" onto + * the same 0n..0n result. + */ +export const timerangeHasMedia = (timerange) => { + if (typeof timerange !== "string" || !timerange) return false; + if (EMPTY_PATTERNS.has(timerange)) return false; + + const match = TIMERANGE_REGEX.exec(timerange); + if (!match) return false; + + const { startSeconds, startNanos, endSeconds, endNanos } = match.groups; + const hasStart = startSeconds != null || startNanos != null; + const hasEnd = endSeconds != null || endNanos != null; + if (!hasStart && !hasEnd) return false; + + // A bare timestamp with no range separator is a single point, so zero-length. + if (!timerange.includes("_")) return false; + if (!hasStart || !hasEnd) return true; + + return !(startSeconds === endSeconds && startNanos === endNanos); +}; diff --git a/src/views/Embed/index.jsx b/src/views/Embed/index.jsx index 6d95fc6..8aa0284 100644 --- a/src/views/Embed/index.jsx +++ b/src/views/Embed/index.jsx @@ -5,7 +5,7 @@ import { useIngestingFlows } from "@/hooks/useFlows"; import { useCapabilities } from "@/hooks/useService"; import useStoreManager from "@/stores/useStoreManager"; import StoreManager from "@/views/StoreManager"; -import { parseTimerange } from "@/utils/timerange"; +import { parseTimerange, timerangeHasMedia } from "@/utils/timerange"; import { isFlowGrowing } from "@/utils/flowStatus"; import { findVideoMember } from "@/utils/editorialPurpose"; import { TAMS_PAGE_LIMIT, VIEW_MODE } from "@/constants"; @@ -113,8 +113,8 @@ const resolveVideoSourceId = async (source, api, canQueryCollections) => { }; /** - * For each displayed source, fetch its video flow to get fps, duration and - * growing status. Growing status comes from the flow's `status` (8.2) or the + * For each displayed source, fetch its video flow to get fps, duration, + * growing status, and whether any segments have landed yet. Growing status comes from the flow's `status` (8.2) or the * deprecated flow_status tag, guarded by recent segment activity. * * On initial load, all sources are fetched once. Sources that are growing are @@ -166,6 +166,7 @@ const useSourceFlowDetails = (displayedSources, liveSourceIds) => { let durationMs = null; const detailRes = await api.get(`/flows/${flow.id}?include_timerange=true`); const tr = detailRes.data?.timerange; + const hasSegments = timerangeHasMedia(tr); if (tr) { const parsed = parseTimerange(tr); if (parsed.start !== null && parsed.end !== null) { @@ -183,7 +184,7 @@ const useSourceFlowDetails = (displayedSources, liveSourceIds) => { if (!cancelled) { setFlowDetails((prev) => { const next = new Map(prev); - next.set(source.id, { fps, isGrowing, durationMs }); + next.set(source.id, { fps, isGrowing, durationMs, hasSegments }); return next; }); } @@ -216,6 +217,7 @@ const useSourceFlowDetails = (displayedSources, liveSourceIds) => { let durationMs = null; const tr = flow.timerange; + const hasSegments = timerangeHasMedia(tr); if (tr) { const parsed = parseTimerange(tr); if (parsed.start !== null && parsed.end !== null) { @@ -231,7 +233,7 @@ const useSourceFlowDetails = (displayedSources, liveSourceIds) => { setFlowDetails((prev) => { const next = new Map(prev); const existing = next.get(sourceId); - next.set(sourceId, { ...existing, isGrowing, durationMs }); + next.set(sourceId, { ...existing, isGrowing, durationMs, hasSegments }); return next; }); } catch { /* skip */ } @@ -315,6 +317,7 @@ const Embed = () => { ...source, isGrowing: details?.isGrowing ?? false, durationMs: details?.durationMs ?? null, + hasSegments: details?.hasSegments ?? false, fps: details?.fps ?? null, detailsLoaded: !!details, }; @@ -426,7 +429,7 @@ const Embed = () => { ) : source.durationMs > 0 ? ( {formatDuration(source.durationMs)} ) : null} - {source.durationMs > 0 && ( + {source.hasSegments && (