diff options
Diffstat (limited to 'ui/client-scripts/src/player/mediacaps.ts')
| -rw-r--r-- | ui/client-scripts/src/player/mediacaps.ts | 79 |
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" +} |