1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Merge pull request #3144 from jellyfin/blurhash-worker

Decode blurhash in a WebWorker
This commit is contained in:
Bill Thornton 2022-01-14 09:15:44 -05:00 committed by GitHub
commit 0277726418
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 151 additions and 114 deletions

87
package-lock.json generated
View file

@ -3082,7 +3082,8 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.0.tgz",
"integrity": "sha512-ttOkEkoalEHa7RaFYpM0ErK1xc4twg3Am9hfHhL7MVqlHebnkYd2wuI/ZqTDj0cVzZho6PdinY0phFZV3O0Mzg==",
"dev": true
"dev": true,
"requires": {}
},
"@webpack-cli/info": {
"version": "1.4.0",
@ -3097,7 +3098,8 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.0.tgz",
"integrity": "sha512-ZkVeqEmRpBV2GHvjjUZqEai2PpUbuq8Bqd//vEYsp63J8WyexI8ppCqVS3Zs0QADf6aWuPdU+0XsPI647PVlQA==",
"dev": true
"dev": true,
"requires": {}
},
"@xmldom/xmldom": {
"version": "0.7.5",
@ -3142,13 +3144,15 @@
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz",
"integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==",
"dev": true
"dev": true,
"requires": {}
},
"acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true
"dev": true,
"requires": {}
},
"aggregate-error": {
"version": "3.1.0",
@ -3205,7 +3209,8 @@
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true
"dev": true,
"requires": {}
},
"alphanum-sort": {
"version": "1.0.2",
@ -4648,7 +4653,8 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-2.0.1.tgz",
"integrity": "sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==",
"dev": true
"dev": true,
"requires": {}
},
"csso": {
"version": "4.2.0",
@ -5801,7 +5807,8 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz",
"integrity": "sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==",
"dev": true
"dev": true,
"requires": {}
},
"eslint-rule-composer": {
"version": "0.3.0",
@ -6025,7 +6032,8 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-3.1.0.tgz",
"integrity": "sha512-2RExSo0yJiqP+xiUue13jQa2IHE8kLDzTI7b6kn+vUlBVvlzNSiLDzo4e5Pp5J039usvTUnxZ8sUOhv0Kg15NA==",
"dev": true
"dev": true,
"requires": {}
},
"express": {
"version": "4.17.2",
@ -7030,7 +7038,8 @@
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
"integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
"dev": true
"dev": true,
"requires": {}
},
"idb": {
"version": "6.1.2",
@ -7763,7 +7772,7 @@
},
"libass-wasm": {
"version": "git+https://github.com/jellyfin/JavascriptSubtitlesOctopus.git#f4625ac313b318bd5d2e0ae18679ff516370bae6",
"from": "git+https://github.com/jellyfin/JavascriptSubtitlesOctopus.git#4.0.0-jf-4"
"from": "libass-wasm@git+https://github.com/jellyfin/JavascriptSubtitlesOctopus.git#4.0.0-jf-4"
},
"lie": {
"version": "3.1.1",
@ -9058,25 +9067,29 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz",
"integrity": "sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==",
"dev": true
"dev": true,
"requires": {}
},
"postcss-discard-duplicates": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz",
"integrity": "sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==",
"dev": true
"dev": true,
"requires": {}
},
"postcss-discard-empty": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz",
"integrity": "sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==",
"dev": true
"dev": true,
"requires": {}
},
"postcss-discard-overridden": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz",
"integrity": "sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==",
"dev": true
"dev": true,
"requires": {}
},
"postcss-double-position-gradients": {
"version": "3.0.4",
@ -9692,7 +9705,8 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
"integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
"dev": true
"dev": true,
"requires": {}
},
"postcss-modules-local-by-default": {
"version": "4.0.0",
@ -9748,7 +9762,8 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz",
"integrity": "sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==",
"dev": true
"dev": true,
"requires": {}
},
"postcss-normalize-display-values": {
"version": "5.0.1",
@ -10150,7 +10165,8 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.2.tgz",
"integrity": "sha512-xfdkU128CkKKKVAwkyt0M8OdnelJ3MRcIRAPPQkRpoPeuzWY3RIeg7piRCpZ79MK7Q16diLXMMAD9dN5mauPlQ==",
"dev": true
"dev": true,
"requires": {}
},
"postcss-selector-not": {
"version": "5.0.0",
@ -10225,7 +10241,8 @@
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/postcss-syntax/-/postcss-syntax-0.36.2.tgz",
"integrity": "sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==",
"dev": true
"dev": true,
"requires": {}
},
"postcss-unique-selectors": {
"version": "5.0.2",
@ -11495,6 +11512,14 @@
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"dev": true
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
},
"string-width": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
@ -11542,14 +11567,6 @@
"define-properties": "^1.1.3"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
},
"stringify-entities": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-1.3.2.tgz",
@ -11633,7 +11650,8 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz",
"integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==",
"dev": true
"dev": true,
"requires": {}
},
"style-search": {
"version": "0.1.0",
@ -11845,7 +11863,8 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz",
"integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==",
"dev": true
"dev": true,
"requires": {}
},
"postcss-selector-parser": {
"version": "6.0.8",
@ -13663,7 +13682,8 @@
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/postcss-sorting/-/postcss-sorting-7.0.1.tgz",
"integrity": "sha512-iLBFYz6VRYyLJEJsBJ8M3TCqNcckVzz4wFounSc5Oez35ogE/X+aoC5fFu103Ot7NyvjU3/xqIXn93Gp3kJk4g==",
"dev": true
"dev": true,
"requires": {}
}
}
},
@ -15340,15 +15360,6 @@
}
}
},
"worker-plugin": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/worker-plugin/-/worker-plugin-5.0.1.tgz",
"integrity": "sha512-Pn7+19jIiANcGuTSGdy+vrzyF+SGH03A5wV8iu4jRTMAOfAC9bNeiHo4+l5tPS7F0uvICMBv+h8UCvL7lunxcA==",
"dev": true,
"requires": {
"loader-utils": "^1.1.0"
}
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",

