diff --git a/lib/Listener/CSPListener.php b/lib/Listener/CSPListener.php index 491a0d6fe92..d430a68bb44 100644 --- a/lib/Listener/CSPListener.php +++ b/lib/Listener/CSPListener.php @@ -50,6 +50,8 @@ public function handle(Event $event): void { $csp->addAllowedConnectDomain($server); } + $csp->addAllowedWorkerSrcDomain('\'self\''); + $event->addPolicy($csp); } } diff --git a/src/components/CallView/Grid/Grid.vue b/src/components/CallView/Grid/Grid.vue index e15a9e2f531..1cccb80ddc4 100644 --- a/src/components/CallView/Grid/Grid.vue +++ b/src/components/CallView/Grid/Grid.vue @@ -372,7 +372,7 @@ export default { // Blur radius for each background in the grid videoBackgroundBlur() { - return this.$store.getters.getBlurFilter(this.videoWidth, this.videoHeight) + return this.$store.getters.getBlurRadius(this.videoWidth, this.videoHeight) }, }, diff --git a/src/components/CallView/shared/Video.vue b/src/components/CallView/shared/Video.vue index 6c17f358841..ca10f3e6b3e 100644 --- a/src/components/CallView/shared/Video.vue +++ b/src/components/CallView/shared/Video.vue @@ -150,8 +150,8 @@ export default { }, // Calculated once in the grid component for each video background videoBackgroundBlur: { - type: String, - default: '', + type: Number, + default: 0, }, }, diff --git a/src/components/CallView/shared/VideoBackground.vue b/src/components/CallView/shared/VideoBackground.vue index 523e61da5ca..e75325c66c6 100644 --- a/src/components/CallView/shared/VideoBackground.vue +++ b/src/components/CallView/shared/VideoBackground.vue @@ -25,14 +25,15 @@ ref="darkener" class="darken">
{ + createImageBitmap(image).then(imageBitmap => { + this.blurredBackgroundImageSource = imageBitmap + }) + } + image.src = this.backgroundImageUrl + }, + }, + generatedBackgroundBlur: { + immediate: true, + handler() { + if (this.generatedBackgroundBlur === false) { + return + } + + this.generateBlurredBackgroundImage() + }, + }, }, async beforeMount() { + if (this.isChrome) { + this.useCssBlurFilter = false + } + if (!this.user) { return } @@ -142,10 +222,68 @@ export default { } }, + beforeDestroy() { + this.isDestroyed = true + }, + methods: { // Calculate the background blur based on the height of the background element setBlur({ width, height }) { - this.blur = this.$store.getters.getBlurFilter(width, height) + this.blur = this.$store.getters.getBlurRadius(width, height) + }, + + generateBlurredBackgroundImage() { + // Reset image source so the width and height are adjusted to + // the element rather than to the previous image being shown. + this.$refs.backgroundImage.src = '' + + let width = this.$refs.backgroundImage.width + let height = this.$refs.backgroundImage.height + + // Restore the current background so it is shown instead of an empty + // background while the new one is being generated. + this.$refs.backgroundImage.src = this.blurredBackgroundImage + + const sourceAspectRatio = this.blurredBackgroundImageSource.width / this.blurredBackgroundImageSource.height + const canvasAspectRatio = width / height + + if (canvasAspectRatio > sourceAspectRatio) { + height = width / sourceAspectRatio + } else if (canvasAspectRatio < sourceAspectRatio) { + width = height * sourceAspectRatio + } + + const cacheId = this.backgroundImageUrl + '-' + width + '-' + height + '-' + this.backgroundBlur + if (this.blurredBackgroundImageCache[cacheId]) { + this.blurredBackgroundImage = this.blurredBackgroundImageCache[cacheId] + + return + } + + if (this.pendingGenerateBlurredBackgroundImageCount) { + this.pendingGenerateBlurredBackgroundImageCount++ + + return + } + + this.pendingGenerateBlurredBackgroundImageCount = 1 + + blur(this.blurredBackgroundImageSource, width, height, this.backgroundBlur).then(image => { + if (this.isDestroyed) { + return + } + + this.blurredBackgroundImage = image + this.blurredBackgroundImageCache[cacheId] = this.blurredBackgroundImage + + const generateBlurredBackgroundImageCalledAgain = this.pendingGenerateBlurredBackgroundImageCount > 1 + + this.pendingGenerateBlurredBackgroundImageCount = 0 + + if (generateBlurredBackgroundImageCalledAgain) { + this.generateBlurredBackgroundImage() + } + }) }, }, } diff --git a/src/store/callViewStore.js b/src/store/callViewStore.js index 52eb576ed95..d3bb89e99bb 100644 --- a/src/store/callViewStore.js +++ b/src/store/callViewStore.js @@ -33,8 +33,12 @@ const getters = { selectedVideoPeerId: (state) => { return state.selectedVideoPeerId }, - getBlurFilter: (state) => (width, height) => { - return `filter: blur(${(width * height * state.videoBackgroundBlur) / 1000}px)` + /** + * @param {object} state the width and height to calculate the radius from + * @returns {number} the blur radius to use, in pixels + */ + getBlurRadius: (state) => (width, height) => { + return (width * height * state.videoBackgroundBlur) / 1000 }, } diff --git a/src/utils/imageBlurrer.js b/src/utils/imageBlurrer.js new file mode 100644 index 00000000000..8f8705cb3ca --- /dev/null +++ b/src/utils/imageBlurrer.js @@ -0,0 +1,76 @@ +/** + * + * @copyright Copyright (c) 2020, Daniel Calviño Sánchez (danxuliu@gmail.com) + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { generateFilePath } from '@nextcloud/router' + +const worker = new Worker(generateFilePath('spreed', '', 'js/image-blurrer-worker.js')) + +const pendingResults = {} +let pendingResultsNextId = 0 + +worker.onmessage = function(message) { + const pendingResult = pendingResults[message.data.id] + if (!pendingResult) { + console.debug('No pending result for blurring image with id ' + message.data.id) + + return + } + + pendingResult(message.data.blurredImageAsDataUrl) + + delete pendingResults[message.data.id] +} + +function blurSync(image, width, height, blurRadius) { + return new Promise((resolve, reject) => { + const canvas = document.createElement('canvas') + canvas.width = width + canvas.height = height + + const context = canvas.getContext('2d') + context.filter = `blur(${blurRadius}px)` + context.drawImage(image, 0, 0, canvas.width, canvas.height) + + resolve(canvas.toDataURL()) + }) +} + +export default function blur(image, width, height, blurRadius) { + if (typeof OffscreenCanvas === 'undefined') { + return blurSync(image, width, height, blurRadius) + } + + const id = pendingResultsNextId + + pendingResultsNextId++ + + return new Promise((resolve, reject) => { + pendingResults[id] = resolve + + worker.postMessage({ + id: id, + image: image, + width: width, + height: height, + blurRadius: blurRadius, + }) + }) +} diff --git a/src/utils/imageBlurrerWorker.js b/src/utils/imageBlurrerWorker.js new file mode 100644 index 00000000000..756da1ff0a2 --- /dev/null +++ b/src/utils/imageBlurrerWorker.js @@ -0,0 +1,37 @@ +/** + * + * @copyright Copyright (c) 2020, Daniel Calviño Sánchez (danxuliu@gmail.com) + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +const fileReaderSync = new global.FileReaderSync() + +onmessage = function(message) { + const offscreenCanvas = new OffscreenCanvas(message.data.width, message.data.height) + + const context = offscreenCanvas.getContext('2d') + context.filter = `blur(${message.data.blurRadius}px)` + context.drawImage(message.data.image, 0, 0, offscreenCanvas.width, offscreenCanvas.height) + + offscreenCanvas.convertToBlob().then(blob => { + postMessage({ + id: message.data.id, + blurredImageAsDataUrl: fileReaderSync.readAsDataURL(blob), + }) + }) +} diff --git a/webpack.common.js b/webpack.common.js index 42f733aa7da..8fd83749819 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -7,6 +7,11 @@ module.exports = { entry: { 'admin-settings': path.join(__dirname, 'src', 'mainAdminSettings.js'), 'collections': path.join(__dirname, 'src', 'collections.js'), + // There is a "worker-loader" plugin for Webpack, but I was not able to + // get it to work ("publicPath" uses "output.publicPath" rather than the + // one set in the plugin + // https://github.com/webpack-contrib/worker-loader/issues/281). + 'image-blurrer-worker': path.join(__dirname, 'src', 'utils/imageBlurrerWorker.js'), 'talk': path.join(__dirname, 'src', 'main.js'), 'talk-files-sidebar': [ path.join(__dirname, 'src', 'mainFilesSidebar.js'),