mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-11 14:30:26 +01:00
Implement WebRTC URL resolution and update stream playback logic across components and API routes
This commit is contained in:
@@ -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 \
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
2
packages/web/drizzle/0003_add_webrtc_url_to_streams.sql
Normal file
2
packages/web/drizzle/0003_add_webrtc_url_to_streams.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "streams" ADD COLUMN "webrtc_url" text;
|
||||||
|
--> statement-breakpoint
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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" ||
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" }), {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user