Skip to content
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

Merged
merged 26 commits into from Aug 17, 2019
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
75b7ab6
chore(npm): Install blurhash
sorin-davidoi Aug 8, 2019
7dd3ac2
feat(media): Show blurhash
sorin-davidoi Aug 8, 2019
219ff37
fix(media/blurhash): Better sensitive video handling
sorin-davidoi Aug 8, 2019
4dc5a46
feat(media): Preference for using blurhash
sorin-davidoi Aug 8, 2019
ddef2dd
chore(utils/blurhash): Add performance marks
sorin-davidoi Aug 8, 2019
2bebfdd
fix(utils/blurhash): Performance marks
sorin-davidoi Aug 8, 2019
a1f5096
fix(utils/blurhash): Use correct dimension
sorin-davidoi Aug 8, 2019
dcb0c69
refactor(utils/blurhash): Use constant for number of pixels
sorin-davidoi Aug 9, 2019
fdef73b
refactor(media): Simplify logic for displaying blurhash
sorin-davidoi Aug 9, 2019
0562e66
chore(tests/spec): Attempt to adjust sensitivity tests for blurhash
sorin-davidoi Aug 9, 2019
f8f479d
chore(tests/spec): Update sensitivity tests for blurhash
sorin-davidoi Aug 9, 2019
8f25d11
chore(tests/spec): Check for sensitive
sorin-davidoi Aug 9, 2019
5aa04d6
fix(media/blurhash): Handle videos
sorin-davidoi Aug 9, 2019
f080728
fix: Video handling
sorin-davidoi Aug 9, 2019
ba9c396
fix: Videos
sorin-davidoi Aug 9, 2019
557176f
minor refactoring, fix Svelte warning
nolanlawson Aug 10, 2019
dc1d11e
fix: Large inline images and videos
sorin-davidoi Aug 10, 2019
f0306e5
feat(settings): Rename blurhash setting
sorin-davidoi Aug 11, 2019
7af9278
refactor: Use toBlob, block media rendering until blurhash ready
sorin-davidoi Aug 14, 2019
39c24ea
refactor: Move computations to Web Worker
sorin-davidoi Aug 14, 2019
20a6ee8
fix(workers/blurhash): More error handling
sorin-davidoi Aug 15, 2019
66c829a
feat(workers/blurhash): Use quick-lru for caching
sorin-davidoi Aug 15, 2019
421af34
fix: Don't create Context2D needlessly
sorin-davidoi Aug 15, 2019
b711fb6
fix(workers/blurhash): Increase cache size to 100
sorin-davidoi Aug 15, 2019
fa00e04
fix(workers/blurhash): Don't resolve promise twice
sorin-davidoi Aug 15, 2019
6ec0d28
fix(utils/decode-image): Ignore data URLs
sorin-davidoi Aug 15, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -48,6 +48,7 @@
"@webcomponents/custom-elements": "^1.2.4",
"babel-loader": "^8.0.6",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"blurhash": "^1.1.3",
Copy link
Contributor Author

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.

"cheerio": "^1.0.0-rc.2",
"child-process-promise": "^2.2.1",
"chokidar": "^3.0.1",
Expand Down
2 changes: 1 addition & 1 deletion src/routes/_components/LazyImage.html
Expand Up @@ -71,7 +71,7 @@
return `object-position: ${coordsToPercent(focus.x)}% ${100 - coordsToPercent(focus.y)}%;`
},
fillFixSize: ({ forceSize, $largeInlineMedia }) => !$largeInlineMedia && !forceSize,
displaySrc: ({ error, src, fallback }) => ((error && fallback) || src)
displaySrc: ({ showBlurhash, blurhash, error, src, fallback }) => ((showBlurhash && blurhash) || (error && fallback) || src)
}
}
</script>
6 changes: 5 additions & 1 deletion src/routes/_components/status/Media.html
Expand Up @@ -53,6 +53,8 @@
title={description}
src={previewUrl}
fallback={oneTransparentPixel}
showBlurhash={showBlurhash}
blurhash={blurhash}
width={inlineWidth}
height={inlineHeight}
background="var(--loading-bg)"
Expand Down Expand Up @@ -91,8 +93,9 @@
import LazyImage from '../LazyImage.html'
import AutoplayVideo from '../AutoplayVideo.html'
import { registerClickDelegate } from '../../_utils/delegate'
import { decode } from '../../_utils/blurhash'

