Implement WebRTC URL resolution and update stream playback logic across components and API routes

This commit is contained in:
Nikita
2025-12-23 11:32:08 -08:00
parent f45c28f941
commit 93bd99f9ed
10 changed files with 133 additions and 30 deletions

View File

@@ -22,7 +22,7 @@ if [ -z "$STREAM_KEY" ]; then
fi fi
exec ffmpeg -f avfoundation -capture_cursor 1 -framerate 60 -i "2:1" \ exec ffmpeg -f avfoundation -capture_cursor 1 -framerate 60 -i "2:1" \
-c:v h264_videotoolbox -b:v 30000k -maxrate 45000k -bufsize 90000k \ -c:v h264_videotoolbox -b:v 50000k -maxrate 100000k -bufsize 200000k \
-profile:v high -pix_fmt yuv420p \ -profile:v high -pix_fmt yuv420p \
-g 120 -keyint_min 120 \ -g 120 -keyint_min 120 \
-c:a aac -b:a 256k -ar 48000 -ac 2 \ -c:a aac -b:a 256k -ar 48000 -ac 2 \

View File

@@ -388,13 +388,13 @@ class HardwareEncoder {
let baseWidth = 2560 let baseWidth = 2560
let baseHeight = 1440 let baseHeight = 1440
let baseFrameRate = 60 let baseFrameRate = 60
let baseBitrate = 30_000_000 let baseBitrate = 50_000_000
let pixels = max(1, width) * max(1, height) let pixels = max(1, width) * max(1, height)
let basePixels = baseWidth * baseHeight let basePixels = baseWidth * baseHeight
let fpsScale = Double(max(frameRate, 1)) / Double(baseFrameRate) let fpsScale = Double(max(frameRate, 1)) / Double(baseFrameRate)
let raw = Double(baseBitrate) * (Double(pixels) / Double(basePixels)) * fpsScale let raw = Double(baseBitrate) * (Double(pixels) / Double(basePixels)) * fpsScale
return min(max(Int(raw.rounded()), 12_000_000), 80_000_000) return min(max(Int(raw.rounded()), 12_000_000), 120_000_000)
} }
deinit { deinit {

View File

@@ -0,0 +1,2 @@
ALTER TABLE "streams" ADD COLUMN "webrtc_url" text;
--> statement-breakpoint

View File

@@ -22,6 +22,13 @@
"when": 1765916542205, "when": 1765916542205,
"tag": "0002_uneven_the_renegades", "tag": "0002_uneven_the_renegades",
"breakpoints": true "breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1766456542436,
"tag": "0003_add_webrtc_url_to_streams",
"breakpoints": true
} }
] ]
} }

View File

