1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
/*
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) 2025 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("V_") || format.codec.startsWith("D_")) {
// TODO do we need to check this?
return format.codec == "V_TEXT/WEBVTT" || 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
}
})
}
console.log(format, res);
return res?.supported ?? false
}
export function track_to_content_type(format: FormatInfo, container: StreamContainer): string {
return `${CONTAINER_TO_MIME_TYPE[container]}; 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": "h264",
"V_MPEGH/ISO/HEVC": "h265",
"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"
}
|