New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(media): Blurhash #1381
feat(media): Blurhash #1381
Changes from 20 commits
75b7ab6
7dd3ac2
219ff37
4dc5a46
ddef2dd
2bebfdd
a1f5096
dcb0c69
fdef73b
0562e66
f8f479d
8f25d11
5aa04d6
f080728
ba9c396
557176f
dc1d11e
f0306e5
7af9278
39c24ea
20a6ee8
66c829a
421af34
b711fb6
fa00e04
6ec0d28
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,40 @@ | ||
{#if type === 'video' || type === 'audio'} | ||
<button id={elementId} | ||
type="button" | ||
class="play-video-button focus-after {$largeInlineMedia ? '' : 'fixed-size'} {type === 'audio' ? 'play-audio-button' : ''}" | ||
aria-label="Play video: {description}" | ||
style="width: {inlineWidth}px; height: {inlineHeight}px;"> | ||
<PlayVideoIcon /> | ||
{#if blurhash} | ||
{#if type === 'video'} | ||
<LazyImage | ||
alt={description} | ||
title={description} | ||
src={previewUrl} | ||
fallback={oneTransparentPixel} | ||
blurhash={blurhash} | ||
width={inlineWidth} | ||
height={inlineHeight} | ||
background="var(--loading-bg)" | ||
{focus} | ||
/> | ||
{/if} | ||
</button> | ||
{:else} | ||
<button id={elementId} | ||
type="button" | ||
class="play-video-button focus-after {$largeInlineMedia ? '' : 'fixed-size'} {type === 'audio' ? 'play-audio-button' : ''}" | ||
aria-label="Play video: {description}" | ||
style="width: {inlineWidth}px; height: {inlineHeight}px;"> | ||
<PlayVideoIcon /> | ||
{#if type === 'video'} | ||
<LazyImage | ||
alt={description} | ||
title={description} | ||
src={previewUrl} | ||
fallback={oneTransparentPixel} | ||
blurhash={blurhash} | ||
width={inlineWidth} | ||
height={inlineHeight} | ||
background="var(--loading-bg)" | ||
{focus} | ||
/> | ||
{/if} | ||
</button> | ||
{/if} | ||
{:else} | ||
<button id={elementId} | ||
type="button" | ||
|
@@ -40,6 +57,7 @@ | |
class={noNativeWidthHeight ? 'no-native-width-height' : ''} | ||
label="Animated GIF: {description}" | ||
poster={previewUrl} | ||
blurhash={blurhash} | ||
src={url} | ||
staticSrc={previewUrl} | ||
width={inlineWidth} | ||
|
@@ -53,6 +71,7 @@ | |
title={description} | ||
src={previewUrl} | ||
fallback={oneTransparentPixel} | ||
blurhash={blurhash} | ||
width={inlineWidth} | ||
height={inlineHeight} | ||
background="var(--loading-bg)" | ||
|
@@ -91,11 +110,16 @@ | |
import LazyImage from '../LazyImage.html' | ||
import AutoplayVideo from '../AutoplayVideo.html' | ||
import { registerClickDelegate } from '../../_utils/delegate' | ||
import { decode } from '../../_utils/blurhash' | ||
|
||
export default { | ||
oncreate () { | ||
const { elementId } = this.get() | ||
export default { | ||
async oncreate () { | ||
const { elementId, media } = this.get() | ||
registerClickDelegate(this, elementId, () => this.onClick()) | ||
|
||
if (media.blurhash) { | ||
this.set({ decodedBlurhash: await decode(media.blurhash) }) | ||
} | ||
}, | ||
computed: { | ||
focus: ({ meta }) => meta && meta.focus, | ||
|
@@ -126,6 +150,7 @@ | |
elementId: ({ media, uuid }) => `media-${uuid}-${media.id}`, | ||
description: ({ media }) => media.description || '', | ||
previewUrl: ({ media }) => media.preview_url, | ||
blurhash: ({ showBlurhash, decodedBlurhash }) => showBlurhash && decodedBlurhash, | ||
url: ({ media }) => media.url, | ||
type: ({ media }) => media.type | ||
}, | ||
|
@@ -141,6 +166,7 @@ | |
}, | ||
data: () => ({ | ||
oneTransparentPixel: ONE_TRANSPARENT_PIXEL, | ||
decodedBlurhash: ONE_TRANSPARENT_PIXEL, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good workaround! |
||
mouseover: void 0 | ||
}), | ||
store: () => store, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import BlurhashWorker from 'worker-loader!../_workers/blurhash' // eslint-disable-line | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I couldn't get There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's fine; I don't really care whether the |
||
|
||
const RESOLUTION = 32 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could maybe lower this based on some heuristics (e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure what the limiting factor is for blurhash, but yeah for now a hard-coded value is probably fine. |
||
let worker | ||
let canvas | ||
|
||
export function init () { | ||
worker = worker || new BlurhashWorker() | ||
} | ||
|
||
export async function decode (blurhash) { | ||
return new Promise((resolve, reject) => { | ||
try { | ||
init() | ||
|
||
const onMessage = ({ data: { encoded, decoded, imageData, error } }) => { | ||
if (encoded !== blurhash) { | ||
return | ||
} | ||
|
||
worker.removeEventListener('message', onMessage) | ||
|
||
if (error) { | ||
return reject(error) | ||
} | ||
|
||
if (decoded) { | ||
resolve(decoded) | ||
} else { | ||
if (!canvas) { | ||
canvas = document.createElement('canvas') | ||
canvas.height = RESOLUTION | ||
canvas.width = RESOLUTION | ||
} | ||
|
||
canvas.getContext('2d').putImageData(imageData, 0, 0) | ||
canvas.toBlob(blob => { | ||
resolve(URL.createObjectURL(blob)) | ||
}) | ||
} | ||
} | ||
|
||
worker.addEventListener('message', onMessage) | ||
worker.postMessage({ encoded: blurhash }) | ||
} catch (e) { | ||
reject(e) | ||
} | ||
}) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { decode as decodeBlurHash } from 'blurhash' | ||
|
||
const RESOLUTION = 32 | ||
const OFFSCREEN_CANVAS = typeof OffscreenCanvas === 'function' | ||
? new OffscreenCanvas(RESOLUTION, RESOLUTION) : null | ||
|
||
self.addEventListener('message', ({ data: { encoded } }) => { | ||
try { | ||
const pixels = decodeBlurHash(encoded, RESOLUTION, RESOLUTION) | ||
|
||
if (pixels) { | ||
const imageData = new ImageData(pixels, RESOLUTION, RESOLUTION) | ||
|
||
if (OFFSCREEN_CANVAS) { | ||
OFFSCREEN_CANVAS.getContext('2d').putImageData(imageData, 0, 0) | ||
OFFSCREEN_CANVAS.convertToBlob().then(blob => { | ||
postMessage({ encoded, decoded: URL.createObjectURL(blob), imageData: null, error: null }) | ||
}) | ||
} else { | ||
postMessage({ encoded, imageData, decoded: null, error: null }) | ||
} | ||
} | ||
} catch (error) { | ||
postMessage({ encoded, decoded: null, imageData: null, error }) | ||
} | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could try https://github.com/fpapado/blurhash-rust-wasm if performance turns out to be an issue.