mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
filter by hw decode
This commit is contained in:
@@ -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,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();
|
||||
|
||||
Reference in New Issue
Block a user