Adventure Labs Logo
Scout Docs

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.

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:

StatusMeaning
readyThe derivative exists and is signed.
processingNo derivative yet, and processing is not blocked.
blockedprocessing_blocked_at is set and nothing was produced. Read processing_blocked_reason.
url_unavailableThe 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.

On this page