refact(encode): rewrite with tasks

This commit is contained in:
wryk 2019-05-20 20:22:27 +02:00
parent b5e48f7566
commit 6e83c88c04
2 changed files with 115 additions and 77 deletions

View file

@ -1,76 +1,103 @@
import EventEmitter from 'eventemitter3' import { calcProgress } from '/services/util.js'
import genericPool from 'generic-pool' import { task, do as taskDo, waitAll } from 'folktale/concurrency/task'
import { promisesProgress, calcProgress } from '/services/util.js'
import { GIF_PALETTE_SIZE } from '/constants.js' import { GIF_PALETTE_SIZE } from '/constants.js'
export function encode ({ imageDataList, imageWidth, imageHeight, delayTime }, { boomerangEffect }) { export function encode ({ imageDataList, imageWidth, imageHeight, delayTime }, { boomerangEffect }, progressCallback) {
const emitter = new EventEmitter() return taskDo(function * () {
const indexedColorImageList = yield quantizeColorImageDataList(
imageDataList,
GIF_PALETTE_SIZE,
(value) => progressCallback(calcProgress(0, 0.9, value))
)
const quantizeColorWorkerPool = genericPool.createPool({ return writeBlob(
create: () => new Worker('/services/quantize-color.worker.js'), imageWidth,
destroy: worker => worker.terminate() imageHeight,
}, { indexedColorImageList,
min: 0, delayTime,
max: 2 boomerangEffect,
(value) => progressCallback(calcProgress(0.9, 1, value))
)
}) })
}
const indexedColorImagesP = imageDataList function quantizeColorImageDataList (imageDataList, paletteSize, progressCallback) {
.map(async imageData => { let complete = 0
const worker = await quantizeColorWorkerPool.acquire()
const indexedColorImage = await new Promise((resolve, reject) => { const tasks = imageDataList
worker.onerror = reject .map(imageData => quantizeColorImageData(imageData, paletteSize))
worker.onmessageerror = reject .map(task => {
worker.onmessage = event => { return task.map(x => {
resolve(event.data) progressCallback(++complete / imageDataList.length)
} return x
worker.postMessage({
imageData,
paletteSize: GIF_PALETTE_SIZE
})
}) })
await quantizeColorWorkerPool.release(worker)
return indexedColorImage
}) })
const progressPromises = promisesProgress(indexedColorImagesP, function (value) { return waitAll(tasks)
emitter.emit('progress', calcProgress(0, 0.9, value)) }
function quantizeColorImageData (imageData, paletteSize) {
return task((resolver) => {
const worker = new Worker('/services/quantize-color.worker.js')
resolver.cleanup(() => {
worker.terminate()
})
worker.onerror = resolver.reject
worker.onmessageerror = resolver.reject
worker.onmessage = event => {
resolver.resolve(event.data)
}
worker.postMessage({
imageData,
paletteSize
})
}) })
}
Promise.all(progressPromises).then(indexedColorImages => { function writeBlob (
const writeWorker = new Worker('/services/write-gif.worker.js') imageWidth,
imageHeight,
indexedColorImages,
delayTime,
boomerangEffect,
onProgress
) {
return task((resolver) => {
const worker = new Worker('/services/write-gif.worker.js')
writeWorker.onerror = error => emitter.emit('error', error) resolver.cleanup(() => {
writeWorker.onmessageerror = error => emitter.emit('error', error) worker.terminate()
})
writeWorker.onmessage = event => { worker.onerror = resolver.reject
worker.onmessageerror = resolver.reject
worker.onmessage = event => {
const { type, payload } = event.data const { type, payload } = event.data
switch (type) { switch (type) {
default:
emitter.emit('error', new Error(`Unexpected worker message with type ${type}`))
break
case 'progress': case 'progress':
emitter.emit('progress', calcProgress(0.9, 1, payload.value)) onProgress(payload.value)
break return
case 'done': case 'done':
const byteArray = new Uint8Array(payload.buffer) resolver.resolve({
const blob = new Blob([byteArray], { type: 'image/gif' }) blob: new Blob([new Uint8Array(payload.buffer)], { type: 'image/gif' }),
emitter.emit('done', {
blob,
createdAt: new Date() createdAt: new Date()
}) })
break return
default:
resolver.reject(new Error(`Unexpected worker message with type ${type}`))
} }
} }
writeWorker.postMessage({ worker.postMessage({
imageWidth, imageWidth,
imageHeight, imageHeight,
indexedColorImages, indexedColorImages,
@ -78,7 +105,4 @@ export function encode ({ imageDataList, imageWidth, imageHeight, delayTime }, {
boomerangEffect boomerangEffect
}) })
}) })
.catch(error => emitter.emit('error', error))
return emitter
} }

View file

@ -35,6 +35,7 @@ export default {
}, },
data: () => ({ data: () => ({
encoding: false, encoding: false,
encodingExecution: null,
encodingProgress: 0, encodingProgress: 0,
objectUrl: null, objectUrl: null,
downloadReady: false downloadReady: false
@ -54,6 +55,7 @@ export default {
}, },
methods: { methods: {
back () { back () {
this.cancelEncode()
this.$router.push({ name: 'preview' }) this.$router.push({ name: 'preview' })
}, },
backHome () { backHome () {
@ -66,47 +68,59 @@ export default {
URL.revokeObjectURL(this.objectUrl) URL.revokeObjectURL(this.objectUrl)
this.$store.commit('updateGif', null) this.$store.commit('updateGif', null)
}, },
startEncoding () { startEncode () {
this.encoding = true this.encoding = true
const encoding = encode(this.capture, { boomerangEffect: this.boomerang }) this.encodingProgress = 0
encoding.once('error', error => { this.encodingExecution = encode(
console.error(error) this.capture,
{ boomerangEffect: this.boomerang },
(value) => {
this.encodingProgress = value
}
).run()
const cleanup = () => {
this.encoding = false this.encoding = false
this.encodingProgress = 0 this.encodingProgress = 0
}) this.encodingExecution = null
}
encoding.on('progress', value => { this.encodingExecution.listen({
this.encodingProgress = value onCancelled: cleanup,
}) onRejected: cleanup,
onResolved: (gif) => {
cleanup()
this.$store.commit('updateGif', gif)
this.fillGIF()
this.downloadReady = true
encoding.once('done', gif => { if (document.hidden && ('Notification' in window) && Notification.permission === 'granted') {
this.encoding = false const notification = new Notification('You can now download your souvenir', {
this.encodingProgress = 0 body: 'Thank you for your patience.',
this.$store.commit('updateGif', gif) icon: appLogo
this.fillGIF() })
this.downloadReady = true
if (document.hidden && ('Notification' in window) && Notification.permission === 'granted') { notification.addEventListener('click', () => {
const notification = new Notification('You can now download your souvenir', { parent.focus()
body: 'Thank you for your patience.', }, {
icon: appLogo once: true
}) })
}
notification.addEventListener('click', () => {
parent.focus()
}, {
once: true
})
} }
}) })
},
cancelEncode () {
if (this.encodingExecution) {
this.encodingExecution.cancel()
}
} }
}, },
created () { created () {
if (!this.capture) { if (!this.capture) {
this.$router.push({ name: 'home' }) this.$router.push({ name: 'home' })
} else if (!this.gif) { } else if (!this.gif) {
this.startEncoding() this.startEncode()
} else { } else {
this.fillGIF() this.fillGIF()
} }