add capture and encode (this one is borken ah ah)

nik les workers ofc
This commit is contained in:
wryk 2019-03-10 04:13:25 +01:00
parent beefe38b3b
commit d468ad3728
10 changed files with 308 additions and 52 deletions

56
package-lock.json generated
View file

@ -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",

View file

@ -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"
}
}

View file

@ -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
View 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
View 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
})
})
}

View 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
View 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)
}
}

View file

@ -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')
})
}
}
})

View file

@ -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>

View file

@ -8,5 +8,17 @@ module.exports = {
workboxOptions: {
importWorkboxFrom: 'local'
}
},
configureWebpack: {
module: {
rules: [
{
test: /\.worker\.js$/,
use: {
loader: 'worker-loader'
}
}
]
}
}
}