@@ -209,6 +209,7 @@ export function WebRTCPlayer({
method: "POST", method: "POST",
headers: { headers: {
"content-type": "application/sdp", "content-type": "application/sdp",
accept: "application/sdp",
}, },
body: localSdp, body: localSdp,
signal: abortController.signal, signal: abortController.signal,

View File

@@ -13,6 +13,7 @@ type PlaybackInput = {
webrtcUrl?: string | null webrtcUrl?: string | null
cloudflareUid?: string | null cloudflareUid?: string | null
cloudflareCustomerCode?: string | null cloudflareCustomerCode?: string | null
preferWebRtc?: boolean
} }
export function resolveStreamPlayback({ export function resolveStreamPlayback({
@@ -20,24 +21,25 @@ export function resolveStreamPlayback({
webrtcUrl, webrtcUrl,
cloudflareUid, cloudflareUid,
cloudflareCustomerCode, cloudflareCustomerCode,
preferWebRtc = true,
}: PlaybackInput): StreamPlayback | null { }: PlaybackInput): StreamPlayback | null {
if (webrtcUrl) { const cloudflare = resolveCloudflareStreamRef({
return { type: "webrtc", url: webrtcUrl } hlsUrl,
cloudflareUid,
cloudflareCustomerCode,
})
const resolvedWebRtcUrl = preferWebRtc
? resolveWebRtcUrl({
webrtcUrl,
cloudflare,
})
: null
if (resolvedWebRtcUrl) {
return { type: "webrtc", url: resolvedWebRtcUrl }
} }
if (cloudflareUid) {
return {
type: "cloudflare",
uid: cloudflareUid,
customerCode: cloudflareCustomerCode ?? undefined,
}
}
if (!hlsUrl) {
return null
}
const cloudflare = parseCloudflareStreamUrl(hlsUrl)
if (cloudflare) { if (cloudflare) {
return { return {
type: "cloudflare", type: "cloudflare",
@@ -46,6 +48,10 @@ export function resolveStreamPlayback({
} }
} }
if (!hlsUrl) {
return null
}
return { type: "hls", url: hlsUrl } return { type: "hls", url: hlsUrl }
} }
@@ -82,3 +88,55 @@ export function parseCloudflareStreamUrl(url: string): CloudflareStreamRef | nul
return { uid, customerCode } return { uid, customerCode }
} }
type CloudflareResolveInput = {
hlsUrl?: string | null
cloudflareUid?: string | null
cloudflareCustomerCode?: string | null
}
export function resolveCloudflareStreamRef({
hlsUrl,
cloudflareUid,
cloudflareCustomerCode,
}: CloudflareResolveInput): CloudflareStreamRef | null {
if (cloudflareUid) {
return {
uid: cloudflareUid,
customerCode: cloudflareCustomerCode ?? undefined,
}
}
if (!hlsUrl) {
return null
}
return parseCloudflareStreamUrl(hlsUrl)
}
export function buildCloudflareWhepUrl(ref: CloudflareStreamRef): string {
if (ref.customerCode) {
return `https://customer-${ref.customerCode}.cloudflarestream.com/${ref.uid}/whep`
}
return `https://videodelivery.net/${ref.uid}/whep`
}
type WebRtcResolveInput = {
webrtcUrl?: string | null
cloudflare: CloudflareStreamRef | null
}
export function resolveWebRtcUrl({
webrtcUrl,
cloudflare,
}: WebRtcResolveInput): string | null {
if (webrtcUrl) {
return webrtcUrl
}
if (!cloudflare) {
return null
}
return buildCloudflareWhepUrl(cloudflare)
}

View File

@@ -111,7 +111,11 @@ function StreamPage() {
const stream = data?.stream ?? null const stream = data?.stream ?? null
const playback = stream?.playback ?? null const playback = stream?.playback ?? null
const fallbackPlayback = stream?.hls_url const fallbackPlayback = stream?.hls_url
? resolveStreamPlayback({ hlsUrl: stream.hls_url, webrtcUrl: null }) ? resolveStreamPlayback({
hlsUrl: stream.hls_url,
webrtcUrl: null,
preferWebRtc: false,
})
: null : null
const activePlayback = const activePlayback =
playback?.type === "webrtc" && webRtcFailed playback?.type === "webrtc" && webRtcFailed
@@ -179,7 +183,6 @@ function StreamPage() {
) )
} }
const { user } = data
const showPlayer = const showPlayer =
activePlayback?.type === "cloudflare" || activePlayback?.type === "cloudflare" ||
activePlayback?.type === "webrtc" || activePlayback?.type === "webrtc" ||

View File

@@ -4,7 +4,11 @@ import { getDb } from "@/db/connection"
import { users, streams } from "@/db/schema" import { users, streams } from "@/db/schema"
import { getAuth } from "@/lib/auth" import { getAuth } from "@/lib/auth"
import { randomUUID } from "crypto" import { randomUUID } from "crypto"
import { resolveStreamPlayback } from "@/lib/stream/playback" import {
resolveCloudflareStreamRef,
resolveStreamPlayback,
resolveWebRtcUrl,
} from "@/lib/stream/playback"
const resolveDatabaseUrl = (request: Request) => { const resolveDatabaseUrl = (request: Request) => {
try { try {
@@ -49,6 +53,12 @@ const getProfile = async ({ request }: { request: Request }) => {
where: eq(streams.user_id, user.id), where: eq(streams.user_id, user.id),
}) })
const cloudflare = stream
? resolveCloudflareStreamRef({ hlsUrl: stream.hls_url })
: null
const webRtcUrl = stream
? resolveWebRtcUrl({ webrtcUrl: stream.webrtc_url, cloudflare })
: null
const playback = stream const playback = stream
? resolveStreamPlayback({ ? resolveStreamPlayback({
hlsUrl: stream.hls_url, hlsUrl: stream.hls_url,
@@ -69,7 +79,7 @@ const getProfile = async ({ request }: { request: Request }) => {
title: stream.title, title: stream.title,
is_live: stream.is_live, is_live: stream.is_live,
hls_url: stream.hls_url, hls_url: stream.hls_url,
webrtc_url: stream.webrtc_url, webrtc_url: webRtcUrl,
playback, playback,
stream_key: stream.stream_key, stream_key: stream.stream_key,
} }

View File

@@ -3,7 +3,11 @@ import { eq } from "drizzle-orm"
import { getDb } from "@/db/connection" import { getDb } from "@/db/connection"
import { streams } from "@/db/schema" import { streams } from "@/db/schema"
import { getAuth } from "@/lib/auth" import { getAuth } from "@/lib/auth"
import { resolveStreamPlayback } from "@/lib/stream/playback" import {
resolveCloudflareStreamRef,
resolveStreamPlayback,
resolveWebRtcUrl,
} from "@/lib/stream/playback"
const resolveDatabaseUrl = (request: Request) => { const resolveDatabaseUrl = (request: Request) => {
try { try {
@@ -43,15 +47,23 @@ const getStream = async ({ request }: { request: Request }) => {
}) })
} }
const cloudflare = resolveCloudflareStreamRef({ hlsUrl: stream.hls_url })
const webRtcUrl = resolveWebRtcUrl({
webrtcUrl: stream.webrtc_url,
cloudflare,
})
const playback = resolveStreamPlayback({ const playback = resolveStreamPlayback({
hlsUrl: stream.hls_url, hlsUrl: stream.hls_url,
webrtcUrl: stream.webrtc_url, webrtcUrl: stream.webrtc_url,
}) })
return new Response(JSON.stringify({ ...stream, playback }), { return new Response(
status: 200, JSON.stringify({ ...stream, webrtc_url: webRtcUrl, playback }),
headers: { "content-type": "application/json" }, {
}) status: 200,
headers: { "content-type": "application/json" },
}
)
} catch (error) { } catch (error) {
console.error("Stream GET error:", error) console.error("Stream GET error:", error)
return new Response(JSON.stringify({ error: "Internal server error" }), { return new Response(JSON.stringify({ error: "Internal server error" }), {

View File

@@ -2,7 +2,11 @@ import { createFileRoute } from "@tanstack/react-router"
import { eq } from "drizzle-orm" import { eq } from "drizzle-orm"
import { getDb } from "@/db/connection" import { getDb } from "@/db/connection"
import { users, streams } from "@/db/schema" import { users, streams } from "@/db/schema"
import { resolveStreamPlayback } from "@/lib/stream/playback" import {
resolveCloudflareStreamRef,
resolveStreamPlayback,
resolveWebRtcUrl,
} from "@/lib/stream/playback"
const resolveDatabaseUrl = (request: Request) => { const resolveDatabaseUrl = (request: Request) => {
try { try {
@@ -59,6 +63,12 @@ const serve = async ({
where: eq(streams.user_id, user.id), where: eq(streams.user_id, user.id),
}) })
const cloudflare = stream
? resolveCloudflareStreamRef({ hlsUrl: stream.hls_url })
: null
const webRtcUrl = stream
? resolveWebRtcUrl({ webrtcUrl: stream.webrtc_url, cloudflare })
: null
const playback = stream const playback = stream
? resolveStreamPlayback({ ? resolveStreamPlayback({
hlsUrl: stream.hls_url, hlsUrl: stream.hls_url,
@@ -81,7 +91,7 @@ const serve = async ({
is_live: stream.is_live, is_live: stream.is_live,
viewer_count: stream.viewer_count, viewer_count: stream.viewer_count,
hls_url: stream.hls_url, hls_url: stream.hls_url,
webrtc_url: stream.webrtc_url, webrtc_url: webRtcUrl,
playback, playback,
thumbnail_url: stream.thumbnail_url, thumbnail_url: stream.thumbnail_url,
started_at: stream.started_at?.toISOString() ?? null, started_at: stream.started_at?.toISOString() ?? null,