mirror of
https://github.com/jellyfin/jellyfin-vue.git
synced 2024-10-07 03:23:37 +00:00
perf: add generic worker, divide blurhash worker
Canvas drawing and blurhash decoding are no longer blocking. Blurhash decoding decoupling is needed for caching it later alongside the api store. Signed-off-by: Fernando Fernández <ferferga@hotmail.com>
This commit is contained in:
parent
eb94694469
commit
353a6784ad
@ -312,10 +312,10 @@ export default tseslint.config(
|
||||
'import/no-nodejs-modules': 'off'
|
||||
}
|
||||
},
|
||||
/** Settings for WebWorkers (the pattern matches any file that includes the word 'worker', regardless it's capitalization) */
|
||||
/** Settings for WebWorkers (the pattern matches any file that ends in .worker.ts) */
|
||||
{
|
||||
name: 'Environment config for WebWorker files',
|
||||
files: ['**/*[Ww][Oo][Rr][Kk][Ee][Rr]*.ts'],
|
||||
files: ['**/*.worker.ts'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.worker
|
||||
|
@ -9,30 +9,12 @@
|
||||
<slot v-else />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { wrap, transfer } from 'comlink';
|
||||
import { shallowRef, watch } from 'vue';
|
||||
import { DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_PUNCH, type IBlurhashWorker } from './BlurhashWorker';
|
||||
import BlurhashWorker from './BlurhashWorker?worker';
|
||||
import { remote } from '@/plugins/remote';
|
||||
|
||||
const worker = new BlurhashWorker();
|
||||
const blurhashWorker = wrap<IBlurhashWorker>(worker);
|
||||
|
||||
/**
|
||||
* Clear cached blurhashes on logout
|
||||
*/
|
||||
watch(
|
||||
() => remote.auth.currentUser,
|
||||
async () => {
|
||||
if (remote.auth.currentUser === undefined) {
|
||||
await blurhashWorker.clearCache();
|
||||
}
|
||||
}, { flush: 'post' }
|
||||
);
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { transfer } from 'comlink';
|
||||
import { shallowRef, watch } from 'vue';
|
||||
import { blurhashDecoder, canvasDrawer } from '@/plugins/workers';
|
||||
import { BLURHASH_DEFAULT_HEIGHT, BLURHASH_DEFAULT_WIDTH, BLURHASH_DEFAULT_PUNCH } from '@/store';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
hash: string;
|
||||
@ -40,7 +22,7 @@ const props = withDefaults(
|
||||
height?: number;
|
||||
punch?: number;
|
||||
}>(),
|
||||
{ width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT, punch: DEFAULT_PUNCH }
|
||||
{ width: BLURHASH_DEFAULT_WIDTH, height: BLURHASH_DEFAULT_HEIGHT, punch: BLURHASH_DEFAULT_PUNCH }
|
||||
);
|
||||
|
||||
const error = shallowRef(false);
|
||||
@ -48,6 +30,8 @@ const canvas = shallowRef<HTMLCanvasElement>();
|
||||
let offscreen: OffscreenCanvas | undefined;
|
||||
|
||||
watch([props, canvas], async () => {
|
||||
const pixels = await blurhashDecoder.getPixels(props.hash, props.width, props.height, props.punch);
|
||||
|
||||
if (canvas.value) {
|
||||
if (!offscreen) {
|
||||
offscreen = canvas.value.transferControlToOffscreen();
|
||||
@ -55,12 +39,11 @@ watch([props, canvas], async () => {
|
||||
|
||||
try {
|
||||
error.value = false;
|
||||
await blurhashWorker.drawCanvas(transfer(
|
||||
{ hash: props.hash,
|
||||
canvas: offscreen,
|
||||
await canvasDrawer.drawBlurhash(transfer(
|
||||
{ canvas: offscreen,
|
||||
pixels,
|
||||
width: props.width,
|
||||
height: props.height,
|
||||
punch: props.punch
|
||||
height: props.height
|
||||
}, [offscreen]));
|
||||
} catch {
|
||||
error.value = true;
|
||||
|
@ -1,81 +0,0 @@
|
||||
import { decode } from 'blurhash';
|
||||
import { expose } from 'comlink';
|
||||
import { sealed } from '@/utils/validation';
|
||||
|
||||
/**
|
||||
* By default, 20x20 pixels with a punch of 1 is returned.
|
||||
* Although the default values recommended by Blurhash developers is 32x32,
|
||||
* a size of 20x20 seems to be the sweet spot for us, improving the performance
|
||||
* and reducing the memory usage, while retaining almost full blur quality.
|
||||
* Lower values had more visible pixelation
|
||||
*/
|
||||
export const DEFAULT_WIDTH = 20;
|
||||
export const DEFAULT_HEIGHT = 20;
|
||||
export const DEFAULT_PUNCH = 1;
|
||||
|
||||
@sealed
|
||||
class BlurhashWorker {
|
||||
private readonly _pixelsCache = new Map<string, Uint8ClampedArray>();
|
||||
|
||||
/**
|
||||
* Decodes blurhash outside the main thread, in a web worker.
|
||||
*
|
||||
* @param hash - Hash to decode.
|
||||
* @param width - Width of the decoded pixel array
|
||||
* @param height - Height of the decoded pixel array.
|
||||
* @param punch - Contrast of the decoded pixels
|
||||
* @returns - Returns the decoded pixels in the proxied response by Comlink
|
||||
*/
|
||||
public readonly getPixels = (
|
||||
hash: string,
|
||||
width: number = DEFAULT_WIDTH,
|
||||
height: number = DEFAULT_HEIGHT,
|
||||
punch: number = DEFAULT_PUNCH
|
||||
): Uint8ClampedArray => {
|
||||
try {
|
||||
const params = String([hash, width, height, punch]);
|
||||
let pixels = this._pixelsCache.get(params);
|
||||
|
||||
if (!pixels) {
|
||||
pixels = decode(hash, width, height, punch);
|
||||
this._pixelsCache.set(params, pixels);
|
||||
}
|
||||
|
||||
return pixels;
|
||||
} catch {
|
||||
throw new TypeError(`Blurhash ${hash} is not valid`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws the transferred canvas from the main thread
|
||||
*
|
||||
* @param hash - Hash to decode.
|
||||
* @param canvas - Canvas to draw the decoded pixels. Must come from main thread's canvas.transferControlToOffscreen()
|
||||
* @param width - Width of the decoded pixel array
|
||||
* @param height - Height of the decoded pixel array.
|
||||
* @param punch - Contrast of the decoded pixels
|
||||
*/
|
||||
public readonly drawCanvas = ({
|
||||
hash, canvas, width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT, punch = DEFAULT_PUNCH
|
||||
}: { hash: string; canvas: OffscreenCanvas; width: number; height: number; punch: number }) => {
|
||||
const pixels = this.getPixels(hash, width, height, punch);
|
||||
const ctx = canvas.getContext('2d');
|
||||
const imageData = ctx!.createImageData(width, height);
|
||||
imageData.data.set(pixels);
|
||||
ctx!.putImageData(imageData, 0, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear the blurhashes cache
|
||||
*/
|
||||
public readonly clearCache = (): void => {
|
||||
this._pixelsCache.clear();
|
||||
};
|
||||
}
|
||||
|
||||
const instance = new BlurhashWorker();
|
||||
export default instance;
|
||||
export type IBlurhashWorker = typeof instance;
|
||||
|
||||
expose(instance);
|
54
frontend/src/plugins/workers/blurhash-decoder.worker.ts
Normal file
54
frontend/src/plugins/workers/blurhash-decoder.worker.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { decode } from 'blurhash';
|
||||
import { expose } from 'comlink';
|
||||
import { sealed } from '@/utils/validation';
|
||||
|
||||
/**
|
||||
* Decodes blurhash strings into pixels
|
||||
*/
|
||||
@sealed
|
||||
class BlurhashDecoder {
|
||||
private readonly _pixelsCache = new Map<string, Uint8ClampedArray>();
|
||||
|
||||
/**
|
||||
* Decodes blurhash outside the main thread, in a web worker.
|
||||
*
|
||||
* @param hash - Hash to decode.
|
||||
* @param width - Width of the decoded pixel array
|
||||
* @param height - Height of the decoded pixel array.
|
||||
* @param punch - Contrast of the decoded pixels
|
||||
* @returns - Returns the decoded pixels in the proxied response by Comlink
|
||||
*/
|
||||
public readonly getPixels = (
|
||||
hash: string,
|
||||
width: number,
|
||||
height: number,
|
||||
punch: number
|
||||
): Uint8ClampedArray => {
|
||||
try {
|
||||
const params = String([hash, width, height, punch]);
|
||||
let pixels = this._pixelsCache.get(params);
|
||||
|
||||
if (!pixels) {
|
||||
pixels = decode(hash, width, height, punch);
|
||||
this._pixelsCache.set(params, pixels);
|
||||
}
|
||||
|
||||
return pixels;
|
||||
} catch {
|
||||
throw new TypeError(`Blurhash ${hash} is not valid`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear the blurhashes cache
|
||||
*/
|
||||
public readonly clearCache = (): void => {
|
||||
this._pixelsCache.clear();
|
||||
};
|
||||
}
|
||||
|
||||
const instance = new BlurhashDecoder();
|
||||
export default instance;
|
||||
export type IBlurhashDecoder = typeof instance;
|
||||
|
||||
expose(instance);
|
30
frontend/src/plugins/workers/canvas-drawer.worker.ts
Normal file
30
frontend/src/plugins/workers/canvas-drawer.worker.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { expose } from 'comlink';
|
||||
import { sealed } from '@/utils/validation';
|
||||
|
||||
/**
|
||||
* Draws canvases offscreen
|
||||
*/
|
||||
@sealed
|
||||
class CanvasDrawer {
|
||||
/**
|
||||
* Draws a transferred canvas from the main thread
|
||||
*
|
||||
* @param canvas - Canvas to draw the decoded pixels. Must come from main thread's canvas.transferControlToOffscreen()
|
||||
* @param width - Width of the target imageData
|
||||
* @param height - Height of the target imageData
|
||||
*/
|
||||
public readonly drawBlurhash = ({
|
||||
canvas, pixels, width, height
|
||||
}: { canvas: OffscreenCanvas; pixels: Uint8ClampedArray; width: number; height: number }) => {
|
||||
const ctx = canvas.getContext('2d');
|
||||
const imageData = ctx!.createImageData(width, height);
|
||||
imageData.data.set(pixels);
|
||||
ctx!.putImageData(imageData, 0, 0);
|
||||
};
|
||||
}
|
||||
|
||||
const instance = new CanvasDrawer();
|
||||
export default instance;
|
||||
export type ICanvasDrawer = typeof instance;
|
||||
|
||||
expose(instance);
|
18
frontend/src/plugins/workers/generic.worker.ts
Normal file
18
frontend/src/plugins/workers/generic.worker.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { expose } from 'comlink';
|
||||
import { shuffle as _shuffle } from 'lodash-es';
|
||||
import { sealed } from '@/utils/validation';
|
||||
|
||||
/**
|
||||
* All functions that could take some time to complete and block the main thread
|
||||
* must be offloaded to this worker
|
||||
*/
|
||||
@sealed
|
||||
class GenericWorker {
|
||||
public readonly shuffle = (...args: Parameters<typeof _shuffle>) => _shuffle(...args);
|
||||
}
|
||||
|
||||
const instance = new GenericWorker();
|
||||
export default instance;
|
||||
export type IGenericWorker = typeof instance;
|
||||
|
||||
expose(instance);
|
47
frontend/src/plugins/workers/index.ts
Normal file
47
frontend/src/plugins/workers/index.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { wrap } from 'comlink';
|
||||
import { watch } from 'vue';
|
||||
import type { IBlurhashDecoder } from './blurhash-decoder.worker';
|
||||
import BlurhashDecoder from './blurhash-decoder.worker?worker';
|
||||
import type { ICanvasDrawer } from './canvas-drawer.worker';
|
||||
import CanvasDrawer from './canvas-drawer.worker?worker';
|
||||
import type { IGenericWorker } from './generic.worker';
|
||||
import GenericWorker from './generic.worker?worker';
|
||||
import { remote } from '@/plugins/remote';
|
||||
|
||||
/**
|
||||
* A worker for decoding blurhash strings into pixels
|
||||
*/
|
||||
export const blurhashDecoder = wrap<IBlurhashDecoder>(new BlurhashDecoder());
|
||||
|
||||
/**
|
||||
* A worker for drawing canvas offscreen. The canvas must be transferred like this:
|
||||
* ```ts
|
||||
* import { transfer } from 'comlink';
|
||||
*
|
||||
* await canvasDrawer.drawBlurhash(transfer(
|
||||
* { canvas: offscreen,
|
||||
* pixels,
|
||||
* width,
|
||||
* height
|
||||
* }, [offscreen]));
|
||||
* ```
|
||||
*/
|
||||
export const canvasDrawer = wrap<ICanvasDrawer>(new CanvasDrawer());
|
||||
|
||||
/**
|
||||
* A worker for running any non-specific function that could be expensive and take some time to complete,
|
||||
* blocking the main thread
|
||||
*/
|
||||
export const genericWorker = wrap<IGenericWorker>(new GenericWorker());
|
||||
|
||||
/**
|
||||
* Clear cached blurhashes on logout
|
||||
*/
|
||||
watch(
|
||||
() => remote.auth.currentUser,
|
||||
async () => {
|
||||
if (remote.auth.currentUser === undefined) {
|
||||
await blurhashDecoder.clearCache();
|
||||
}
|
||||
}, { flush: 'post' }
|
||||
);
|
@ -3,12 +3,25 @@ import { computedAsync, useFps, useMediaControls, useMediaQuery, useNetwork, use
|
||||
import { shallowRef, computed } from 'vue';
|
||||
import { remote } from '@/plugins/remote';
|
||||
import { isNil } from '@/utils/validation';
|
||||
|
||||
/**
|
||||
* This file contains global variables (specially VueUse refs) that are used multiple times across the client.
|
||||
* VueUse composables will set new event handlers, so it's more
|
||||
* efficient to reuse those, both in components and TS files.
|
||||
*/
|
||||
|
||||
/**
|
||||
* == BLURHASH DEFAULTS ==
|
||||
* By default, 20x20 pixels with a punch of 1 is returned.
|
||||
* Although the default values recommended by Blurhash developers is 32x32,
|
||||
* a size of 20x20 seems to be the sweet spot for us, improving the performance
|
||||
* and reducing the memory usage, while retaining almost full blur quality.
|
||||
* Lower values had more visible pixelation
|
||||
*/
|
||||
export const BLURHASH_DEFAULT_WIDTH = 20;
|
||||
export const BLURHASH_DEFAULT_HEIGHT = 20;
|
||||
export const BLURHASH_DEFAULT_PUNCH = 1;
|
||||
|
||||
/**
|
||||
* Reactive Date.now() instance
|
||||
*/
|
||||
|
@ -21,9 +21,8 @@ import { getMediaInfoApi } from '@jellyfin/sdk/lib/utils/api/media-info-api';
|
||||
import { getPlaystateApi } from '@jellyfin/sdk/lib/utils/api/playstate-api';
|
||||
import { getTvShowsApi } from '@jellyfin/sdk/lib/utils/api/tv-shows-api';
|
||||
import { useEventListener, watchThrottled } from '@vueuse/core';
|
||||
import { shuffle } from 'lodash-es';
|
||||
import { v4 } from 'uuid';
|
||||
import { watch, watchEffect } from 'vue';
|
||||
import { toRaw, watch, watchEffect } from 'vue';
|
||||
import { isNil, sealed } from '@/utils/validation';
|
||||
import { useBaseItem } from '@/composables/apis';
|
||||
import { useSnackbar } from '@/composables/use-snackbar';
|
||||
@ -36,6 +35,7 @@ import playbackProfile from '@/utils/playback-profiles';
|
||||
import { msToTicks } from '@/utils/time';
|
||||
import { mediaControls, mediaElementRef } from '@/store';
|
||||
import { CommonStore } from '@/store/super/common-store';
|
||||
import { genericWorker } from '@/plugins/workers';
|
||||
|
||||
/**
|
||||
* == INTERFACES AND TYPES ==
|
||||
@ -600,7 +600,7 @@ class PlaybackManagerStore extends CommonStore<PlaybackManagerState> {
|
||||
this._state.currentItemIndex = startFromIndex;
|
||||
|
||||
if (startShuffled) {
|
||||
this.toggleShuffle(false);
|
||||
void this.toggleShuffle(false);
|
||||
}
|
||||
|
||||
this.currentTime = startFromTime;
|
||||
@ -755,8 +755,8 @@ class PlaybackManagerStore extends CommonStore<PlaybackManagerState> {
|
||||
this.currentVolume = this.currentVolume - 5;
|
||||
};
|
||||
|
||||
public readonly toggleShuffle = (preserveCurrentItem = true): void => {
|
||||
if (this._state.queue && !isNil(this._state.currentItemIndex)) {
|
||||
public readonly toggleShuffle = async (preserveCurrentItem = true): Promise<void> => {
|
||||
if (!isNil(this._state.currentItemIndex)) {
|
||||
if (this._state.isShuffling) {
|
||||
const item = this._state.queue[this._state.currentItemIndex];
|
||||
|
||||
@ -765,7 +765,7 @@ class PlaybackManagerStore extends CommonStore<PlaybackManagerState> {
|
||||
this._state.originalQueue = [];
|
||||
this._state.isShuffling = false;
|
||||
} else {
|
||||
const queue = shuffle(this._state.queue);
|
||||
const queue = await genericWorker.shuffle(toRaw(this._state.queue));
|
||||
|
||||
this._state.originalQueue = this._state.queue;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user