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 { t } from 'svelte-i18n';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||||
|
import { mediaCapabilitiesManager } from '$lib/managers/media-capabilities-manager.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
asset: AssetResponseDto;
|
asset: AssetResponseDto;
|
||||||
@@ -116,6 +117,7 @@
|
|||||||
errorRetry: { maxNumRetry: 3, retryDelayMs: 1000, maxRetryDelayMs: 8000 },
|
errorRetry: { maxNumRetry: 3, retryDelayMs: 1000, maxRetryDelayMs: 8000 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
useMediaCapabilities: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const releaseSession = () => {
|
const releaseSession = () => {
|
||||||
@@ -138,24 +140,36 @@
|
|||||||
return;
|
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];
|
const id = api.levels[0]?.url[0]?.match(SESSION_ID_REGEX)?.[1];
|
||||||
if (id) {
|
if (id) {
|
||||||
activeSession = { assetId, id };
|
activeSession = { assetId, id };
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Once ABR has picked an initial codec tier, drop the others.
|
const decodingInfo = await Promise.all(api.levels.map((level) => mediaCapabilitiesManager.decodingInfo(level)));
|
||||||
// The player already avoids switching codecs, and removing the other codecs
|
const lowestBitrateByHeight = new Map<number, number>();
|
||||||
// makes the quality selector cleaner with only resolution options.
|
for (let i = 0; i < api.levels.length; i++) {
|
||||||
api.once(Hls.Events.LEVEL_SWITCHED, (_event, data) => {
|
if (!decodingInfo[i].powerEfficient) {
|
||||||
const chosenLevel = api.levels[data.level];
|
continue;
|
||||||
for (let idx = api.levels.length - 1; idx >= 0; idx--) {
|
}
|
||||||
const { codecSet, videoRange } = api.levels[idx];
|
|
||||||
if (codecSet !== chosenLevel.codecSet || videoRange !== chosenLevel.videoRange) {
|
const { bitrate, height } = api.levels[i];
|
||||||
api.removeLevel(idx);
|
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));
|
api.on(Hls.Events.FRAG_LOADED, () => (rebuildCount = 0));
|
||||||
@@ -174,17 +188,10 @@
|
|||||||
}
|
}
|
||||||
activeSession = undefined;
|
activeSession = undefined;
|
||||||
resumeTime = el.currentTime;
|
resumeTime = el.currentTime;
|
||||||
// Re-setting src triggers attributeChangedCallback → load(), which
|
el.load();
|
||||||
// destroys the old Hls and instantiates a new one with our config.
|
// wireHlsListeners must run after el.api is repopulated.
|
||||||
const url = el.src;
|
|
||||||
el.removeAttribute('src');
|
|
||||||
el.setAttribute('src', url);
|
|
||||||
queueMicrotask(() => wireHlsListeners(el, assetId, resumeTime));
|
queueMicrotask(() => wireHlsListeners(el, assetId, resumeTime));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (resumeTime) {
|
|
||||||
el.addEventListener('loadedmetadata', () => (el.currentTime = resumeTime!), { once: true });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
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 { memoize } from 'lodash-es';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { featureFlagsManager } from '$lib/managers/feature-flags-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 { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
|
||||||
import { initLanguage } from '$lib/utils';
|
import { initLanguage } from '$lib/utils';
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ async function _init(fetch: Fetch) {
|
|||||||
// https://kit.svelte.dev/docs/load#making-fetch-requests
|
// https://kit.svelte.dev/docs/load#making-fetch-requests
|
||||||
// https://github.com/oazapfts/oazapfts/blob/main/README.md#fetch-options
|
// https://github.com/oazapfts/oazapfts/blob/main/README.md#fetch-options
|
||||||
defaults.fetch = fetch;
|
defaults.fetch = fetch;
|
||||||
|
mediaCapabilitiesManager.init();
|
||||||
await initLanguage();
|
await initLanguage();
|
||||||
await serverConfigManager.init();
|
await serverConfigManager.init();
|
||||||
await authManager.load();
|
await authManager.load();
|
||||||
|
|||||||
Reference in New Issue
Block a user