aboutsummaryrefslogtreecommitdiff
path: root/ui/client-scripts/src/player/mediacaps.ts
diff options
context:
space:
mode:
Diffstat (limited to 'ui/client-scripts/src/player/mediacaps.ts')
-rw-r--r--ui/client-scripts/src/player/mediacaps.ts79
1 files changed, 79 insertions, 0 deletions
diff --git a/ui/client-scripts/src/player/mediacaps.ts b/ui/client-scripts/src/player/mediacaps.ts
new file mode 100644
index 0000000..9b0e934
--- /dev/null
+++ b/ui/client-scripts/src/player/mediacaps.ts
@@ -0,0 +1,79 @@
+/*
+ This file is part of jellything (https://codeberg.org/metamuffin/jellything)
+ which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
+ Copyright (C) 2026 metamuffin <metamuffin.org>
+*/
+/// <reference lib="dom" />
+
+import { FormatInfo, StreamContainer } from "./types_stream.ts";
+
+const cache = new Map<string, boolean>()
+
+// TODO this testing method makes the assumption, that if the codec is supported on its own, it can be
+// TODO arbitrarly combined with others that are supported. in reality this is true but the spec does not gurantee it.
+
+export async function test_media_capability(format: FormatInfo, container: StreamContainer): Promise<boolean> {
+ const cache_key = JSON.stringify(format) + container
+ const cached = cache.get(cache_key);
+ if (cached !== undefined) return cached
+ const r = await test_media_capability_inner(format, container)
+ console.log(`${r ? "positive" : "negative"} media capability test finished for codec=${format.codec}`);
+ cache.set(cache_key, r)
+ return r
+}
+async function test_media_capability_inner(format: FormatInfo, container: StreamContainer) {
+ if (format.codec.startsWith("S_") || format.codec.startsWith("D_")) {
+ // TODO do we need to check this?
+ return format.codec == "S_TEXT/WEBVTT" || format.codec == "S_TEXT/UTF8" || format.codec == "D_WEBVTT/SUBTITLES"
+ }
+ let res;
+ if (format.codec.startsWith("A_")) {
+ res = await navigator.mediaCapabilities.decodingInfo({
+ type: "media-source",
+ audio: {
+ contentType: track_to_content_type(format, container),
+ samplerate: format.samplerate,
+ channels: "" + format.channels,
+ bitrate: format.bitrate,
+ }
+ })
+ }
+ if (format.codec.startsWith("V_")) {
+ res = await navigator.mediaCapabilities.decodingInfo({
+ type: "media-source",
+ video: {
+ contentType: track_to_content_type(format, container),
+ framerate: 30, // TODO get average framerate from server
+ width: format.width ?? 1920,
+ height: format.height ?? 1080,
+ bitrate: format.bitrate
+ }
+ })
+ }
+ return res?.supported ?? false
+}
+
+export function track_to_content_type(format: FormatInfo, container: StreamContainer): string {
+ let c = CONTAINER_TO_MIME_TYPE[container];
+ if (format.codec.startsWith("A_")) c = c.replace("video/", "audio/")
+ return `${c}; codecs="${MASTROSKA_CODEC_MAP[format.codec]}"`
+}
+
+const MASTROSKA_CODEC_MAP: { [key: string]: string } = {
+ "V_VP9": "vp9",
+ "V_VP8": "vp8",
+ "V_AV1": "av1",
+ "V_MPEG4/ISO/AVC": "avc1.42C01F",
+ "V_MPEGH/ISO/HEVC": "hev1.1.6.L93.90",
+ "A_OPUS": "opus",
+ "A_VORBIS": "vorbis",
+ "S_TEXT/WEBVTT": "webvtt",
+ "D_WEBVTT/SUBTITLES": "webvtt",
+}
+const CONTAINER_TO_MIME_TYPE: { [key in StreamContainer]: string } = {
+ webvtt: "text/webvtt",
+ webm: "video/webm",
+ matroska: "video/x-matroska",
+ mpeg4: "video/mp4",
+ jvtt: "application/jellything-vtt+json"
+}