View file

@ -55,8 +55,7 @@
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.7.2",
"webpack-merge": "^5.8.0",
"workbox-webpack-plugin": "^6.2.4",
"worker-plugin": "^5.0.1"
"workbox-webpack-plugin": "^6.2.4"
},
"dependencies": {
"@fontsource/noto-sans": "^4.5.1",

View file

@ -0,0 +1,16 @@
/* eslint-disable no-restricted-globals */
import { decode } from 'blurhash';
self.onmessage = ({ data: { hash, width, height } }): void => {
try {
self.postMessage({
pixels: decode(hash, width, height),
hsh: hash,
width: width,
height: height
});
} catch {
throw new TypeError(`Blurhash ${hash} is not valid`);
}
};
/* eslint-enable no-restricted-globals */

View file

@ -1,7 +1,21 @@
import * as lazyLoader from '../lazyLoader/lazyLoaderIntersectionObserver';
import * as userSettings from '../../scripts/settings/userSettings';
import { decode, isBlurhashValid } from 'blurhash';
import './style.scss';
// eslint-disable-next-line compat/compat
const worker = new Worker(new URL('./blurhash.worker.ts', import.meta.url));
const targetDic = {};
worker.addEventListener(
'message',
({ data: { pixels, hsh, width, height } }) => {
const elems = targetDic[hsh];
if (elems && elems.length) {
for (const elem of elems) {
drawBlurhash(elem, pixels, width, height);
}
delete targetDic[hsh];
}
}
);
/* eslint-disable indent */
export function lazyImage(elem, source = elem.getAttribute('data-src')) {
@ -12,21 +26,7 @@ import './style.scss';
fillImageElement(elem, source);
}
function itemBlurhashing(target, blurhashstr) {
if (isBlurhashValid(blurhashstr)) {
// Although the default values recommended by Blurhash developers is 32x32, a size of 18x18 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
const width = 18;
const height = 18;
let pixels;
try {
pixels = decode(blurhashstr, width, height);
} catch (err) {
console.error('Blurhash decode error: ', err);
target.classList.add('non-blurhashable');
return;
}
function drawBlurhash(target, pixels, width, height) {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
@ -37,18 +37,35 @@ import './style.scss';
ctx.putImageData(imgData, 0, 0);
requestAnimationFrame(() => {
// This class is just an utility class, so users can customize the canvas using their own CSS.
canvas.classList.add('blurhash-canvas');
if (userSettings.enableFastFadein()) {
canvas.classList.add('lazy-blurhash-fadein-fast');
} else {
canvas.classList.add('lazy-blurhash-fadein');
}
target.parentNode.insertBefore(canvas, target);
target.classList.add('blurhashed');
target.removeAttribute('data-blurhash');
});
}
function itemBlurhashing(target, hash) {
try {
// 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
const width = 20;
const height = 20;
targetDic[hash] = (targetDic[hash] || []).filter(item => item !== target);
targetDic[hash].push(target);
worker.postMessage({
hash,
width,
height
});
} catch (err) {
console.error(err);
target.classList.add('non-blurhashable');
return;
}
}
export function fillImage(entry) {
@ -65,12 +82,23 @@ import './style.scss';
}
if (entry.intersectionRatio > 0) {
if (source) fillImageElement(target, source);
} else if (!source) {
requestAnimationFrame(() => {
emptyImageElement(target);
});
if (source) {
fillImageElement(target, source);
}
} else if (!source) {
emptyImageElement(target);
}
}
function onAnimationEnd(event) {
const elem = event.target;
requestAnimationFrame(() => {
const canvas = elem.previousSibling;
if (elem.classList.contains('blurhashed') && canvas && canvas.tagName === 'CANVAS') {
canvas.classList.add('lazy-hidden');
}
});
elem.removeEventListener('animationend', onAnimationEnd);
}
function fillImageElement(elem, url) {
@ -82,6 +110,7 @@ import './style.scss';
preloaderImg.src = url;
elem.classList.add('lazy-hidden');
elem.addEventListener('animationend', onAnimationEnd);
preloaderImg.addEventListener('load', () => {
requestAnimationFrame(() => {
@ -92,25 +121,22 @@ import './style.scss';
}
elem.removeAttribute('data-src');
elem.classList.remove('lazy-hidden');
if (userSettings.enableFastFadein()) {
elem.classList.add('lazy-image-fadein-fast');
} else {
elem.classList.add('lazy-image-fadein');
}
const canvas = elem.previousSibling;
if (elem.classList.contains('blurhashed') && canvas && canvas.tagName === 'CANVAS') {
canvas.classList.remove('lazy-image-fadein-fast', 'lazy-image-fadein');
canvas.classList.add('lazy-hidden');
}
elem.classList.remove('lazy-hidden');
});
});
}
function emptyImageElement(elem) {
// block repeated call - requestAnimationFrame twice for one image
if (elem.getAttribute('data-src')) return;
elem.removeEventListener('animationend', onAnimationEnd);
const canvas = elem.previousSibling;
if (canvas && canvas.tagName === 'CANVAS') {
canvas.classList.remove('lazy-hidden');
}
let url;
@ -125,16 +151,6 @@ import './style.scss';
elem.classList.remove('lazy-image-fadein-fast', 'lazy-image-fadein');
elem.classList.add('lazy-hidden');
const canvas = elem.previousSibling;
if (canvas && canvas.tagName === 'CANVAS') {
canvas.classList.remove('lazy-hidden');
if (userSettings.enableFastFadein()) {
canvas.classList.add('lazy-image-fadein-fast');
} else {
canvas.classList.add('lazy-image-fadein');
}
}
}
export function lazyChildren(elem) {

View file

@ -1,17 +1,3 @@
.lazy-image-fadein {
opacity: 1;
transition: opacity 0.5s;
}
.lazy-image-fadein-fast {
opacity: 1;
transition: opacity 0.1s;
}
.lazy-hidden {
opacity: 0;
}
@keyframes fadein {
from {
opacity: 0;
@ -22,12 +8,18 @@
}
}
.lazy-blurhash-fadein-fast {
.lazy-image-fadein {
opacity: 1;
animation: fadein 0.5s;
}
.lazy-image-fadein-fast {
opacity: 1;
animation: fadein 0.1s;
}
.lazy-blurhash-fadein {
animation: fadein 0.4s;
.lazy-hidden {
opacity: 0;
}
.blurhash-canvas {

View file

@ -13,7 +13,10 @@
callback(entry);
});
},
{rootMargin: '25%'});
{
rootMargin: '50%',
threshold: 0
});
this.observer = observer;
}