From b6bdd71dbc10ab99fae3918f3d659658912f6561 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Sat, 6 Jun 2020 11:40:01 -0700 Subject: [PATCH 1/5] initial GIF Encoder implementation --- examples/terrain/app.js | 4 +- modules/core/package.json | 2 + modules/core/src/capture/video-capture.js | 4 ++ modules/core/src/encoders/index.js | 1 + .../core/src/encoders/video/gif-encoder.js | 40 +++++++++++++++++++ modules/core/src/index.js | 3 +- modules/core/test/keyframes/utils.spec.js | 6 +-- yarn.lock | 38 +++++++++++++++++- 8 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 modules/core/src/encoders/video/gif-encoder.js diff --git a/examples/terrain/app.js b/examples/terrain/app.js index bfa39ef3..9cfea67d 100644 --- a/examples/terrain/app.js +++ b/examples/terrain/app.js @@ -1,6 +1,6 @@ import React, {useState, useRef} from 'react'; import DeckGL from '@deck.gl/react'; -import {DeckAdapter, WebMEncoder} from '@hubble.gl/core'; +import {DeckAdapter, GifEncoder} from '@hubble.gl/core'; import {useNextFrame, BasicControls} from '@hubble.gl/react'; import {sceneBuilder} from './scene'; @@ -12,7 +12,7 @@ const INITIAL_VIEW_STATE = { pitch: 60 }; -const adapter = new DeckAdapter(sceneBuilder, WebMEncoder); +const adapter = new DeckAdapter(sceneBuilder, GifEncoder, {framerate: 15}); export default function App() { const deckgl = useRef(null); diff --git a/modules/core/package.json b/modules/core/package.json index 8b150227..feb064f2 100644 --- a/modules/core/package.json +++ b/modules/core/package.json @@ -30,6 +30,8 @@ "build-bundle": "webpack --display=minimal --config ../../scripts/bundle.config.js" }, "dependencies": { + "@loaders.gl/core": "^2.1.6", + "@loaders.gl/video": "2.2.0-alpha.1", "@luma.gl/engine": "^8.1.2", "downloadjs": "^1.4.7", "popmotion": "^8.6.10", diff --git a/modules/core/src/capture/video-capture.js b/modules/core/src/capture/video-capture.js index 8d3d1473..480495a0 100644 --- a/modules/core/src/capture/video-capture.js +++ b/modules/core/src/capture/video-capture.js @@ -92,6 +92,7 @@ export class VideoCapture { // Start recording. Pass in lengthMs of recording and on-stop callback. start(startTimeMs = 0, filename = undefined) { + console.time('capture'); console.log(`Starting recording for ${this.recordingLengthMs}ms.`); this.filename = filename || guid(); this.timeMs = startTimeMs; @@ -134,17 +135,20 @@ export class VideoCapture { * @param {{ (blob: Blob): boolean }} [callback] */ save(callback) { + console.timeEnd('capture'); if (!callback) { /** * @param {Blob} blob */ callback = blob => { + console.timeEnd('save'); if (blob) { download(blob, this.filename + this.encoder.extension, this.encoder.mimeType); } return false; }; } + console.time('save'); this.encoder.save().then(callback); } } diff --git a/modules/core/src/encoders/index.js b/modules/core/src/encoders/index.js index 9da0c2bb..17ba7a31 100644 --- a/modules/core/src/encoders/index.js +++ b/modules/core/src/encoders/index.js @@ -26,3 +26,4 @@ export {default as WebMEncoder} from './video/webm-encoder'; export {default as StreamEncoder} from './video/stream-encoder'; export {default as FrameEncoder} from './frame-encoder'; export {default as PreviewEncoder} from './utils/preview-encoder'; +export {default as GifEncoder} from './video/gif-encoder'; diff --git a/modules/core/src/encoders/video/gif-encoder.js b/modules/core/src/encoders/video/gif-encoder.js new file mode 100644 index 00000000..437a3724 --- /dev/null +++ b/modules/core/src/encoders/video/gif-encoder.js @@ -0,0 +1,40 @@ +import {GIFBuilder} from '@loaders.gl/video'; +import FrameEncoder from '../frame-encoder'; + +export default class GifEncoder extends FrameEncoder { + /** @param {import('types').FrameEncoderSettings} settings */ + constructor(settings) { + super(settings); + this.mimeType = 'image/gif'; + this.extension = '.gif'; + this.gifBuilder = null; + + // this.source = settings.source + this.source = 'images'; + + this.start = this.start.bind(this); + this.add = this.add.bind(this); + this.save = this.save.bind(this); + this.dispose = this.dispose.bind(this); + } + + start() { + this.gifBuilder = new GIFBuilder({source: this.source, width: 720, height: 480, numWorkers: 4}); + } + + /** @param {HTMLCanvasElement} canvas */ + async add(canvas) { + if (this.source === 'images') { + const dataUrl = canvas.toDataURL('image/jpeg', 0.8); + await this.gifBuilder.add(dataUrl); + } + } + + async save() { + return this.gifBuilder.build(); + } + + dispose() { + this.gifBuilder = null; + } +} diff --git a/modules/core/src/index.js b/modules/core/src/index.js index 70d37ced..ed379da2 100644 --- a/modules/core/src/index.js +++ b/modules/core/src/index.js @@ -27,7 +27,8 @@ export { PNGEncoder, WebMEncoder, FrameEncoder, - PreviewEncoder + PreviewEncoder, + GifEncoder } from './encoders'; export { diff --git a/modules/core/test/keyframes/utils.spec.js b/modules/core/test/keyframes/utils.spec.js index b5a62186..c451943f 100644 --- a/modules/core/test/keyframes/utils.spec.js +++ b/modules/core/test/keyframes/utils.spec.js @@ -73,11 +73,7 @@ test('Keyframes#merge', t => { const noop = () => {}; const timings1 = [0, 1, 2]; const easings1 = [noop, noop]; - const keyframes1 = [ - {a: '', b: true}, - {a: 'a', b: false}, - {a: '2', b: true} - ]; + const keyframes1 = [{a: '', b: true}, {a: 'a', b: false}, {a: '2', b: true}]; const expectedMergedKeyframes1 = [ [0, {a: '', b: true, ease: undefined}], diff --git a/yarn.lock b/yarn.lock index 8614916f..04e87527 100644 --- a/yarn.lock +++ b/yarn.lock @@ -853,6 +853,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.3.1": + version "7.10.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839" + integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.4.0", "@babel/template@^7.8.3", "@babel/template@^7.8.6": version "7.8.6" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" @@ -1645,6 +1652,30 @@ npmlog "^4.1.2" write-file-atomic "^2.3.0" +"@loaders.gl/core@^2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@loaders.gl/core/-/core-2.1.6.tgz#9c3e0cf5a1aebb672c50876ab75fd35033b8672c" + integrity sha512-1uxlSZ3dKbraWmvsOsBeSfJxR/+mXjDCsDLkl8N9QzXeN1nIer3siDPLqckox4MMsPxPo5uo1Be8iwORAUo3VQ== + dependencies: + "@babel/runtime" "^7.3.1" + "@loaders.gl/loader-utils" "2.1.6" + +"@loaders.gl/loader-utils@2.1.6", "@loaders.gl/loader-utils@^2.1.3": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@loaders.gl/loader-utils/-/loader-utils-2.1.6.tgz#8eb5a9a2215b3c94c3bb548767cbc276e3701a5c" + integrity sha512-V9ZJH60HwSmSIw2jqNDMFpDqZVfy02WZ1/1e2ENw3PODE8ubNqs1bQ8nFj9omqwVMPF7fBVLvzoeoGrpeW0gfQ== + dependencies: + "@babel/runtime" "^7.3.1" + "@probe.gl/stats" "^3.2.1" + +"@loaders.gl/video@2.2.0-alpha.1": + version "2.2.0-alpha.1" + resolved "https://registry.yarnpkg.com/@loaders.gl/video/-/video-2.2.0-alpha.1.tgz#1bb09e9e507605a72c6e8a13b34926b69c717191" + integrity sha512-4NDe3fNPOTS4MvPxlxrbqr/kKgkB9tOVY2in/s/oYBo0uiDK+WSU0AztnyKxF1b/EsHzTEIzhOEhjvSwbgRQgQ== + dependencies: + "@loaders.gl/loader-utils" "^2.1.3" + gifshot "^0.4.5" + "@luma.gl/constants@8.1.2": version "8.1.2" resolved "https://registry.yarnpkg.com/@luma.gl/constants/-/constants-8.1.2.tgz#729fb10ba6dc6946ccb2745f4b4d22c2f4b021d7" @@ -1837,7 +1868,7 @@ "@babel/runtime" "^7.0.0" probe.gl "3.2.1" -"@probe.gl/stats@3.2.1": +"@probe.gl/stats@3.2.1", "@probe.gl/stats@^3.2.1": version "3.2.1" resolved "https://registry.yarnpkg.com/@probe.gl/stats/-/stats-3.2.1.tgz#4e95c75513229ebb01384045e89524a466c022eb" integrity sha512-tXe5krgbodxtVdUVWG4oIicMoCHNGp7QYkaHSDrUeTfJVqYuZz99T6r7qmn0bCo4wQlzn936jJ+QiAltHxatig== @@ -4848,6 +4879,11 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +gifshot@^0.4.5: + version "0.4.5" + resolved "https://registry.yarnpkg.com/gifshot/-/gifshot-0.4.5.tgz#e3cb570203a3b139ff3069d7578098a29c03b0f8" + integrity sha1-48tXAgOjsTn/MGnXV4CYopwDsPg= + git-raw-commits@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.0.tgz#d92addf74440c14bcc5c83ecce3fb7f8a79118b5" From 1bb3ca588f9bdfc32c219fb39e2b3b0e4b5a6c2a Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Sun, 7 Jun 2020 19:26:42 -0700 Subject: [PATCH 2/5] Refactor encoder settings and scene settings api - scene settings are defined at scene load time, so you can set scene resolution and animaiton length there. - encoder settings are defined before every render, so you can set seek options there. --- examples/terrain/app.js | 7 +-- examples/terrain/scene.js | 12 ++++- modules/core/src/adapters/deck-adapter.js | 39 ++++++++++------- modules/core/src/capture/video-capture.js | 53 +++++++++++++++-------- modules/core/src/scene/deck-scene.js | 6 ++- modules/core/src/scene/kepler-scene.js | 6 ++- modules/core/src/types.d.ts | 12 +++-- 7 files changed, 87 insertions(+), 48 deletions(-) diff --git a/examples/terrain/app.js b/examples/terrain/app.js index c3e24d69..711935fc 100644 --- a/examples/terrain/app.js +++ b/examples/terrain/app.js @@ -15,10 +15,9 @@ const INITIAL_VIEW_STATE = { const adapter = new DeckAdapter(sceneBuilder); +/** @type {import('@hubble.gl/core/src/types').EncoderSettings} */ const encoderSettings = { - animationLengthMs: 15000, - startOffsetMs: 0, - framerate: 15, + framerate: 10, webm: { quality: 0.8 }, @@ -36,8 +35,6 @@ export default function App() { return (
{ - const length = 15000; + const lengthMs = 15000; const data = {}; // set up keyframes const keyframes = getKeyframes(animationLoop, data); - return new DeckScene({animationLoop, length, keyframes, data, renderLayers}); + return new DeckScene({ + animationLoop, + keyframes, + data, + renderLayers, + lengthMs, + width: 720, + height: 480 + }); }; diff --git a/modules/core/src/adapters/deck-adapter.js b/modules/core/src/adapters/deck-adapter.js index d27d7da7..65b7be86 100644 --- a/modules/core/src/adapters/deck-adapter.js +++ b/modules/core/src/adapters/deck-adapter.js @@ -32,6 +32,9 @@ export default class DeckAdapter { /** @type {boolean} */ shouldAnimate; + /** + * @param {(animationLoop: any) => DeckScene | Promise} sceneBuilder + */ constructor(sceneBuilder) { this.sceneBuilder = sceneBuilder; this.videoCapture = new VideoCapture(); @@ -53,7 +56,7 @@ export default class DeckAdapter { * @param {(nextTimeMs: number) => void} onNextFrame */ getProps(deckRef, setReady, onNextFrame) { - return { + const props = { viewState: this._getViewState(), layers: this._getLayers(), onAfterRender: () => this._onAfterRender(onNextFrame), @@ -64,16 +67,22 @@ export default class DeckAdapter { }), _animate: this.shouldAnimate }; + + if (this.scene) { + props.width = this.scene.width; + props.height = this.scene.height; + } + return props; } + /** + * @param {typeof import('../encoders').FrameEncoder} Encoder + * @param {import('types').FrameEncoderSettings} encoderSettings + * @param {() => void} onStop + */ render(Encoder = PreviewEncoder, encoderSettings = {}, onStop = undefined) { this.shouldAnimate = true; - - if (!encoderSettings.animationLengthMs) { - encoderSettings.animationLengthMs = this.scene.length; - } - - this.videoCapture.render(Encoder, encoderSettings, onStop); + this.videoCapture.render(Encoder, encoderSettings, this.scene.lengthMs, onStop); this.scene.animationLoop.timeline.setTime(encoderSettings.startOffsetMs); } @@ -106,7 +115,7 @@ export default class DeckAdapter { } _getViewState() { - if (!this.videoCapture || !this.scene) { + if (!this.scene) { return null; } const frame = this.scene.keyframes.camera.getFrame(); @@ -115,7 +124,7 @@ export default class DeckAdapter { } _getLayers() { - if (!this.videoCapture || !this.scene) { + if (!this.scene) { return []; } return this.scene.renderLayers(); @@ -125,12 +134,10 @@ export default class DeckAdapter { * @param {(nextTimeMs: number) => void} proceedToNextFrame */ _onAfterRender(proceedToNextFrame) { - if (this.videoCapture) { - // console.log('after render'); - this.videoCapture.capture(this.deck.canvas, nextTimeMs => { - this.scene.animationLoop.timeline.setTime(nextTimeMs); - proceedToNextFrame(nextTimeMs); - }); - } + // console.log('after render'); + this.videoCapture.capture(this.deck.canvas, nextTimeMs => { + this.scene.animationLoop.timeline.setTime(nextTimeMs); + proceedToNextFrame(nextTimeMs); + }); } } diff --git a/modules/core/src/capture/video-capture.js b/modules/core/src/capture/video-capture.js index 0cae354a..f6c197cf 100644 --- a/modules/core/src/capture/video-capture.js +++ b/modules/core/src/capture/video-capture.js @@ -25,8 +25,6 @@ import {FrameEncoder} from '../encoders'; import {guid} from './utils'; export class VideoCapture { - /** @type {number} */ - recordingLengthMs; /** @type {boolean} */ recording; /** @type {boolean} */ @@ -34,6 +32,10 @@ export class VideoCapture { /** @type {number} */ timeMs; /** @type {number} */ + endTimeMs; + /** @type {number} */ + durationMs; + /** @type {number} */ framerate; /** @type {FrameEncoder} */ encoder; @@ -55,7 +57,11 @@ export class VideoCapture { this.save = this.save.bind(this); } - parseEncoderSettings(encoderSettings) { + /** + * @param {import('types').FrameEncoderSettings} encoderSettings + * @param {number} sceneLengthMs + */ + parseEncoderSettings(encoderSettings, sceneLengthMs) { const parsedSettings = encoderSettings; if (!parsedSettings.startOffsetMs) { @@ -63,17 +69,28 @@ export class VideoCapture { } this.timeMs = parsedSettings.startOffsetMs; - if (!parsedSettings.filename) { - parsedSettings.filename = guid(); + if (parsedSettings.durationMs) { + this.endTimeMs = parsedSettings.startOffsetMs + parsedSettings.durationMs; + } else { + parsedSettings.durationMs = sceneLengthMs - parsedSettings.startOffsetMs; + this.endTimeMs = sceneLengthMs; } - this.filename = parsedSettings.filename; - - if (parsedSettings.animationLengthMs <= 0) { + if (this.endTimeMs > sceneLengthMs) { throw new Error( - `Invalid recording length in ms (${parsedSettings.animationLengthMs}). Must be greater than 0.` + `Recording end time (${this.endTimeMs}) cannot be greater then scene length (${sceneLengthMs})` ); } - this.recordingLengthMs = parsedSettings.animationLengthMs; + if (parsedSettings.durationMs <= 0) { + throw new Error( + `Invalid recording length in ms (${parsedSettings.durationMs}). Must be greater than 0.` + ); + } + this.durationMs = parsedSettings.durationMs; + + if (!parsedSettings.filename) { + parsedSettings.filename = guid(); + } + this.filename = parsedSettings.filename; return parsedSettings; } @@ -85,14 +102,16 @@ export class VideoCapture { /** * Start recording. - * @param {typeof FrameEncoder} Encoder - * @param {import('types').FrameEncoderSettings} encoderSettings + * @param {typeof FrameEncoder} Encoder + * @param {import('types').FrameEncoderSettings} encoderSettings + * @param {number} sceneLengthMs + * @param {() => void} onStop */ - render(Encoder, encoderSettings, onStop = undefined) { + render(Encoder, encoderSettings, sceneLengthMs, onStop = undefined) { if (!this.isRecording()) { - encoderSettings = this.parseEncoderSettings(encoderSettings); + encoderSettings = this.parseEncoderSettings(encoderSettings, sceneLengthMs); - console.log(`Starting recording for ${this.recordingLengthMs}ms.`); + console.log(`Starting recording for ${this.durationMs}ms.`); this.onStop = onStop; this.encoder = new Encoder(encoderSettings); this.recording = true; @@ -135,7 +154,7 @@ export class VideoCapture { */ stop(callback = undefined) { if (this.isRecording()) { - console.log(`Stopping recording. Recorded for ${this.recordingLengthMs}ms.`); + console.log(`Stopping recording. Recorded for ${this.durationMs}ms.`); this.recording = false; this.save(); @@ -190,7 +209,7 @@ export class VideoCapture { _step() { // generating next frame timestamp this.timeMs = this._getNextTimeMs(); - if (this.timeMs > this.recordingLengthMs) { + if (this.timeMs > this.endTimeMs) { return {kind: 'error', error: 'STOP'}; } return {kind: 'step', nextTimeMs: this.timeMs}; diff --git a/modules/core/src/scene/deck-scene.js b/modules/core/src/scene/deck-scene.js index d3b9c4c7..bc645210 100644 --- a/modules/core/src/scene/deck-scene.js +++ b/modules/core/src/scene/deck-scene.js @@ -19,13 +19,15 @@ // THE SOFTWARE. export default class DeckScene { /** @param {import('types').DeckSceneParams} params */ - constructor({animationLoop, length, keyframes, data, renderLayers}) { + constructor({animationLoop, keyframes, data, renderLayers, lengthMs, width, height}) { this.animationLoop = animationLoop; this.keyframes = keyframes; this.data = data; this._renderLayers = renderLayers; - this.length = length; this.renderLayers = this.renderLayers.bind(this); + this.lengthMs = lengthMs; + this.width = width; + this.height = height; } renderLayers() { diff --git a/modules/core/src/scene/kepler-scene.js b/modules/core/src/scene/kepler-scene.js index 40b1e4ee..8260d28b 100644 --- a/modules/core/src/scene/kepler-scene.js +++ b/modules/core/src/scene/kepler-scene.js @@ -19,14 +19,16 @@ // THE SOFTWARE. export default class KeplerScene { /** @param {import('types').KeplerSceneParams} params */ - constructor({animationLoop, length, keyframes, data, filters, getFrame}) { + constructor({animationLoop, keyframes, data, filters, getFrame, lengthMs, width, height}) { this.animationLoop = animationLoop; this.keyframes = keyframes; this.data = data; this._getFrame = getFrame; this.filters = filters; - this.length = length; this.getFrame = this.getFrame.bind(this); + this.lengthMs = lengthMs; + this.width = width; + this.height = height; } /** diff --git a/modules/core/src/types.d.ts b/modules/core/src/types.d.ts index 495c3920..7ee65490 100644 --- a/modules/core/src/types.d.ts +++ b/modules/core/src/types.d.ts @@ -24,8 +24,8 @@ type DeckGl = { type FrameEncoderSettings = Partial interface EncoderSettings { - animationLengthMs: number - startOffsetMs: number + startOffsetMs?: number + durationMs?: number filename: string framerate: number jpeg: { @@ -46,7 +46,9 @@ interface EncoderSettings { interface DeckSceneParams { animationLoop: any - length: number + lengthMs: number + width: number + height: number keyframes: any data: any renderLayers: (scene: DeckScene) => any[] @@ -54,7 +56,9 @@ interface DeckSceneParams { interface KeplerSceneParams { animationLoop: any - length: number + lengthMs: number + width: number + height: number keyframes: any[] data: any filters: any[] From b7e0638487cf733dbfabfc7a18a52bfc2a4321c9 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Sun, 7 Jun 2020 19:31:01 -0700 Subject: [PATCH 3/5] Additional scene deifinition changes --- examples/minimal/app.js | 3 +-- examples/minimal/scene.js | 11 +++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/examples/minimal/app.js b/examples/minimal/app.js index 3c06c199..ecef37b1 100644 --- a/examples/minimal/app.js +++ b/examples/minimal/app.js @@ -14,9 +14,8 @@ const INITIAL_VIEW_STATE = { const adapter = new DeckAdapter(sceneBuilder); +/** @type {import('@hubble.gl/core/src/types').EncoderSettings} */ const encoderSettings = { - animationLengthMs: 15000, - startOffsetMs: 0, framerate: 30, webm: { quality: 0.8 diff --git a/examples/minimal/scene.js b/examples/minimal/scene.js index dbbf5273..37723ad9 100644 --- a/examples/minimal/scene.js +++ b/examples/minimal/scene.js @@ -49,10 +49,17 @@ function getKeyframes(animationLoop, data) { } export async function sceneBuilder(animationLoop) { - const length = 5000; const data = await fetch( 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/bart-lines.json' ).then(x => x.json()); const keyframes = getKeyframes(animationLoop, data); - return new DeckScene({animationLoop, length, keyframes, data, renderLayers}); + return new DeckScene({ + animationLoop, + keyframes, + data, + renderLayers, + lengthMs: 5000, + width: 720, + height: 480 + }); } From aaa453a42643e208c3ffd7c2eebeedc88379d383 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Sun, 7 Jun 2020 19:34:39 -0700 Subject: [PATCH 4/5] Add GIF encoder settings --- examples/minimal/app.js | 3 +++ examples/terrain/app.js | 3 +++ modules/core/src/capture/video-capture.js | 3 ++- .../core/src/encoders/video/gif-encoder.js | 21 +++++++++++++------ modules/core/src/types.d.ts | 14 ++++++------- 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/examples/minimal/app.js b/examples/minimal/app.js index ecef37b1..9fc159f9 100644 --- a/examples/minimal/app.js +++ b/examples/minimal/app.js @@ -22,6 +22,9 @@ const encoderSettings = { }, jpeg: { quality: 0.8 + }, + gif: { + sampleInterval: 1000 } }; diff --git a/examples/terrain/app.js b/examples/terrain/app.js index 711935fc..d6c270a9 100644 --- a/examples/terrain/app.js +++ b/examples/terrain/app.js @@ -23,6 +23,9 @@ const encoderSettings = { }, jpeg: { quality: 0.8 + }, + gif: { + sampleInterval: 1000 } }; diff --git a/modules/core/src/capture/video-capture.js b/modules/core/src/capture/video-capture.js index f6c197cf..219efdbe 100644 --- a/modules/core/src/capture/video-capture.js +++ b/modules/core/src/capture/video-capture.js @@ -109,6 +109,7 @@ export class VideoCapture { */ render(Encoder, encoderSettings, sceneLengthMs, onStop = undefined) { if (!this.isRecording()) { + console.time('render'); encoderSettings = this.parseEncoderSettings(encoderSettings, sceneLengthMs); console.log(`Starting recording for ${this.durationMs}ms.`); @@ -169,7 +170,7 @@ export class VideoCapture { * @param {{ (blob: Blob): boolean }} [callback] */ save(callback) { - console.timeEnd('capture'); + console.timeEnd('render'); if (!callback) { /** * @param {Blob} blob diff --git a/modules/core/src/encoders/video/gif-encoder.js b/modules/core/src/encoders/video/gif-encoder.js index 437a3724..a9ed31ed 100644 --- a/modules/core/src/encoders/video/gif-encoder.js +++ b/modules/core/src/encoders/video/gif-encoder.js @@ -8,6 +8,16 @@ export default class GifEncoder extends FrameEncoder { this.mimeType = 'image/gif'; this.extension = '.gif'; this.gifBuilder = null; + this.options = {}; + + if (settings.gif) { + this.options = settings.gif; + } + + this.options.width = this.options.width || 720; + this.options.height = this.options.height || 480; + this.options.numWorkers = this.options.numWorkers || 4; + this.options.sampleInterval = this.options.sampleInterval || 10; // this.source = settings.source this.source = 'images'; @@ -15,11 +25,14 @@ export default class GifEncoder extends FrameEncoder { this.start = this.start.bind(this); this.add = this.add.bind(this); this.save = this.save.bind(this); - this.dispose = this.dispose.bind(this); } start() { - this.gifBuilder = new GIFBuilder({source: this.source, width: 720, height: 480, numWorkers: 4}); + this.gifBuilder = new GIFBuilder({ + source: this.source, + ...this.options, + interval: 1 / this.framerate + }); } /** @param {HTMLCanvasElement} canvas */ @@ -33,8 +46,4 @@ export default class GifEncoder extends FrameEncoder { async save() { return this.gifBuilder.build(); } - - dispose() { - this.gifBuilder = null; - } } diff --git a/modules/core/src/types.d.ts b/modules/core/src/types.d.ts index 7ee65490..361798dc 100644 --- a/modules/core/src/types.d.ts +++ b/modules/core/src/types.d.ts @@ -34,14 +34,12 @@ interface EncoderSettings { webm: { quality: number } - // gif: { - // numWorkers: number, - // sampleInterval: number, - // resize: { - // width: number, - // height: number - // } - // }, + gif: { + numWorkers: number, + sampleInterval: number, + width: number, + height: number + }, } interface DeckSceneParams { From 2c795f7d501bc967102cfbc7fd67d5dbb2b28246 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Sun, 7 Jun 2020 19:36:00 -0700 Subject: [PATCH 5/5] Add GifEncoder to EncoderDropdown --- modules/react/src/components/basic-controls.js | 7 +++++-- modules/react/src/components/encoder-dropdown.js | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/react/src/components/basic-controls.js b/modules/react/src/components/basic-controls.js index a8f8e481..7a8d079f 100644 --- a/modules/react/src/components/basic-controls.js +++ b/modules/react/src/components/basic-controls.js @@ -23,12 +23,13 @@ import { WebMEncoder, JPEGSequenceEncoder, PNGSequenceEncoder, - PreviewEncoder + PreviewEncoder, + GifEncoder } from '@hubble.gl/core'; import EncoderDropdown from './encoder-dropdown'; export default function BasicControls({adapter, busy, setBusy, encoderSettings}) { - const [encoder, setEncoder] = useState('webm'); + const [encoder, setEncoder] = useState('gif'); const onRender = () => { if (encoder === 'preview') { @@ -39,6 +40,8 @@ export default function BasicControls({adapter, busy, setBusy, encoderSettings}) adapter.render(JPEGSequenceEncoder, encoderSettings, () => setBusy(false)); } else if (encoder === 'png') { adapter.render(PNGSequenceEncoder, encoderSettings, () => setBusy(false)); + } else if (encoder === 'gif') { + adapter.render(GifEncoder, encoderSettings, () => setBusy(false)); } setBusy(true); diff --git a/modules/react/src/components/encoder-dropdown.js b/modules/react/src/components/encoder-dropdown.js index b02f7df3..fa32f8dc 100644 --- a/modules/react/src/components/encoder-dropdown.js +++ b/modules/react/src/components/encoder-dropdown.js @@ -28,6 +28,7 @@ export default function EncoderDropdown({disabled, encoder, setEncoder}) { + ); }