export default {
export default {
oncreate () {
const { elementId } = this.get()
registerClickDelegate(this, elementId, () => this.onClick())
Expand Down Expand Up @@ -126,6 +129,7 @@
elementId: ({ media, uuid }) => `media-${uuid}-${media.id}`,
description: ({ media }) => media.description || '',
previewUrl: ({ media }) => media.preview_url,
blurhash: ({ media }) => media.blurhash && decode(media.blurhash),
url: ({ media }) => media.url,
type: ({ media }) => media.type
},
Expand Down
6 changes: 5 additions & 1 deletion src/routes/_components/status/MediaAttachments.html
@@ -1,7 +1,7 @@
<div class={computedClass}
style="grid-template-columns: repeat({nCols}, 1fr);" >
{#each mediaAttachments as media, index}
<Media {media} {uuid} {mediaAttachments} {index} />
<Media {media} {uuid} {mediaAttachments} {index} {showBlurhash} />
{/each}
</div>
<style>
Expand Down Expand Up @@ -55,6 +55,10 @@
twoCols && 'two-cols',
!$largeInlineMedia && 'grouped-images'
),
showBlurhash:
({ sensitive, sensitiveShown, mediaAttachments }) => {
return sensitive && mediaAttachments.every(attachment => !!attachment.blurhash) ? !sensitiveShown : false
},
nCols:
({ mediaAttachments, $largeInlineMedia }) => {
return (!$largeInlineMedia && mediaAttachments.length > 1) ? 2 : 1
Expand Down
37 changes: 31 additions & 6 deletions src/routes/_components/status/StatusMediaAttachments.html
Expand Up @@ -10,28 +10,32 @@
<SvgIcon className="status-sensitive-media-svg" href="#fa-eye-slash" />
</div>
</button>
<MediaAttachments {mediaAttachments} {sensitive} {uuid} />
{:else}
<button id={elementId}
type="button"
class="status-sensitive-media-button"
aria-label="Show sensitive media" >

<div class="status-sensitive-media-warning">
Sensitive content. Click to show.
<div class="{customWarningClass}">
<div class="status-sensitive-media-warning-text">
Sensitive content. Click to show.
</div>
</div>
<div class="svg-wrapper">
<SvgIcon className="status-sensitive-media-svg" href="#fa-eye" />
</div>
</button>
{/if}
{#if sensitiveShown || canUseBlurhash}
<MediaAttachments {mediaAttachments} {sensitive} {sensitiveShown} {uuid} />
{/if}
</div>
</div>
{#if enableShortcuts}
<Shortcut scope={shortcutScope} key="y" on:pressed="toggleSensitiveMedia()"/>
{/if}
{:else}
<MediaAttachments {mediaAttachments} {sensitive} {uuid} />
<MediaAttachments {mediaAttachments} {sensitive} {sensitiveShown} {uuid} />
{/if}
<style>
.status-sensitive-media-container {
Expand Down Expand Up @@ -81,6 +85,7 @@
}

.status-sensitive-media-hidden .status-sensitive-media-button {
position: absolute;
right: 0;
bottom: 0;
width: 100%;
Expand All @@ -96,7 +101,6 @@
}

.status-sensitive-media-container .status-sensitive-media-warning {
position: absolute;
top: 0;
left: 0;
right: 0;
Expand All @@ -109,6 +113,21 @@
padding: 0 10px;
}

.status-sensitive-media-container .status-sensitive-media-warning-transparent {
position: absolute;
}

.status-sensitive-media-container .status-sensitive-media-warning-opaque {
background: var(--mask-bg);
height: 100%;
}

.status-sensitive-media-container .status-sensitive-media-warning-transparent .status-sensitive-media-warning-text {
background: var(--mask-bg);
padding: 10px;
border-radius: 6px;
}

.status-sensitive-media-container .svg-wrapper {
display: flex;
align-items: flex-start;
Expand All @@ -119,6 +138,7 @@
}
.status-sensitive-media-hidden .svg-wrapper {
position: absolute;
background: none;
top: 0;
left: 0;
right: 0;
Expand Down Expand Up @@ -171,6 +191,7 @@
$largeInlineMedia ? 'not-grouped-images' : 'grouped-images'
),
mediaAttachments: ({ originalStatus }) => originalStatus.media_attachments,
canUseBlurhash: ({ $useBlurhash, mediaAttachments }) => $useBlurhash && mediaAttachments && mediaAttachments.every(media => !!media.blurhash),
sensitiveShown: ({ $sensitivesShown, uuid }) => !!$sensitivesShown[uuid],
sensitive: ({ originalStatus, $markMediaAsSensitive, $neverMarkMediaAsSensitive }) => (
!$neverMarkMediaAsSensitive && ($markMediaAsSensitive || originalStatus.sensitive)
Expand All @@ -181,7 +202,11 @@
return ''
}
return `padding-bottom: ${Math.ceil(mediaAttachments.length / 2) * 29}%;`
}
},
customWarningClass: ({ canUseBlurhash }) => classname(
'status-sensitive-media-warning',
canUseBlurhash ? 'status-sensitive-media-warning-transparent' : 'status-sensitive-media-warning-opaque'
)
},
methods: {
toggleSensitiveMedia () {
Expand Down
5 changes: 5 additions & 0 deletions src/routes/_pages/settings/general.html
Expand Up @@ -8,6 +8,11 @@ <h2>Media</h2>
bind:checked="$neverMarkMediaAsSensitive" on:change="onChange(event)">
<label for="choice-never-mark-media-sensitive">Show sensitive media by default</label>
</div>
<div class="setting-group">
<input type="checkbox" id="choice-use-blurhash"
bind:checked="$useBlurhash" on:change="onChange(event)">
<label for="choice-use-blurhash">Use blurhash for previews</label>
</div>
<div class="setting-group">
<input type="checkbox" id="choice-mark-media-sensitive"
bind:checked="$markMediaAsSensitive" on:change="onChange(event)">
Expand Down
1 change: 1 addition & 0 deletions src/routes/_store/store.js
Expand Up @@ -30,6 +30,7 @@ const persistedState = {
loggedInInstancesInOrder: [],
markMediaAsSensitive: false,
neverMarkMediaAsSensitive: false,
useBlurhash: true,
omitEmojiInDisplayNames: undefined,
pinnedPages: {},
pushSubscriptions: {},
Expand Down
22 changes: 22 additions & 0 deletions src/routes/_utils/blurhash.js
@@ -0,0 +1,22 @@
import { decode as decodeBlurHash } from 'blurhash'
import { mark, stop } from './marks'

let canvas

export function decode (blurhash) {
mark('computeBlurhash')
const pixels = decodeBlurHash(blurhash, 32, 32)

if (pixels) {
canvas = canvas || document.createElement('canvas')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mastodon renders the canvases directly but I though that it's too much overhead to create and destroy that many canvases so I went with this approach.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Profile sample with 6x CPU throttling.
image

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if toBlob() with a Blob URL may actually be more performant here than a data URL. I'll play around with it. :)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, so the problem with toBlob() is it's asynchronous, meaning that there might be a flash of unhidden sensistive media unless we're careful about doing the blurhashing in advance. I might keep noodling on this after merging the PR.

canvas.height = 32
canvas.width = 32
const imageData = new window.ImageData(pixels, 32, 32)
canvas.getContext('2d').putImageData(imageData, 0, 0)
const base64Image = canvas.toDataURL()
stop('computeBlurhash')
return base64Image
}

stop('computeBlurhash')
}
5 changes: 5 additions & 0 deletions yarn.lock
Expand Up @@ -1489,6 +1489,11 @@ bluebird@^3.5.5:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==

blurhash@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.3.tgz#dc325af7da836d07a0861d830bdd63694382483e"
integrity sha512-yUhPJvXexbqbyijCIE/T2NCXcj9iNPhWmOKbPTuR/cm7Q5snXYIfnVnz6m7MWOXxODMz/Cr3UcVkRdHiuDVRDw==

bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
version "4.11.8"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
Expand Down