filter by hw decode

This commit is contained in:
mertalev
2026-05-08 21:46:11 -04:00
parent 24ad0fed53
commit 596e8ea397
3 changed files with 101 additions and 20 deletions
@@ -43,6 +43,7 @@
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { mediaCapabilitiesManager } from '$lib/managers/media-capabilities-manager.svelte';
interface Props {
asset: AssetResponseDto;
@@ -116,6 +117,7 @@
errorRetry: { maxNumRetry: 3, retryDelayMs: 1000, maxRetryDelayMs: 8000 },
},
},
useMediaCapabilities: false,
};
const releaseSession = () => {
@@ -138,24 +140,36 @@
return;
}
api.on(Hls.Events.MANIFEST_PARSED, () => {
api.on(Hls.Events.MANIFEST_PARSED, async () => {
// Defer hls.js's first fragment load until we filter out suboptimal variants
api.stopLoad();
const id = api.levels[0]?.url[0]?.match(SESSION_ID_REGEX)?.[1];
if (id) {
activeSession = { assetId, id };
}
});
// Once ABR has picked an initial codec tier, drop the others.
// The player already avoids switching codecs, and removing the other codecs
// makes the quality selector cleaner with only resolution options.
api.once(Hls.Events.LEVEL_SWITCHED, (_event, data) => {
const chosenLevel = api.levels[data.level];
for (let idx = api.levels.length - 1; idx >= 0; idx--) {
const { codecSet, videoRange } = api.levels[idx];
if (codecSet !== chosenLevel.codecSet || videoRange !== chosenLevel.videoRange) {
api.removeLevel(idx);
const decodingInfo = await Promise.all(api.levels.map((level) => mediaCapabilitiesManager.decodingInfo(level)));
const lowestBitrateByHeight = new Map<number, number>();
for (let i = 0; i < api.levels.length; i++) {
if (!decodingInfo[i].powerEfficient) {
continue;
}
const { bitrate, height } = api.levels[i];
const cur = lowestBitrateByHeight.get(height);
if (cur === undefined || bitrate < api.levels[cur].bitrate) {
lowestBitrateByHeight.set(height, i);
}
}
const keep = new Set(lowestBitrateByHeight.values());
for (let i = api.levels.length - 1; i >= 0; i--) {
if (!keep.has(i)) {
api.removeLevel(i);
}
}
api.startLoad(resumeTime);
});
api.on(Hls.Events.FRAG_LOADED, () => (rebuildCount = 0));
@@ -174,17 +188,10 @@
}
activeSession = undefined;
resumeTime = el.currentTime;
// Re-setting src triggers attributeChangedCallback → load(), which
// destroys the old Hls and instantiates a new one with our config.
const url = el.src;
el.removeAttribute('src');
el.setAttribute('src', url);
el.load();
// wireHlsListeners must run after el.api is repopulated.
queueMicrotask(() => wireHlsListeners(el, assetId, resumeTime));
});
if (resumeTime) {
el.addEventListener('loadedmetadata', () => (el.currentTime = resumeTime!), { once: true });
}
};
onMount(() => {
@@ -0,0 +1,72 @@
export type Level = { videoCodec?: string; width: number; height: number; bitrate: number; frameRate: number };
export const DEFAULT_DECODING_INFO: MediaCapabilitiesDecodingInfo = {
powerEfficient: true,
smooth: true,
supported: true,
keySystemAccess: null,
};
class MediaCapabilitiesManager {
private cache = new Map<string, Promise<MediaCapabilitiesDecodingInfo>>();
init() {
for (const level of [
{ videoCodec: 'av01.0.04M.08', width: 854, height: 480, bitrate: 1_000_000, frameRate: 60 },
{ videoCodec: 'hvc1.1.6.L90.B0', width: 854, height: 480, bitrate: 1_200_000, frameRate: 60 },
{ videoCodec: 'av01.0.08M.08', width: 1280, height: 720, bitrate: 2_000_000, frameRate: 60 },
{ videoCodec: 'hvc1.1.6.L93.B0', width: 1280, height: 720, bitrate: 2_500_000, frameRate: 60 },
{ videoCodec: 'av01.0.09M.08', width: 1920, height: 1080, bitrate: 4_000_000, frameRate: 60 },
{ videoCodec: 'hvc1.1.6.L120.B0', width: 1920, height: 1080, bitrate: 4_500_000, frameRate: 60 },
{ videoCodec: 'av01.0.12M.08', width: 2560, height: 1440, bitrate: 7_000_000, frameRate: 60 },
{ videoCodec: 'hvc1.2.4.L150.B0', width: 2560, height: 1440, bitrate: 8_000_000, frameRate: 60 },
]) {
this.cache.set(this.cacheKey(level), this.queryDecodingInfo(level));
}
for (const level of [
{ videoCodec: 'avc1.64001e', width: 854, height: 480, bitrate: 2_500_000, frameRate: 60 },
{ videoCodec: 'avc1.64001f', width: 1280, height: 720, bitrate: 5_000_000, frameRate: 60 },
{ videoCodec: 'avc1.640028', width: 1920, height: 1080, bitrate: 8_000_000, frameRate: 60 },
{ videoCodec: 'avc1.640032', width: 2560, height: 1440, bitrate: 16_000_000, frameRate: 60 },
]) {
this.cache.set(this.cacheKey(level), Promise.resolve(DEFAULT_DECODING_INFO));
}
}
decodingInfo(level: Level) {
const key = this.cacheKey(level);
const existing = this.cache.get(key);
if (existing) {
return existing;
}
const promise = this.queryDecodingInfo(level);
this.cache.set(key, promise);
return promise;
}
private async queryDecodingInfo(level: Level) {
try {
return await navigator.mediaCapabilities.decodingInfo({
type: 'media-source',
video: {
contentType: `video/mp4; codecs="${level.videoCodec}"`,
width: level.width,
height: level.height,
bitrate: level.bitrate,
framerate: level.frameRate,
},
});
} catch {
return DEFAULT_DECODING_INFO;
}
}
private cacheKey({ videoCodec, width, height, frameRate }: Level) {
const resolution = Math.min(width, height);
const fpsBucket = Math.trunc(frameRate / 61) * 60;
return `${videoCodec}|${resolution}|${fpsBucket}`;
}
}
export const mediaCapabilitiesManager = new MediaCapabilitiesManager();
+2
View File
@@ -2,6 +2,7 @@ import { defaults } from '@immich/sdk';
import { memoize } from 'lodash-es';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { mediaCapabilitiesManager } from '$lib/managers/media-capabilities-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { initLanguage } from '$lib/utils';
@@ -12,6 +13,7 @@ async function _init(fetch: Fetch) {
// https://kit.svelte.dev/docs/load#making-fetch-requests
// https://github.com/oazapfts/oazapfts/blob/main/README.md#fetch-options
defaults.fetch = fetch;
mediaCapabilitiesManager.init();
await initLanguage();
await serverConfigManager.init();
await authManager.load();