Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Windows Tipps & SecurityGetting Repeated No Caller ID Calls? Here’s What’s Really Going On(22.09.2026 um 22:31 Uhr)
Windows Tipps & SecurityHöllenmaschine: Gaming-Peripherie für gut 1.800 Euro für die HMX 6(23.09.2026 um 10:20 Uhr)
Windows Tipps & SecurityDas nächste große Ding: KI-Agenten(23.09.2026 um 10:30 Uhr)
Sichere ProgrammierungHow AI Is Making Restaurant Menus Easier to Navigate(23.09.2026 um 10:55 Uhr)
Windows Tipps & SecurityGetting Repeated No Caller ID Calls? Here’s What’s Really Going On(22.09.2026 um 22:31 Uhr)
Windows Tipps & SecurityHöllenmaschine: Gaming-Peripherie für gut 1.800 Euro für die HMX 6(23.09.2026 um 10:20 Uhr)
Windows Tipps & SecurityDas nächste große Ding: KI-Agenten(23.09.2026 um 10:30 Uhr)
Sichere ProgrammierungHow AI Is Making Restaurant Menus Easier to Navigate(23.09.2026 um 10:55 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

How I stream Mux video from Sanity to Next.js with signed HLS playback

Sanity Mux video playback in Next.js is where most tutorials stop short. The upload side — pushing a file to Mux via Sanity Studio — is well documented. What I rarely see covered is the read path: pulling a stored Mux asset reference out of…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

Sanity Mux video playback in Next.js is where most tutorials stop short. The upload side — pushing a file to Mux via Sanity Studio — is well documented. What I rarely see covered is the read path: pulling a stored Mux asset reference out of Sanity, generating a signed playback URL, initialising hls.js in a React component, and making sure the poster frame does not wreck your LCP score. This post covers exactly that.






What Sanity stores after a Mux upload



The sanity-plugin-mux-input plugin writes a mux.video asset object into your document. After a successful upload, your document will contain something like:




// GROQ — fetch just the fields the playback component needs
*[_type == "post" && slug.current == $slug][0] {
title,
"video": video.asset-> {
playbackId,
data {
duration,
aspect_ratio
},
"signingKeyId": @.data.signing_key_id
}
}






playbackId is the string you pass to https://stream.mux.com/{playbackId}.m3u8. If the asset was created with a signed playback policy (you toggled "Enable signed URLs" in the plugin settings or set mp4_support: "none" with playback_policy: ["signed"] via the Mux API), you cannot use that URL directly — Mux will return a 401. You need a short-lived JWT.



Sanity stores playbackId but not the signing key secret. That secret lives in an environment variable and never touches your CMS.






Generating a signed playback URL in a Route Handler



I generate tokens server-side in a Next.js Route Handler so the signing secret is never exposed to the browser.




// app/api/mux-token/route.ts
import { NextRequest, NextResponse } from "next/server";
import { SignJWT } from "jose";

const KEY_ID = process.env.MUX_SIGNING_KEY_ID!;
// Base64-encoded private key from Mux dashboard
const KEY_SECRET = Buffer.from(
process.env.MUX_SIGNING_KEY_PRIVATE_KEY_BASE64!,
"base64"
);

export async function GET(req: NextRequest) {
const playbackId = req.nextUrl.searchParams.get("playbackId");
if (!playbackId) {
return NextResponse.json({ error: "missing playbackId" }, { status: 400 });
}

// Mux signed tokens use RS256 with a PKCS#8 private key
const privateKey = await crypto.subtle.importKey(
"pkcs8",
KEY_SECRET,
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
false,
["sign"]
);

const token = await new SignJWT({})
.setProtectedHeader({ alg: "RS256", kid: KEY_ID })
.setAudience("v")
.setSubject(playbackId)
.setExpirationTime("4h")
.sign(privateKey);

// Poster token needs a separate audience: "t" for thumbnail
const thumbToken = await new SignJWT({})
.setProtectedHeader({ alg: "RS256", kid: KEY_ID })
.setAudience("t")
.setSubject(playbackId)
.setExpirationTime("4h")
.sign(privateKey);

return NextResponse.json(
{ token, thumbToken },
{
headers: {
// Cache at the edge for 3 h; leave 1 h buffer before expiry
"Cache-Control": "public, max-age=10800, s-maxage=10800",
},
}
);
}






A few notes: jose runs in the Edge runtime without a Node.js polyfill. The audience field ("v" for video, "t" for thumbnails, "s" for storyboards) is required by Mux — getting it wrong is the most common 401 I see from developers copying JWT examples from unrelated docs.






The playback component with hls.js and LCP-safe poster



This is a client component. It fetches the tokens on mount, then initialises hls.js only when the browser cannot play HLS natively (Safari can; Chrome cannot).




// components/mux-player.tsx
"use client";

import { useEffect, useRef, useState } from "react";
import Hls from "hls.js";

interface MuxPlayerProps {
playbackId: string;
aspectRatio?: string; // e.g. "16:9"
title: string;
}

export function MuxPlayer({
playbackId,
aspectRatio = "16:9",
title,
}: MuxPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const [tokens, setTokens] = useState<{
token: string;
thumbToken: string;
} | null>(null);

// Convert "16:9" to a padding-bottom percentage for CLS-safe aspect ratio
const [w, h] = aspectRatio.split(":").map(Number);
const paddingBottom = `${((h / w) * 100).toFixed(4)}%`;

useEffect(() => {
fetch(`/api/mux-token?playbackId=${playbackId}`)
.then((r) => r.json())
.then(setTokens);
}, [playbackId]);

useEffect(() => {
if (!tokens || !videoRef.current) return;

const src = `https://stream.mux.com/${playbackId}.m3u8?token=${tokens.token}`;
const video = videoRef.current;

if (video.canPlayType("application/vnd.apple.mpegurl")) {
// Safari native HLS
video.src = src;
} else if (Hls.isSupported()) {
const hls = new Hls({
// Start with lowest quality on mobile connections
startLevel: -1,
// Reduce initial buffer to get first frame faster
maxBufferLength: 30,
});
hls.loadSource(src);
hls.attachMedia(video);
return () => hls.destroy();
}
}, [tokens, playbackId]);

const posterSrc = tokens
? `https://image.mux.com/${playbackId}/thumbnail.webp?token=${tokens.thumbToken}&time=0&width=1280`
: undefined;

return (
// Padding-bottom trick keeps the aspect ratio slot reserved before video loads
// — eliminates CLS when the poster image has not yet arrived
<div
style={{ position: "relative", paddingBottom, height: 0, overflow: "hidden" }}
>
<video
ref={videoRef}
poster={posterSrc}
controls
playsInline
aria-label={title}
style={{
position: "absolute",
inset: 0,
width: "100%",
height: "100%",
}}
/>
</div>
);
}









LCP: the poster image is your real target



If a video is above the fold, the browser picks the poster frame as the LCP candidate. Two things hurt LCP here.



First, the poster URL is only available after the client fetches the token, which means it is blank on first render. Fix: on the server-side page component, fetch the tokens once during SSR and pass posterSrc as a prop so the <video poster> attribute is present in the initial HTML. You can do this with fetch inside a React Server Component, writing the result to a short-lived cache entry, then passing the resolved poster URL as a prop to the client component. The token fetch is already cached for 3 hours at the edge so the RSC does not add latency per request.



Second, the image itself — Mux thumbnail URLs return WebP when you append &width=1280. Use <link rel="preload" as="image"> in your page <head> via Next.js metadata's other field, or via generateMetadata, to push the poster to the browser before it parses the video element:




// app/posts/[slug]/page.tsx (excerpt)
export async function generateMetadata({ params }: Props) {
const { playbackId, thumbToken } = await getVideoTokens(params.slug);
return {
other: {
// Preloads the poster so it is ready when the <video> element renders
"link-preload-poster": `<link rel="preload" as="image" href="https://image.mux.com/${playbackId}/thumbnail.webp?token=${thumbToken}&width=1280">`,
},
};
}






This is admittedly a rough edge in Next.js metadata — arbitrary <link> tags in <head> require the other map or a custom <Head> in a layout. The preload alone typically moves poster LCP from ~2.8 s to under 1 s on a fast 4G connection in my testing.






Aspect ratio and CLS



Mux stores aspect_ratio as a string like "16:9" in the asset data. Pull it out of your GROQ projection (shown above) and pass it straight to the component. The padding-bottom wrapper reserves the correct height before the video element loads, which eliminates layout shift. Do not skip this step — a video element with no intrinsic size is one of the most common CLS offenders I find in audits.



If aspect_ratio is missing (older assets, external ingest), fall back to "16:9" and add a Sanity validation rule that enforces the field is populated before the document can be published.

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten How I stream Mux video from Sanity to Next.js with signed HLS playback

Thematisch verwandte Begriffe: stream, video, from, Sanity · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-96258 | A vulnerability has been found in onSite internet GmbH Auktion NG Auktio…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick