mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-11 05:02:24 +09:00
188 lines
5.5 KiB
TypeScript
188 lines
5.5 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import {Logger} from '@app/features/platform/utils/AppLogger';
|
|
import type {VideoCodec} from 'livekit-client';
|
|
|
|
const logger = new Logger('VideoDecoderCapabilities');
|
|
|
|
interface WebCodecsConfig {
|
|
codec: string;
|
|
width: number;
|
|
height: number;
|
|
hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';
|
|
}
|
|
|
|
interface CodecProbe {
|
|
codec: VideoCodec;
|
|
configs: Array<WebCodecsConfig>;
|
|
}
|
|
|
|
const CODEC_PROBES: ReadonlyArray<CodecProbe> = [
|
|
{
|
|
codec: 'h265',
|
|
configs: [
|
|
{codec: 'hev1.1.6.L120.B0', width: 1920, height: 1080},
|
|
{codec: 'hvc1.1.6.L120.B0', width: 1920, height: 1080},
|
|
{codec: 'hev1.1.6.L120.B0', width: 1920, height: 1080, hardwareAcceleration: 'prefer-hardware'},
|
|
{codec: 'hvc1.1.6.L120.B0', width: 1920, height: 1080, hardwareAcceleration: 'prefer-hardware'},
|
|
],
|
|
},
|
|
{
|
|
codec: 'h264',
|
|
configs: [
|
|
{codec: 'avc1.42E01F', width: 1920, height: 1080},
|
|
{codec: 'avc1.4D401F', width: 1920, height: 1080},
|
|
{codec: 'avc1.640028', width: 1920, height: 1080},
|
|
],
|
|
},
|
|
{
|
|
codec: 'vp9',
|
|
configs: [{codec: 'vp09.00.31.08', width: 1920, height: 1080}],
|
|
},
|
|
{
|
|
codec: 'av1',
|
|
configs: [{codec: 'av01.0.08M.08', width: 1920, height: 1080}],
|
|
},
|
|
];
|
|
|
|
const SCREEN_SHARE_DECODE_FAILURES_MAX = 1;
|
|
|
|
let cachedExclusions: Array<VideoCodec> | null = null;
|
|
let pendingProbe: Promise<Array<VideoCodec>> | null = null;
|
|
const screenShareDecodeFailures = new Set<VideoCodec>();
|
|
|
|
interface VideoDecoderLike {
|
|
isConfigSupported(config: WebCodecsConfig): Promise<{supported: boolean}>;
|
|
}
|
|
|
|
function getVideoDecoderApi(): VideoDecoderLike | null {
|
|
const vd = (globalThis as Record<string, unknown>).VideoDecoder as VideoDecoderLike | undefined;
|
|
if (vd && typeof vd.isConfigSupported === 'function') return vd;
|
|
return null;
|
|
}
|
|
|
|
function hasRtcReceiverCapability(mimeType: string): boolean {
|
|
const receiver = (globalThis as Record<string, unknown>).RTCRtpReceiver as
|
|
| {getCapabilities?: (kind: 'video') => RTCRtpCapabilities | null}
|
|
| undefined;
|
|
const caps = receiver?.getCapabilities?.('video');
|
|
if (!caps) return false;
|
|
const lower = mimeType.toLowerCase();
|
|
return caps.codecs.some((c) => {
|
|
const candidate = c.mimeType.toLowerCase();
|
|
return candidate === lower || (lower === 'video/av1' && candidate === 'video/av1x');
|
|
});
|
|
}
|
|
|
|
function shouldExcludeByPlatformPolicy(_codec: VideoCodec): boolean {
|
|
return false;
|
|
}
|
|
|
|
function isBaselineWebRtcDecodeCodec(codec: VideoCodec): boolean {
|
|
return codec === 'h264' || codec === 'vp8';
|
|
}
|
|
|
|
function getPlatformPolicyExclusions(): Array<VideoCodec> | null {
|
|
if ((globalThis as Record<string, unknown>).RTCRtpReceiver === undefined) return null;
|
|
const excluded: Array<VideoCodec> = [];
|
|
for (const probe of CODEC_PROBES) {
|
|
if (hasRtcReceiverCapability(`video/${probe.codec}`) && shouldExcludeByPlatformPolicy(probe.codec)) {
|
|
excluded.push(probe.codec);
|
|
}
|
|
}
|
|
return excluded.length > 0 ? excluded : null;
|
|
}
|
|
|
|
async function probeCodecDecode(probe: CodecProbe): Promise<boolean> {
|
|
if (!hasRtcReceiverCapability(`video/${probe.codec}`)) {
|
|
return true;
|
|
}
|
|
if (isBaselineWebRtcDecodeCodec(probe.codec)) {
|
|
return true;
|
|
}
|
|
if (shouldExcludeByPlatformPolicy(probe.codec)) {
|
|
return false;
|
|
}
|
|
const api = getVideoDecoderApi();
|
|
if (!api) {
|
|
return true;
|
|
}
|
|
for (const config of probe.configs) {
|
|
try {
|
|
const result = await api.isConfigSupported(config);
|
|
if (result.supported) return true;
|
|
} catch {}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
async function probeAllCodecs(): Promise<Array<VideoCodec>> {
|
|
const results = await Promise.allSettled(
|
|
CODEC_PROBES.map(async (probe) => {
|
|
const supported = await probeCodecDecode(probe);
|
|
return {codec: probe.codec, supported};
|
|
}),
|
|
);
|
|
const excluded: Array<VideoCodec> = [];
|
|
for (const result of results) {
|
|
if (result.status === 'rejected') {
|
|
logger.warn('Codec decode probe threw unexpectedly', {error: result.reason});
|
|
} else if (!result.value.supported) {
|
|
excluded.push(result.value.codec);
|
|
}
|
|
}
|
|
if (excluded.length > 0) {
|
|
logger.info('Excluding codecs from subscriber decode', {excluded});
|
|
}
|
|
return excluded;
|
|
}
|
|
|
|
export function loadVideoDecoderExclusions(): Promise<Array<VideoCodec>> {
|
|
if (cachedExclusions) return Promise.resolve(cachedExclusions);
|
|
if (pendingProbe) return pendingProbe;
|
|
pendingProbe = probeAllCodecs()
|
|
.then((result) => {
|
|
cachedExclusions = result;
|
|
return result;
|
|
})
|
|
.catch((error) => {
|
|
logger.warn('Video decoder probe failed entirely, not excluding any codecs', {error});
|
|
cachedExclusions = [];
|
|
return [];
|
|
})
|
|
.finally(() => {
|
|
pendingProbe = null;
|
|
});
|
|
return pendingProbe;
|
|
}
|
|
|
|
export function getVideoDecoderExclusionsSync(): Array<VideoCodec> | null {
|
|
return cachedExclusions ?? getPlatformPolicyExclusions();
|
|
}
|
|
|
|
export function markScreenShareDecodeFailure(codec: VideoCodec, reason: string): boolean {
|
|
if (screenShareDecodeFailures.has(codec)) return false;
|
|
if (screenShareDecodeFailures.size >= SCREEN_SHARE_DECODE_FAILURES_MAX) return false;
|
|
screenShareDecodeFailures.add(codec);
|
|
logger.warn('Withdrawing decode advertisement for a codec after a screen share decode stall', {codec, reason});
|
|
return true;
|
|
}
|
|
|
|
export function getScreenShareDecodeFailures(): ReadonlySet<VideoCodec> {
|
|
return screenShareDecodeFailures;
|
|
}
|
|
|
|
export function clearScreenShareDecodeFailures(): void {
|
|
screenShareDecodeFailures.clear();
|
|
}
|
|
|
|
export function resetVideoDecoderExclusions(): void {
|
|
cachedExclusions = null;
|
|
pendingProbe = null;
|
|
screenShareDecodeFailures.clear();
|
|
}
|
|
|
|
if (typeof window !== 'undefined') {
|
|
void loadVideoDecoderExclusions();
|
|
}
|