Media
Paging galleries, signed URLs, and telling processing apart from a signing failure
Media
Events and artifacts both carry an original file plus a thumbnail and a proxy derivative. Storage is private, so every path is signed per request, and a gallery has to distinguish media that is still being produced from media that exists but could not be signed.
Gallery pages
useInfiniteGalleryByHerd and useInfiniteGalleryByDevice page a merged
events-and-artifacts list through the lean gallery_item projection: media
paths, processing state, location, and artifact duration_seconds instead of
full tag arrays and audit columns. Duration is null for events and artifacts
without an observation start. Prefer these hooks
unless a view renders full tags or audit fields.
import { useInfiniteGalleryByHerd } from "@adventurelabs/scout-core/client";
const { items, hasMore, loadMore, isLoading } = useInfiniteGalleryByHerd(herdId, {
supabase,
enabled: true,
limit: 30,
});Every hook here shares the InfiniteScrollData contract: items, hasMore,
loadMore, refetch, and the loading flags. The RPCs over-fetch by one row, so
an exactly-full last page ends the list without a further request.
A refetch re-signs the rows it already holds. Rows that changed only by a
re-minted URL keep their object identity — safe to memoize on — while the new
URLs are copied in, including the ones nested under a feed item's
event_data/artifact_data.
Readiness
media_status on a gallery item says whether the media a tile wants is
available. A null signed URL is not proof of processing, so a derivative that
exists but went unsigned is reported apart from one that was never produced:
| Status | Meaning |
|---|---|
ready | The derivative exists and is signed. |
processing | No derivative yet, and processing is not blocked. |
blocked | processing_blocked_at is set and nothing was produced. Read processing_blocked_reason. |
url_unavailable | The derivative exists but signing failed. Retrying is worthwhile. |
Events report processing_blocked_at and processing_blocked_reason the way
artifacts always have, on the event composite and therefore on feed rows too, so
a permanently blocked event no longer looks like one still in the queue.
mediaStatus and originalMediaStatus compute the same states for any hydrated
event, artifact, or feed row.
Classification
classifyMedia is the one classifier for image versus video: the file extension
decides when it is recognized — .mp4, .m4v, .webm, .mov, .mkv, .avi
and the image extensions — and otherwise the stored media_type or artifact
modality does, which is what keeps extensionless legacy uploads playing as
video.
The extension list is mirrored by private.is_video_file_path and
private.is_image_file_path, which the enqueue triggers use to decide what gets
a derivative. Keeping the two in step is what makes media_status truthful: a
container the client calls video but the pipeline skips would sit in
processing forever.
import { classifyMedia, isVideoMedia } from "@adventurelabs/scout-core/client";
classifyMedia({ file_path: "events/12/clip.m4v" }); // "video"
classifyMedia({ file_path: "events/12/legacy", media_type: "video" }); // "video"
isVideoMedia(artifact);Hydrating a set of ids
Semantic search returns ids and confidences, so get_gallery_items_by_ids
hydrates a whole result set in one read and one signing pass per bucket. Items
come back in the order their ids were requested, which preserves the ranking:
import {
get_gallery_items_by_ids,
match_events_by_vertex_embedding,
} from "@adventurelabs/scout-core/client";
const matches = await match_events_by_vertex_embedding(supabase, embedding, 0.2, 20);
const items = await get_gallery_items_by_ids(supabase, {
eventIds: matches.data?.map((match) => match.id) ?? [],
});Repeated ids are queried once. Omitted arrays return an empty successful
response, while non-positive or unsafe integer ids return an error response.
For mixed event/artifact results, pass rankedIds as { feedType, id } entries;
those ids are fetched automatically and their interleaved order is preserved.
Missing, inactive, pathless, and log-only rows are omitted, so the successful
result can be shorter than the request.
Signing is batched by storage bucket, so a page of originals, thumbnails, and proxies costs a handful of requests rather than one per path. A bucket whose request fails, or a single path that cannot be signed, leaves that URL null and the rest of the page intact.