mirror of
https://github.com/GuerillaStudio/souvenir.git
synced 2024-11-09 16:51:53 +00:00
add capture and encode (this one is borken ah ah)
nik les workers ofc
This commit is contained in:
parent
beefe38b3b
commit
d468ad3728
10 changed files with 308 additions and 52 deletions
56
package-lock.json
generated
56
package-lock.json
generated
|
@ -5293,8 +5293,7 @@
|
|||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
|
@ -5315,14 +5314,12 @@
|
|||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
|
@ -5337,20 +5334,17 @@
|
|||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
|
@ -5467,8 +5461,7 @@
|
|||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
|
@ -5480,7 +5473,6 @@
|
|||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
|
@ -5495,7 +5487,6 @@
|
|||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
|
@ -5503,14 +5494,12 @@
|
|||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
|
@ -5529,7 +5518,6 @@
|
|||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
|
@ -5610,8 +5598,7 @@
|
|||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
|
@ -5623,7 +5610,6 @@
|
|||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
|
@ -5709,8 +5695,7 @@
|
|||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
|
@ -5746,7 +5731,6 @@
|
|||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
|
@ -5766,7 +5750,6 @@
|
|||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
|
@ -5810,14 +5793,12 @@
|
|||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -5869,6 +5850,11 @@
|
|||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"gif-writer": {
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/gif-writer/-/gif-writer-0.9.3.tgz",
|
||||
"integrity": "sha1-0nbwlRBKMqC557tl4Mn9QPs1bQ8="
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
|
||||
|
@ -11626,6 +11612,16 @@
|
|||
"errno": "~0.1.7"
|
||||
}
|
||||
},
|
||||
"worker-loader": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-2.0.0.tgz",
|
||||
"integrity": "sha512-tnvNp4K3KQOpfRnD20m8xltE3eWh89Ye+5oj7wXEEHKac1P4oZ6p9oTj8/8ExqoSBnk9nu5Pr4nKfQ1hn2APJw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loader-utils": "^1.0.0",
|
||||
"schema-utils": "^0.4.0"
|
||||
}
|
||||
},
|
||||
"wrap-ansi": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"gif-writer": "^0.9.3",
|
||||
"register-service-worker": "^1.6.2",
|
||||
"vue": "^2.6.6",
|
||||
"vuex": "^3.0.1"
|
||||
|
@ -21,6 +22,7 @@
|
|||
"babel-eslint": "^10.0.1",
|
||||
"eslint": "^5.8.0",
|
||||
"eslint-plugin-vue": "^5.0.0",
|
||||
"vue-template-compiler": "^2.5.21"
|
||||
"vue-template-compiler": "^2.5.21",
|
||||
"worker-loader": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,10 +27,6 @@ export default {
|
|||
},
|
||||
mounted: function () {
|
||||
this.makeLoading()
|
||||
window.setTimeout(() => {
|
||||
this.$store.commit('stopEncoding')
|
||||
this.$store.commit('startDownloading')
|
||||
}, 2000)
|
||||
},
|
||||
destroyed: function () {
|
||||
window.clearTimeout(this.interval)
|
||||
|
|
73
src/services/capture.js
Normal file
73
src/services/capture.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
import {
|
||||
makeRectangle,
|
||||
crop
|
||||
} from './rectangle.js'
|
||||
|
||||
const FRAMES_PER_SECOND = 10
|
||||
const WIDTH = 200
|
||||
const HEIGHT = WIDTH
|
||||
|
||||
const video = document.createElement('video')
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
const canvasContext = canvas.getContext('2d')
|
||||
|
||||
canvas.width = WIDTH
|
||||
canvas.height = HEIGHT
|
||||
|
||||
export function capture (commit, mediaStream, duration) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const totalFrames = duration / 1000 * FRAMES_PER_SECOND
|
||||
|
||||
if (totalFrames < 1) {
|
||||
resolve([])
|
||||
}
|
||||
|
||||
const delayTime = 1000 / FRAMES_PER_SECOND
|
||||
|
||||
video.srcObject = mediaStream
|
||||
|
||||
const soureRectangle = crop(makeRectangle(0, 0, video.videoWidth, video.videoHeight))
|
||||
const destinationRectangle = makeRectangle(0, 0, canvas.width, canvas.height)
|
||||
|
||||
const imageDataList = []
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
if (imageDataList.length < totalFrames) {
|
||||
console.log(`Capturing frame ${imageDataList.length} / ${totalFrames}`)
|
||||
|
||||
canvasContext.drawImage(
|
||||
video,
|
||||
soureRectangle.x,
|
||||
soureRectangle.y,
|
||||
soureRectangle.width,
|
||||
soureRectangle.height,
|
||||
destinationRectangle.x,
|
||||
destinationRectangle.y,
|
||||
destinationRectangle.width,
|
||||
destinationRectangle.height
|
||||
)
|
||||
|
||||
const imageData = canvasContext.getImageData(
|
||||
destinationRectangle.x,
|
||||
destinationRectangle.y,
|
||||
destinationRectangle.width,
|
||||
destinationRectangle.height
|
||||
)
|
||||
|
||||
imageDataList.push(imageData)
|
||||
|
||||
commit('updateCaptureState', imageDataList.length / totalFrames * 100)
|
||||
} else {
|
||||
clearInterval(intervalId)
|
||||
|
||||
resolve({
|
||||
imageDataList,
|
||||
imageWidth: WIDTH,
|
||||
imageHeight: HEIGHT,
|
||||
delayTime
|
||||
})
|
||||
}
|
||||
}, delayTime)
|
||||
})
|
||||
}
|
38
src/services/encode.js
Normal file
38
src/services/encode.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
import EncodeWorker from './encode.worker.js'
|
||||
|
||||
const PALETTE_SIZE = 255
|
||||
|
||||
export function encode (imageDataList, imageWidth, imageHeight, paletteSize, delayTime) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new EncodeWorker()
|
||||
|
||||
worker.onerror = error => reject(error)
|
||||
|
||||
worker.onmessage = event => {
|
||||
const { type, payload } = event.data
|
||||
|
||||
switch (type) {
|
||||
default:
|
||||
reject(new Error(`Unexpected EncodeWorker message with type ${type}`))
|
||||
break
|
||||
|
||||
case 'progress':
|
||||
console.log(`Encoding progress : ${payload.value}`)
|
||||
break
|
||||
|
||||
case 'done':
|
||||
const dataUrl = 'data:image/gif;base64,' + btoa(payload.buffer.map((b) => String.fromCharCode(b)).join(''))
|
||||
resolve(dataUrl)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
worker.postMessage({
|
||||
imageDataList,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
paletteSize: PALETTE_SIZE,
|
||||
delayTime
|
||||
})
|
||||
})
|
||||
}
|
104
src/services/encode.worker.js
Normal file
104
src/services/encode.worker.js
Normal file
|
@ -0,0 +1,104 @@
|
|||
import {
|
||||
GifWriter,
|
||||
MedianCutColorReducer,
|
||||
IndexedColorImage
|
||||
} from 'gif-writer'
|
||||
|
||||
onmessage = (event) => {
|
||||
console.log(event.data)
|
||||
const { imageDataList, imageWidth, imageHeight, paletteSize, delayTime } = event.data
|
||||
|
||||
console.log('Write GIF')
|
||||
|
||||
const outputStream = new OutputStream()
|
||||
const writer = new GifWriter(outputStream)
|
||||
|
||||
postProgressMessage(0)
|
||||
|
||||
console.log(`Write header`)
|
||||
writer.writeHeader()
|
||||
|
||||
console.log(`Write logical screen informations`)
|
||||
writer.writeLogicalScreenInfo({
|
||||
width: imageWidth,
|
||||
height: imageHeight
|
||||
})
|
||||
|
||||
writer.writeLoopControlInfo(0)
|
||||
|
||||
const indexedColorImages = imageDataList.map((imageData, index, { length }) => {
|
||||
console.log(`Convert frame ${index} ImageData to IndexedColorImage`)
|
||||
const indexedColorImage = imageDataToIndexedColorImage(imageData, paletteSize)
|
||||
postProgressMessage(calcProgress(0, 0.9, length, index + 1))
|
||||
return indexedColorImage
|
||||
})
|
||||
|
||||
indexedColorImages.forEach((indexedColorImage, index, { length }) => {
|
||||
console.log(`Write frame IndexedColorImage ${index}`)
|
||||
writer.writeTableBasedImageWithGraphicControl(indexedColorImage, { delayTimeInMS: delayTime })
|
||||
postProgressMessage(calcProgress(0.9, 1, length, index + 1))
|
||||
})
|
||||
|
||||
console.log(`Write trailer`)
|
||||
writer.writeTrailer()
|
||||
|
||||
postDoneMessage(outputStream.buffer)
|
||||
}
|
||||
|
||||
class OutputStream {
|
||||
constructor () {
|
||||
this.buffer = []
|
||||
}
|
||||
|
||||
writeByte (b) {
|
||||
this.buffer.push(b)
|
||||
}
|
||||
|
||||
writeBytes (bb) {
|
||||
Array.prototype.push.apply(this.buffer, bb)
|
||||
}
|
||||
}
|
||||
|
||||
function imageDataToIndexedColorImage (imageData, paletteSize) {
|
||||
var reducer = new MedianCutColorReducer(imageData, paletteSize)
|
||||
var paletteData = reducer.process()
|
||||
var dat = Array.prototype.slice.call(imageData.data)
|
||||
|
||||
var indexedColorImageData = []
|
||||
|
||||
for (var idx = 0, len = dat.length; idx < len; idx += 4) {
|
||||
var d = dat.slice(idx, idx + 4) // r,g,b,a
|
||||
indexedColorImageData.push(reducer.map(d[0], d[1], d[2]))
|
||||
}
|
||||
|
||||
return new IndexedColorImage(
|
||||
{
|
||||
width: imageData.width,
|
||||
height: imageData.height
|
||||
},
|
||||
indexedColorImageData,
|
||||
paletteData
|
||||
)
|
||||
}
|
||||
|
||||
function calcProgress (from, to, steps, current) {
|
||||
return from + ((to - from) / steps * current)
|
||||
}
|
||||
|
||||
function postProgressMessage (value) {
|
||||
postMessage({
|
||||
type: 'progress',
|
||||
payload: {
|
||||
value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function postDoneMessage (buffer) {
|
||||
postMessage({
|
||||
type: 'done',
|
||||
payload: {
|
||||
buffer
|
||||
}
|
||||
})
|
||||
}
|
18
src/services/rectangle.js
Normal file
18
src/services/rectangle.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
export function makeRectangle (x, y, width, height) {
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height
|
||||
}
|
||||
}
|
||||
|
||||
export function crop ({ x, y, width: w, height: h }) {
|
||||
if (w < h) {
|
||||
return makeRectangle((h - w) / 2, y, h, h)
|
||||
} else if (w > h) {
|
||||
return makeRectangle((w - h) / 2, y, h, h)
|
||||
} else {
|
||||
return makeRectangle(x, y, w, h)
|
||||
}
|
||||
}
|
33
src/store.js
33
src/store.js
|
@ -1,6 +1,9 @@
|
|||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
import { capture } from './services/capture.js'
|
||||
import { encode } from './services/encode.js'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export default new Vuex.Store({
|
||||
|
@ -22,7 +25,7 @@ export default new Vuex.Store({
|
|||
}
|
||||
},
|
||||
mutations: {
|
||||
updateMediaStream(store, mediaStream) {
|
||||
updateMediaStream (store, mediaStream) {
|
||||
if (store.mediaStream) {
|
||||
store.mediaStream.getTracks().forEach(track => track.stop())
|
||||
}
|
||||
|
@ -61,6 +64,34 @@ export default new Vuex.Store({
|
|||
commit('updateMediaStream', mediaStream)
|
||||
})
|
||||
.catch(error => console.error(error))
|
||||
},
|
||||
capture ({ commit, dispatch, state }) {
|
||||
commit('startCapture')
|
||||
|
||||
capture(commit, state.mediaStream, state.timer.selected * 1000)
|
||||
.then(captureData => {
|
||||
commit('stopCapture')
|
||||
commit('updateCaptureState', 0)
|
||||
dispatch('encode', captureData)
|
||||
})
|
||||
.catch(error => console.error(error))
|
||||
},
|
||||
encode ({ commit }, captureData) {
|
||||
commit('startEncoding')
|
||||
|
||||
console.log(captureData)
|
||||
|
||||
encode(captureData)
|
||||
.then(clipDataUrl => {
|
||||
commit('stopEncoding')
|
||||
commit('startDownloading')
|
||||
console.log(clipDataUrl)
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error)
|
||||
commit('stopEncoding')
|
||||
commit('startDownloading')
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -39,25 +39,11 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
startCapture () {
|
||||
this.$store.commit('startCapture')
|
||||
this.fakeCapture()
|
||||
},
|
||||
fakeCapture () {
|
||||
const interval = (this.timer.selected * 1000) / 100
|
||||
const fakeProgress = window.setInterval(() => {
|
||||
if (this.capturing.state < 100) {
|
||||
this.$store.commit('updateCaptureState', this.capturing.state + 1)
|
||||
} else {
|
||||
window.clearInterval(fakeProgress)
|
||||
this.$store.commit('stopCapture')
|
||||
this.$store.commit('updateCaptureState', 0)
|
||||
this.$store.commit('startEncoding')
|
||||
}
|
||||
}, interval)
|
||||
this.$store.dispatch('capture')
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
this.$refs.preview.srcObject = this.mediaStream
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -8,5 +8,17 @@ module.exports = {
|
|||
workboxOptions: {
|
||||
importWorkboxFrom: 'local'
|
||||
}
|
||||
},
|
||||
configureWebpack: {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.worker\.js$/,
|
||||
use: {
|
||||
loader: 'worker-loader'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue