From 6495fe098d3fae8308e45a86648379103c50bb09 Mon Sep 17 00:00:00 2001 From: Priveetee Date: Thu, 4 Jun 2026 16:32:22 +0200 Subject: [PATCH 1/3] feat: add high quality playback option --- apps/web/src/components/watch-layout.tsx | 74 ++++++++++++--------- apps/web/src/hooks/use-player-error.ts | 56 ++++++++++++++-- apps/web/src/hooks/use-settings.ts | 5 +- apps/web/src/lib/stream-src.ts | 23 +++++-- apps/web/src/settings/settings-playback.tsx | 28 ++++++++ apps/web/src/types/user.ts | 1 + 6 files changed, 141 insertions(+), 46 deletions(-) diff --git a/apps/web/src/components/watch-layout.tsx b/apps/web/src/components/watch-layout.tsx index 0766144..901d45c 100644 --- a/apps/web/src/components/watch-layout.tsx +++ b/apps/web/src/components/watch-layout.tsx @@ -33,13 +33,12 @@ type Props = { export function WatchLayout({ stream, startTime }: Props) { const navigate = useNavigate(); const save = useSaveProgress(stream.id); - const { settings, update, query: settingsQuery } = useSettings(); - const settingsReady = - (settingsQuery.isSuccess && !settingsQuery.isPlaceholderData) || settingsQuery.isError; + const { settings, update, settingsReady } = useSettings(); const isLive = stream.streamType === "live_stream" || stream.streamType === "audio_live_stream"; const { manifestSrc, playerFailed, qualityFailed, handleError, reset, retryKey } = usePlayerError( stream, isLive, + settings.enableHighQualityPlayback, ); const { on: bulletCommentsOn } = useDanmakuStore(); const isNicoNico = detectProvider(stream.id) === "nicovideo"; @@ -52,6 +51,7 @@ export function WatchLayout({ stream, startTime }: Props) { const [toast, setToast] = useState(null); const handleVolumeChange = useVolumeSync(update.mutate); const { thumbnailVtt, chaptersVtt } = useWatchVttAssets(stream); + const playerKey = `${stream.id}:${retryKey}:${settings.enableHighQualityPlayback ? "hq" : "std"}`; useEffect(() => { if (!toast) return; @@ -109,36 +109,44 @@ export function WatchLayout({ stream, startTime }: Props) {
- { - seekRef.current = s; - }} - className={cinemaMode ? "w-full h-full dark [--video-aspect-ratio:16/9]" : undefined} - mediaClassName={cinemaMode ? "object-cover" : undefined} - /> - {playerFailed && } + {settingsReady ? ( + <> + { + seekRef.current = s; + }} + className={ + cinemaMode ? "w-full h-full dark [--video-aspect-ratio:16/9]" : undefined + } + mediaClassName={cinemaMode ? "object-cover" : undefined} + /> + {playerFailed && } + + ) : ( +
+ )}
{!cinemaMode && ( seekRef.current?.(seconds)} /> diff --git a/apps/web/src/hooks/use-player-error.ts b/apps/web/src/hooks/use-player-error.ts index 72451ab..9f93ae1 100644 --- a/apps/web/src/hooks/use-player-error.ts +++ b/apps/web/src/hooks/use-player-error.ts @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { recordClientEvent } from "../lib/client-debug-log"; import { sanitizeVideoContext } from "../lib/debug-sanitize"; import { isIosDevice } from "../lib/ios-device"; +import { detectProvider } from "../lib/provider"; import { resolveManifestSrc } from "../lib/stream-src"; import type { MediaSrc } from "../lib/vidstack"; import type { VideoStream } from "../types/stream"; @@ -32,11 +33,26 @@ function hasMultipleAudioLanguages(stream: VideoStream): boolean { return false; } -export function usePlayerError(stream: VideoStream, isLive: boolean): UsePlayerErrorReturn { +export function usePlayerError( + stream: VideoStream, + isLive: boolean, + enableHighQualityPlayback = false, +): UsePlayerErrorReturn { const streamId = stream.id; const debugVideo = sanitizeVideoContext(streamId) ?? "unknown"; - const preferNativeManifest = !isIosDevice() && !hasMultipleAudioLanguages(stream); - const nativeEnabled = !isLive && Boolean(stream.videoOnlyStreams?.length) && preferNativeManifest; + const provider = detectProvider(stream.id); + const iosDevice = isIosDevice(); + const preferNativeManifest = !iosDevice && !hasMultipleAudioLanguages(stream); + const videoOnlyCount = stream.videoOnlyStreams?.length ?? 0; + const highQualityEnabled = + enableHighQualityPlayback && + !isLive && + !iosDevice && + !stream.hlsUrl && + videoOnlyCount > 0 && + provider === "youtube"; + const nativeEnabled = !isLive && videoOnlyCount > 0 && preferNativeManifest; + const [highQualityFailed, setHighQualityFailed] = useState(false); const [nativeFailed, setNativeFailed] = useState(false); const [qualityFailed, setQualityFailed] = useState(false); const [compatibilityFallback, setCompatibilityFallback] = useState(false); @@ -48,15 +64,32 @@ export function usePlayerError(stream: VideoStream, isLive: boolean): UsePlayerE return resolveManifestSrc(stream, isLive, nativeFailed, qualityFailed, { preferNativeManifest, compatibilityMode: true, + enableHighQualityPlayback: highQualityEnabled, + highQualityFailed, }); } return resolveManifestSrc(stream, isLive, nativeFailed, qualityFailed, { preferNativeManifest, + enableHighQualityPlayback: highQualityEnabled, + highQualityFailed, }); - }, [stream, isLive, nativeFailed, qualityFailed, preferNativeManifest, compatibilityFallback]); + }, [ + stream, + isLive, + nativeFailed, + qualityFailed, + preferNativeManifest, + compatibilityFallback, + highQualityEnabled, + highQualityFailed, + ]); const handleError = useCallback(() => { - if (nativeEnabled && !nativeFailed) { + if (highQualityEnabled && !highQualityFailed) { + recordClientEvent("player.high_quality_failed", { video: debugVideo }); + setHighQualityFailed(true); + setRetryKey((k) => k + 1); + } else if (nativeEnabled && !nativeFailed) { recordClientEvent("player.native_manifest_failed", { video: debugVideo }); setNativeFailed(true); setRetryKey((k) => k + 1); @@ -72,9 +105,19 @@ export function usePlayerError(stream: VideoStream, isLive: boolean): UsePlayerE recordClientEvent("player.failed", { video: debugVideo }); setPlayerFailed(true); } - }, [debugVideo, nativeEnabled, nativeFailed, qualityFailed, compatibilityFallback, isLive]); + }, [ + debugVideo, + highQualityEnabled, + highQualityFailed, + nativeEnabled, + nativeFailed, + qualityFailed, + compatibilityFallback, + isLive, + ]); const reset = useCallback(() => { + setHighQualityFailed(false); setNativeFailed(false); setQualityFailed(false); setCompatibilityFallback(false); @@ -84,6 +127,7 @@ export function usePlayerError(stream: VideoStream, isLive: boolean): UsePlayerE useEffect(() => { if (streamId.length === 0) return; + setHighQualityFailed(false); setNativeFailed(false); setQualityFailed(false); setCompatibilityFallback(false); diff --git a/apps/web/src/hooks/use-settings.ts b/apps/web/src/hooks/use-settings.ts index 03a4ad5..a55e80c 100644 --- a/apps/web/src/hooks/use-settings.ts +++ b/apps/web/src/hooks/use-settings.ts @@ -15,6 +15,7 @@ const DEFAULTS: SettingsItem = { defaultSubtitleLanguage: "", defaultAudioLanguage: "", preferOriginalLanguage: true, + enableHighQualityPlayback: false, }; export function useSettings() { @@ -27,6 +28,8 @@ export function useSettings() { enabled: authReady && isAuthed, placeholderData: DEFAULTS, }); + const settingsReady = + (authReady && !isAuthed) || (query.isSuccess && !query.isPlaceholderData) || query.isError; const update = useMutation({ mutationFn: (patch: Partial) => { @@ -43,5 +46,5 @@ export function useSettings() { }, }); - return { query, update, settings: query.data ?? DEFAULTS }; + return { query, update, settings: query.data ?? DEFAULTS, settingsReady }; } diff --git a/apps/web/src/lib/stream-src.ts b/apps/web/src/lib/stream-src.ts index 17e9b88..9235ba5 100644 --- a/apps/web/src/lib/stream-src.ts +++ b/apps/web/src/lib/stream-src.ts @@ -15,13 +15,10 @@ type ResolveManifestOptions = { preferOriginalLanguage?: boolean; maxCompactAudioTracks?: number; compatibilityMode?: boolean; + enableHighQualityPlayback?: boolean; + highQualityFailed?: boolean; }; -function isFirefoxBrowser(): boolean { - if (typeof navigator === "undefined") return false; - return navigator.userAgent.includes("Firefox/"); -} - function fallbackSrc( stream: VideoStream, maxHeight: number | undefined, @@ -101,7 +98,7 @@ export function resolveManifestSrc( const compactAudioTracks = options?.compactAudioTracks ?? isShort; const maxCompactAudioTracks = options?.maxCompactAudioTracks ?? (isShort ? 3 : 8); const provider = detectProvider(stream.id); - const isFirefox = isFirefoxBrowser(); + const isFirefox = typeof navigator !== "undefined" && navigator.userAgent.includes("Firefox/"); if (stream.hlsUrl) { return { @@ -134,6 +131,20 @@ export function resolveManifestSrc( if (progressiveSrc) return progressiveSrc; } + if ( + !isLive && + provider === "youtube" && + options?.enableHighQualityPlayback && + !options.highQualityFailed && + stream.videoOnlyStreams?.length && + !compatibilityMode + ) { + return { + src: proxyDashManifest(`${BASE}/streams/manifest?url=${encodeURIComponent(stream.id)}`), + type: "application/dash+xml", + }; + } + if ( !isLive && stream.videoOnlyStreams?.length && diff --git a/apps/web/src/settings/settings-playback.tsx b/apps/web/src/settings/settings-playback.tsx index 8d3a62a..21c9f92 100644 --- a/apps/web/src/settings/settings-playback.tsx +++ b/apps/web/src/settings/settings-playback.tsx @@ -117,6 +117,34 @@ export function SettingsPlayback() { onChange={(q) => update.mutate({ defaultQuality: q })} />
+
+
+ Enable high quality playback + + Allows VP9/AV1 adaptive streams when supported. May not work on all browsers or + devices. + +
+ +
Compatibility playback mode diff --git a/apps/web/src/types/user.ts b/apps/web/src/types/user.ts index 8bf67d0..41f100d 100644 --- a/apps/web/src/types/user.ts +++ b/apps/web/src/types/user.ts @@ -66,6 +66,7 @@ export type SettingsItem = { defaultSubtitleLanguage: string; defaultAudioLanguage: string; preferOriginalLanguage: boolean; + enableHighQualityPlayback: boolean; }; export type SearchHistoryItem = { From 0b2a6a17b5dd40a1f33c5755e54ff5151e858f34 Mon Sep 17 00:00:00 2001 From: Priveetee Date: Thu, 4 Jun 2026 16:32:41 +0200 Subject: [PATCH 2/3] fix: support dash codec track selection --- apps/web/src/components/format-selector.tsx | 74 ++++++++++++--- apps/web/src/components/quality-selector.tsx | 45 +++++++++ apps/web/src/components/video-player-core.tsx | 12 ++- apps/web/src/lib/dash-player-store.ts | 49 ++++++++++ apps/web/src/lib/dash-video.ts | 94 +++++++++++++++++++ apps/web/src/lib/quality-utils.ts | 10 +- 6 files changed, 268 insertions(+), 16 deletions(-) create mode 100644 apps/web/src/lib/dash-player-store.ts create mode 100644 apps/web/src/lib/dash-video.ts diff --git a/apps/web/src/components/format-selector.tsx b/apps/web/src/components/format-selector.tsx index 910bb18..5b23920 100644 --- a/apps/web/src/components/format-selector.tsx +++ b/apps/web/src/components/format-selector.tsx @@ -1,5 +1,8 @@ +import type * as dashjs from "dashjs"; import { useRef } from "react"; -import { activeFamily, groupByFamily } from "../lib/quality-utils"; +import { useDashPlayerSnapshot } from "../lib/dash-player-store"; +import { dashTrackGroups, maxTrackHeight, selectDashTrack } from "../lib/dash-video"; +import { activeFamily, type CodecFamily, codecFamily, groupByFamily } from "../lib/quality-utils"; import type { MenuInstance } from "../lib/vidstack"; import { DefaultMenuButton, @@ -8,35 +11,84 @@ import { useVideoQualityOptions, } from "../lib/vidstack"; -const FORMAT_OPTIONS: { label: string; value: "H.264" | "VP9" }[] = [ - { label: "H.264", value: "H.264" }, - { label: "VP9", value: "VP9" }, -]; +const FORMAT_ORDER: CodecFamily[] = ["H.264", "VP9", "AV1"]; const MENU_ITEMS_CLASS = "vds-menu-items overflow-y-auto overscroll-y-contain pr-0.5 [scrollbar-width:thin] [scrollbar-color:var(--color-zinc-500)_transparent] [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-surface-soft/80 [&::-webkit-scrollbar-thumb:hover]:bg-surface-soft [&::-webkit-scrollbar-track]:bg-transparent"; +function isCodecFamily(value: string): value is CodecFamily { + return value === "H.264" || value === "VP9" || value === "AV1"; +} + +function formatLabel(family: CodecFamily, track: dashjs.MediaInfo): string { + const maxHeight = maxTrackHeight(track); + return maxHeight > 0 ? `${family} ${maxHeight}p` : family; +} + export function FormatSelector() { const menuRef = useRef(null); + const { player, selectedVideoTrack } = useDashPlayerSnapshot(); const options = useVideoQualityOptions({ sort: "descending" }); const videoOptions = options.filter((o) => o.quality !== null); + const dashGroups = player ? dashTrackGroups(player) : null; + + if (player && dashGroups && dashGroups.size > 1) { + const dashPlayer = player; + const current = codecFamily( + selectedVideoTrack?.codec ?? player.getCurrentTrackFor("video")?.codec ?? null, + ); + const selected = current ?? FORMAT_ORDER.find((family) => dashGroups.has(family)); + const availableOptions = FORMAT_ORDER.flatMap((family) => { + const track = dashGroups.get(family); + if (!track) return []; + return [{ label: formatLabel(family, track), value: family }]; + }); + + if (!selected) return null; + + function onDashChange(value: string) { + if (!isCodecFamily(value)) return; + const track = dashGroups?.get(value); + if (!track) return; + selectDashTrack(dashPlayer, track); + menuRef.current?.close(); + } + + return ( + + + + + + + ); + } + const groups = groupByFamily(videoOptions); - const current = activeFamily(videoOptions) ?? "H.264"; - const availableOptions = FORMAT_OPTIONS.filter((f) => groups.has(f.value)); + const selected = activeFamily(videoOptions) ?? FORMAT_ORDER.find((family) => groups.has(family)); + const availableOptions = FORMAT_ORDER.filter((family) => groups.has(family)).map((family) => ({ + label: family, + value: family, + })); if (groups.size <= 1) return null; + if (!selected) return null; function onChange(value: string) { - const best = groups.get(value as "H.264" | "VP9"); - if (best) best.select(); + if (!isCodecFamily(value)) return; + groups.get(value)?.select(); menuRef.current?.close(); } return ( - + - + ); diff --git a/apps/web/src/components/quality-selector.tsx b/apps/web/src/components/quality-selector.tsx index bee2b04..7a15ef9 100644 --- a/apps/web/src/components/quality-selector.tsx +++ b/apps/web/src/components/quality-selector.tsx @@ -1,4 +1,7 @@ +import type * as dashjs from "dashjs"; import { useRef } from "react"; +import { useDashPlayerSnapshot } from "../lib/dash-player-store"; +import { dashQualityOptions, selectDashTrack, selectedDashHeight } from "../lib/dash-video"; import type { DefaultLayoutIcon, MenuInstance } from "../lib/vidstack"; import { ClipIcon, @@ -29,10 +32,52 @@ function collectResolutionOptions(options: QualityOption[]): QualityOption[] { return [...grouped.values()]; } +function activeDashTrack( + player: dashjs.MediaPlayerClass, + selectedVideoTrack: dashjs.MediaInfo | null, +): dashjs.MediaInfo | null { + return selectedVideoTrack ?? player.getCurrentTrackFor("video"); +} + export function QualitySelector() { const menuRef = useRef(null); + const { player, selectedVideoTrack } = useDashPlayerSnapshot(); const options = useVideoQualityOptions({ sort: "descending" }); + const dashTrack = player ? activeDashTrack(player, selectedVideoTrack) : null; + if (player && dashTrack) { + const dashPlayer = player; + const activeTrack = dashTrack; + const selectedHeight = selectedDashHeight(dashPlayer, activeTrack); + const dashOptions = dashQualityOptions(activeTrack, selectedHeight); + const selected = dashOptions.find((option) => option.selected) ?? dashOptions[0]; + + if (dashOptions.length > 1 && selected) { + function onDashChange(value: string) { + const height = Number(value); + if (!Number.isFinite(height)) return; + selectDashTrack(dashPlayer, activeTrack, height); + menuRef.current?.close(); + } + + return ( + + + + ({ + label: option.label, + value: option.value, + }))} + onChange={onDashChange} + /> + + + ); + } + } + const videoOptions = options.filter((o) => o.quality !== null); const filteredOptions = collectResolutionOptions(videoOptions); const selected = filteredOptions.find((o) => o.selected) ?? filteredOptions[0]; diff --git a/apps/web/src/components/video-player-core.tsx b/apps/web/src/components/video-player-core.tsx index 06a768b..6a82e98 100644 --- a/apps/web/src/components/video-player-core.tsx +++ b/apps/web/src/components/video-player-core.tsx @@ -1,4 +1,5 @@ import * as dashjs from "dashjs"; +import { notifyDashPlayer, setDashPlayer } from "../lib/dash-player-store"; import type { MediaProviderAdapter } from "../lib/vidstack"; import { isDASHProvider, Track, useMediaState } from "../lib/vidstack"; @@ -19,9 +20,18 @@ export function ChaptersTrack({ src }: { src: string }) { } export function onProviderChange(provider: MediaProviderAdapter | null) { - if (!isDASHProvider(provider)) return; + const dashProvider = isDASHProvider(provider); + if (!dashProvider) { + if (provider === null) setDashPlayer(null); + return; + } provider.library = dashjs.MediaPlayer; provider.onInstance((player) => { + setDashPlayer(player); + const onDashUpdate = () => notifyDashPlayer(); + player.on(dashjs.MediaPlayer.events.STREAM_INITIALIZED, onDashUpdate); + player.on(dashjs.MediaPlayer.events.TRACK_CHANGE_RENDERED, onDashUpdate); + player.on(dashjs.MediaPlayer.events.QUALITY_CHANGE_RENDERED, onDashUpdate); player.updateSettings({ streaming: { cmcd: { enabled: false } } }); shimDashjsQualityApi(player); }); diff --git a/apps/web/src/lib/dash-player-store.ts b/apps/web/src/lib/dash-player-store.ts new file mode 100644 index 0000000..6631b23 --- /dev/null +++ b/apps/web/src/lib/dash-player-store.ts @@ -0,0 +1,49 @@ +import type * as dashjs from "dashjs"; +import { useSyncExternalStore } from "react"; + +type Listener = () => void; + +type DashPlayerSnapshot = { + player: dashjs.MediaPlayerClass | null; + selectedVideoTrack: dashjs.MediaInfo | null; + version: number; +}; + +const listeners = new Set(); +let player: dashjs.MediaPlayerClass | null = null; +let selectedVideoTrack: dashjs.MediaInfo | null = null; +let snapshot: DashPlayerSnapshot = { player: null, selectedVideoTrack: null, version: 0 }; + +function emit(): void { + snapshot = { player, selectedVideoTrack, version: snapshot.version + 1 }; + for (const listener of listeners) listener(); +} + +function subscribe(listener: Listener): () => void { + listeners.add(listener); + return () => listeners.delete(listener); +} + +function getSnapshot(): DashPlayerSnapshot { + return snapshot; +} + +export function setDashPlayer(next: dashjs.MediaPlayerClass | null): void { + if (player === next) return; + player = next; + selectedVideoTrack = null; + emit(); +} + +export function setDashVideoTrack(next: dashjs.MediaInfo | null): void { + selectedVideoTrack = next; + emit(); +} + +export function notifyDashPlayer(): void { + emit(); +} + +export function useDashPlayerSnapshot(): DashPlayerSnapshot { + return useSyncExternalStore(subscribe, getSnapshot, getSnapshot); +} diff --git a/apps/web/src/lib/dash-video.ts b/apps/web/src/lib/dash-video.ts new file mode 100644 index 0000000..b092a7f --- /dev/null +++ b/apps/web/src/lib/dash-video.ts @@ -0,0 +1,94 @@ +import type * as dashjs from "dashjs"; +import { notifyDashPlayer, setDashVideoTrack } from "./dash-player-store"; +import { type CodecFamily, codecFamily } from "./quality-utils"; + +export type DashQualityOption = { + label: string; + value: string; + height: number; + bandwidth: number; + selected: boolean; +}; + +export function maxTrackHeight(track: dashjs.MediaInfo): number { + return Math.max(0, ...track.bitrateList.map((bitrate) => bitrate.height ?? 0)); +} + +export function dashTrackGroups( + player: dashjs.MediaPlayerClass, +): Map { + const groups = new Map(); + for (const track of player.getTracksFor("video")) { + const family = codecFamily(track.codec); + if (!family) continue; + const current = groups.get(family); + if (!current || maxTrackHeight(track) > maxTrackHeight(current)) groups.set(family, track); + } + return groups; +} + +export function dashQualityOptions( + track: dashjs.MediaInfo, + selectedHeight: number | null, +): DashQualityOption[] { + const grouped = new Map(); + for (const bitrate of track.bitrateList) { + const height = bitrate.height ?? 0; + if (height <= 0) continue; + const bandwidth = bitrate.bandwidth ?? 0; + const current = grouped.get(height); + if (!current || bandwidth > current.bandwidth) grouped.set(height, { height, bandwidth }); + } + return [...grouped.values()] + .sort((left, right) => right.height - left.height || right.bandwidth - left.bandwidth) + .map((option) => ({ + label: `${option.height}p`, + value: String(option.height), + height: option.height, + bandwidth: option.bandwidth, + selected: option.height === selectedHeight, + })); +} + +function activeRepresentationHeight( + player: dashjs.MediaPlayerClass, + track: dashjs.MediaInfo, +): number | null { + const currentTrack = player.getCurrentTrackFor("video"); + const currentFamily = codecFamily(currentTrack?.codec ?? null); + const trackFamily = codecFamily(track.codec); + if (currentFamily !== trackFamily) return null; + return player.getCurrentRepresentationForType("video")?.height ?? null; +} + +function applyDashHeight(player: dashjs.MediaPlayerClass, height: number): void { + const representation = player + .getRepresentationsByType("video") + .find((candidate) => candidate.height === height); + if (!representation) return; + player.setRepresentationForTypeByIndex("video", representation.index, true); +} + +export function selectDashTrack( + player: dashjs.MediaPlayerClass, + track: dashjs.MediaInfo, + height = maxTrackHeight(track), +): void { + setDashVideoTrack(track); + player.setCurrentTrack(track, true); + notifyDashPlayer(); + const apply = () => { + player.setCurrentTrack(track, true); + applyDashHeight(player, height); + notifyDashPlayer(); + }; + window.setTimeout(apply, 120); + window.setTimeout(apply, 650); +} + +export function selectedDashHeight( + player: dashjs.MediaPlayerClass, + track: dashjs.MediaInfo, +): number | null { + return activeRepresentationHeight(player, track); +} diff --git a/apps/web/src/lib/quality-utils.ts b/apps/web/src/lib/quality-utils.ts index 8cf82f9..eada9dc 100644 --- a/apps/web/src/lib/quality-utils.ts +++ b/apps/web/src/lib/quality-utils.ts @@ -1,11 +1,13 @@ import type { VideoQualityOption } from "./vidstack"; -type CodecFamily = "H.264" | "VP9"; +export type CodecFamily = "H.264" | "VP9" | "AV1"; -function codecFamily(codec: string | null): CodecFamily | null { +export function codecFamily(codec: string | null): CodecFamily | null { if (!codec) return null; - if (codec.startsWith("avc1")) return "H.264"; - if (codec.startsWith("vp09") || codec === "vp9") return "VP9"; + const normalized = codec.toLowerCase(); + if (normalized.includes("avc1")) return "H.264"; + if (normalized.includes("vp09") || normalized.includes("vp9")) return "VP9"; + if (normalized.includes("av01")) return "AV1"; return null; } From 24acf3330b6061df4117e7cf6f202f2b2027ce57 Mon Sep 17 00:00:00 2001 From: Priveetee Date: Thu, 4 Jun 2026 16:32:58 +0200 Subject: [PATCH 3/3] chore: run Vite tools under Bun --- apps/web/package.json | 4 ++-- bun.lock | 46 +++++++++++++++++++++---------------------- bunfig.toml | 2 ++ 3 files changed, 26 insertions(+), 26 deletions(-) create mode 100644 bunfig.toml diff --git a/apps/web/package.json b/apps/web/package.json index f4bf05e..d15f83d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -21,13 +21,13 @@ "zustand": "^5.0.12" }, "devDependencies": { - "@tailwindcss/vite": "^4.2.4", + "@tailwindcss/vite": "^4.3.0", "@tanstack/router-plugin": "^1.167.32", "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", - "tailwindcss": "^4.2.4", + "tailwindcss": "^4.3.0", "typescript": "~6.0.3", "vite": "^8.0.10" } diff --git a/bun.lock b/bun.lock index 6b39257..7f43d36 100644 --- a/bun.lock +++ b/bun.lock @@ -26,13 +26,13 @@ "zustand": "^5.0.12", }, "devDependencies": { - "@tailwindcss/vite": "^4.2.4", + "@tailwindcss/vite": "^4.3.0", "@tanstack/router-plugin": "^1.167.32", "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", - "tailwindcss": "^4.2.4", + "tailwindcss": "^4.3.0", "typescript": "~6.0.3", "vite": "^8.0.10", }, @@ -256,35 +256,35 @@ "@svta/cml-xml": ["@svta/cml-xml@1.0.1", "", { "peerDependencies": { "@svta/cml-utils": "1.0.1" } }, "sha512-11LkJa5kDEcsRMWkVI1ABH3KLCxGoiSVe4kQ293ItVj8ncTTQ7htmCGiJDjS+Cmy35UgF3e/vc0ysJIiWRTx2g=="], - "@tailwindcss/node": ["@tailwindcss/node@4.2.4", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.4" } }, "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA=="], + "@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.4", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.4", "@tailwindcss/oxide-darwin-arm64": "4.2.4", "@tailwindcss/oxide-darwin-x64": "4.2.4", "@tailwindcss/oxide-freebsd-x64": "4.2.4", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", "@tailwindcss/oxide-linux-x64-musl": "4.2.4", "@tailwindcss/oxide-wasm32-wasi": "4.2.4", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" } }, "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.4", "", { "os": "android", "cpu": "arm64" }, "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg=="], - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.4", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.0", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA=="], - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="], - "@tailwindcss/vite": ["@tailwindcss/vite@4.2.4", "", { "dependencies": { "@tailwindcss/node": "4.2.4", "@tailwindcss/oxide": "4.2.4", "tailwindcss": "4.2.4" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw=="], + "@tailwindcss/vite": ["@tailwindcss/vite@4.3.0", "", { "dependencies": { "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "tailwindcss": "4.3.0" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw=="], "@tanstack/history": ["@tanstack/history@1.161.6", "", {}, "sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg=="], @@ -364,7 +364,7 @@ "electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="], - "enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="], + "enhanced-resolve": ["enhanced-resolve@5.22.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-0rxICaFZ7NQho/sHely2bvOPRP0Eu2B0NZ9zM54YvRvWMn7jfz3DmnOZDR9LlXDdDcqntAVc6Hfy4gr/tdH/Ag=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -530,9 +530,9 @@ "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], - "tailwindcss": ["tailwindcss@4.2.4", "", {}, "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA=="], + "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], - "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], @@ -570,11 +570,9 @@ "@oxc-resolver/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], - "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..1a56448 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,2 @@ +[run] +bun = true