@@ -55,7 +64,9 @@ export function TextVideoBlock({ content, onUpdate, blockId, readOnly = false })
].join(" ")}>
setPickerOpen(true)}
@@ -101,6 +112,7 @@ export function TextVideoBlock({ content, onUpdate, blockId, readOnly = false })
onSelect={(asset) => onUpdate({
...content,
asset_id: asset.asset_id,
+ storage_provider: asset.storage_provider ?? null,
url: asset.file_url,
thumbnail_url: asset.thumbnail_url ?? null,
})}
diff --git a/src/components/generic/Blocks/Admin/VideoBlock.jsx b/src/components/generic/Blocks/Admin/VideoBlock.jsx
index f0e2d89..cae1d1d 100644
--- a/src/components/generic/Blocks/Admin/VideoBlock.jsx
+++ b/src/components/generic/Blocks/Admin/VideoBlock.jsx
@@ -10,6 +10,8 @@ import {
} from "lucide-react";
import { Label } from "@/components/ui/label";
import { AssetPickerSheet } from "../../AssetPickerSheet";
+import { useAssetPreviewSrc } from "@/hooks/useAssetPreviewSrc";
+import { MediaFallback } from "@/components/generic/MediaFallback";
// ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -25,6 +27,15 @@ const fmtTime = (s) => {
export function VideoBlock({ content, onUpdate, readOnly = false }) {
const [pickerOpen, setPickerOpen] = useState(false);
+ // content.url is redacted (null) server-side for S3 assets — see
+ // redactS3Url() in controllers/admin/assets.controller.js. Re-resolve
+ // through media.util.js instead of trusting the persisted url/thumbnail.
+ const { src, thumbnailUrl, loading } = useAssetPreviewSrc(
+ { asset_id: content?.asset_id, storage_provider: content?.storage_provider, file_url: content?.url, thumbnail_url: content?.thumbnail_url },
+ { scope: "admin" },
+ );
+ const poster = thumbnailUrl ?? content?.thumbnail_url ?? undefined;
+
const vidRef = useRef(null);
const [playing, setPlaying] = useState(false);
const [progress, setProgress] = useState(0);
@@ -41,7 +52,7 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
setCurrentTime(0);
setTotalDuration(0);
setOverlayVisible(true);
- }, [content.url]);
+ }, [src]);
// ── Video event listeners ─────────────────────────────────────────────────
@@ -65,7 +76,7 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
v.removeEventListener("loadedmetadata", onLoaded);
v.removeEventListener("ended", onEnded);
};
- }, [content.url]);
+ }, [src]);
// ── Controls ──────────────────────────────────────────────────────────────
@@ -126,11 +137,12 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
//
const handleSelect = (asset) => {
onUpdate({
- asset_id: asset.asset_id,
- url: asset.file_url,
- thumbnail_url: asset.thumbnail_url ?? null,
- title: asset.display_name,
- tag: asset.extension?.toUpperCase() ?? "",
+ asset_id: asset.asset_id,
+ storage_provider: asset.storage_provider ?? null,
+ url: asset.file_url,
+ thumbnail_url: asset.thumbnail_url ?? null,
+ title: asset.display_name,
+ tag: asset.extension?.toUpperCase() ?? "",
});
setPlaying(false);
setProgress(0);
@@ -145,7 +157,9 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
{!readOnly &&
}
- {content.url ? (
+ {loading ? (
+
+ ) : src ? (
{/* ── Video area ── */}
@@ -157,8 +171,8 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
>
;
+ }
+
+ if (!src) {
return (
@@ -11,5 +28,5 @@ export function ImageBlock({ content }) {
);
}
- return ;
-}
\ No newline at end of file
+ return ;
+}
diff --git a/src/components/generic/Blocks/Client/VideoBlock.jsx b/src/components/generic/Blocks/Client/VideoBlock.jsx
index c76ac2a..c24f9ec 100644
--- a/src/components/generic/Blocks/Client/VideoBlock.jsx
+++ b/src/components/generic/Blocks/Client/VideoBlock.jsx
@@ -165,6 +165,7 @@ export function VideoBlock({ content }) {
// ── Stream state ──────────────────────────────────────────────────────────
const [blobUrl, setBlobUrl] = useState(null);
+ const [freshThumb, setFreshThumb] = useState(null);
const [fetchLoading, setFetchLoading] = useState(false);
const [fetchError, setFetchError] = useState(false);
@@ -193,7 +194,11 @@ export function VideoBlock({ content }) {
const assetId = content?.asset_id;
const storageProvider = content?.storage_provider;
- const poster = content?.thumbnail_url ?? undefined;
+ // content.thumbnail_url is a presigned S3 URL captured at CMS pick time —
+ // it expires. For S3 assets, use the fresh thumbnail_url minted alongside
+ // the stream token below instead; only fall back to the persisted value
+ // for chibisafe (stable public CDN URL).
+ const poster = (storageProvider === "s3" ? freshThumb : content?.thumbnail_url) ?? undefined;
// ── Resolve stream URL → set as video src directly ───────────────────────
//
@@ -206,6 +211,7 @@ export function VideoBlock({ content }) {
if (!assetId) return;
setBlobUrl(null);
+ setFreshThumb(null);
setFetchError(false);
setPlaying(false);
setProgress(0);
@@ -231,10 +237,13 @@ export function VideoBlock({ content }) {
// S3 — get token then stream directly; no blob download
const { data } = await api.post("/client/media/token", { asset_id: assetId });
if (cancelled) return;
- const { token } = data?.data ?? {};
+ const { token, thumbnail_url } = data?.data ?? {};
if (!token) throw new Error("No token returned");
- if (!cancelled) setBlobUrl(`${API_BASE}/client/media/stream/${token}`);
+ if (!cancelled) {
+ setBlobUrl(`${API_BASE}/client/media/stream/${token}`);
+ if (thumbnail_url) setFreshThumb(thumbnail_url);
+ }
} catch (err) {
if (cancelled) return;