diff --git a/CHANGES.md b/CHANGES.md
index 4df8615f..63862cf7 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,15 @@
# videojs-record changelog
+## x.x.x - unreleased
+
+- New gifshot plugin: improved animated GIF support (#291)
+
+**Backwards-incompatible changes** (when upgrading from a previous version):
+
+- Support for animated GIF recording using RecordRTC was removed and replaced
+ with a new plugin using the Gifshot library (#291)
+
+
## 4.2.0 - unreleased
- New ffmpeg.wasm converter plugin: convert recorded data into other
diff --git a/build-config/fragments/common.js b/build-config/fragments/common.js
index d333b501..8d44091d 100644
--- a/build-config/fragments/common.js
+++ b/build-config/fragments/common.js
@@ -72,6 +72,13 @@ module.exports = {
commonjs2: 'recordrtc',
amd: 'recordrtc',
root: 'RecordRTC' // indicates global variable
+ },
+ // plugins
+ 'gifshot': {
+ commonjs: 'gifshot',
+ commonjs2: 'gifshot',
+ amd: 'gifshot',
+ root: 'gifshot' // indicates global variable
}
},
module: {
diff --git a/examples/animated-gif.html b/examples/animated-gif.html
index 51314621..9225f018 100644
--- a/examples/animated-gif.html
+++ b/examples/animated-gif.html
@@ -9,29 +9,30 @@
-
-
+
+
-
+
diff --git a/karma.conf.js b/karma.conf.js
index 3fa214bb..5be796a5 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -38,6 +38,7 @@ const chromeFlags = [
'--enable-experimental-web-platform-features',
'--js-flags=--max-old-space-size=8196'
];
+
//-------------------------------------------
// Firefox CLI options
//-------------------------------------------
@@ -138,8 +139,8 @@ module.exports = function(config) {
// ffmpeg.wasm
{pattern: 'node_modules/@ffmpeg/ffmpeg/dist/ffmpeg.min.js', included: false, served: true},
{pattern: 'node_modules/@ffmpeg/core/dist/ffmpeg-core.js', included: false, served: true},
- // gif-recorder: only available on CDN
- 'http://cdn.webrtc-experiment.com/gif-recorder.js',
+ // gifshot
+ 'node_modules/gifshot/dist/gifshot.min.js',
// -------------------------------------------
// specs
@@ -163,6 +164,7 @@ module.exports = function(config) {
// do not include tests or libraries
'src/js/**/*.js': ['coverage']
},
+ webpack: webpackConfig,
webpackMiddleware: {
stats: 'errors-only'
},
@@ -245,7 +247,6 @@ module.exports = function(config) {
{ type: 'lcov', subdir: 'lcov' }
]
},
- webpack: webpackConfig,
customLaunchers: {
Chrome_dev: {
base: 'Chrome',
diff --git a/package-lock.json b/package-lock.json
index 4f5b51ce..8c5e9f11 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7006,6 +7006,12 @@
"assert-plus": "^1.0.0"
}
},
+ "gifshot": {
+ "version": "0.4.5",
+ "resolved": "https://registry.npmjs.org/gifshot/-/gifshot-0.4.5.tgz",
+ "integrity": "sha1-48tXAgOjsTn/MGnXV4CYopwDsPg=",
+ "dev": true
+ },
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
diff --git a/package.json b/package.json
index 2766498f..6d8e9158 100644
--- a/package.json
+++ b/package.json
@@ -104,6 +104,7 @@
"file-loader": "^6.2.0",
"fine-uploader": ">=5.16",
"formidable": "^1.2.2",
+ "gifshot": ">=0.4.5",
"fs-extra": "^9.1.0",
"host-environment": "^2.1.2",
"htmlhint": "^0.14.2",
diff --git a/src/js/controls/animation-display.js b/src/js/controls/animation-display.js
index 92dfd8c2..d9e89917 100644
--- a/src/js/controls/animation-display.js
+++ b/src/js/controls/animation-display.js
@@ -8,7 +8,7 @@ import videojs from 'video.js';
const Component = videojs.getComponent('Component');
/**
- * Image for displaying animated GIF image.
+ * Component for displaying animated GIF images.
*
* @class
* @augments videojs.Component
@@ -27,6 +27,17 @@ class AnimationDisplay extends Component {
innerHTML: '
'
});
}
+
+ /**
+ * Set `src` of image element.
+ *
+ * @param {object} data - TODO
+ */
+ load(data) {
+ let img = this.el().firstChild;
+
+ img.src = data;
+ }
}
Component.registerComponent('AnimationDisplay', AnimationDisplay);
diff --git a/src/js/controls/record-canvas.js b/src/js/controls/record-canvas.js
index 0e33345d..ecf2aecd 100644
--- a/src/js/controls/record-canvas.js
+++ b/src/js/controls/record-canvas.js
@@ -8,7 +8,7 @@ import videojs from 'video.js';
const Component = videojs.getComponent('Component');
/**
- * Canvas for displaying snapshot image.
+ * Component for displaying images on a `canvas` element.
*
* @class
* @augments videojs.Component
@@ -27,6 +27,49 @@ class RecordCanvas extends Component {
innerHTML: ''
});
}
+
+ /**
+ * Clear the `RecordCanvas`s `canvas` element.
+ */
+ clear() {
+ let canvas = this.el().firstChild;
+
+ canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
+ }
+
+ /**
+ * Draw `ImageData` frame onto `canvas` element.
+ *
+ * @param {ImageData} [imgData] - ImageData to draw onto canvas.
+ */
+ drawFrame(imgData) {
+ let canvas = this.el().firstChild;
+
+ // set the image size to the dimensions of the recorded animation
+ canvas.width = imgData.width;
+ canvas.height = imgData.height;
+
+ canvas.getContext('2d').putImageData(
+ imgData, 0, 0, 0, 0,
+ imgData.width,
+ imgData.height
+ );
+ }
+
+ /**
+ * Draw image onto `canvas` element.
+ *
+ * @param {HTMLElement} [element] - HTML element to draw onto canvas.
+ */
+ drawImage(element) {
+ let canvas = this.el().firstChild;
+
+ canvas.getContext('2d').drawImage(
+ element, 0, 0,
+ canvas.width,
+ canvas.height
+ );
+ }
}
Component.registerComponent('RecordCanvas', RecordCanvas);
diff --git a/src/js/defaults.js b/src/js/defaults.js
index 3ba6a064..71be9e66 100644
--- a/src/js/defaults.js
+++ b/src/js/defaults.js
@@ -11,7 +11,7 @@ const pluginDefaultOptions = {
audio: false,
// Include video in the recorded clip.
video: false,
- // Animated GIF.
+ // Animated GIF using the gifshot library.
animation: false,
// Screen capture.
screen: false,
@@ -100,6 +100,8 @@ const pluginDefaultOptions = {
// Enables the audioBufferUpdate event that provides realtime AudioBuffer
// instances from the input audio device.
audioBufferUpdate: false,
+ // Options for animated GIFs using the gifshot library.
+ animationOptions: {},
// Frame rate in frames per second.
animationFrameRate: 200,
// Sets quality of color quantization (conversion of images to the
diff --git a/src/js/engine/engine-loader.js b/src/js/engine/engine-loader.js
index e8458d88..e2c34582 100644
--- a/src/js/engine/engine-loader.js
+++ b/src/js/engine/engine-loader.js
@@ -7,7 +7,7 @@ import videojs from 'video.js';
import RecordRTCEngine from './record-rtc';
import {CONVERT_PLUGINS, TSEBML, FFMPEGJS, FFMPEGWASM} from './convert-engine';
-import {RECORDRTC, LIBVORBISJS, RECORDERJS, LAMEJS, OPUSRECORDER, OPUSMEDIARECORDER, VMSG, WEBMWASM, AUDIO_PLUGINS} from './record-engine';
+import {RECORDRTC, LIBVORBISJS, RECORDERJS, LAMEJS, OPUSRECORDER, OPUSMEDIARECORDER, VMSG, WEBMWASM, GIFSHOT, AUDIO_PLUGINS} from './record-engine';
/**
* Get audio plugin engine class.
@@ -89,6 +89,17 @@ const getVideoEngine = function(videoEngine) {
return VideoEngineClass;
};
+/**
+ * Get animation plugin engine class.
+ *
+ * @private
+ * @returns {Object} Animation engine plugin.
+ */
+const getAnimationEngine = function() {
+ // only gifshot supported at the moment
+ return videojs.GifshotEngine;
+};
+
/**
* Check whether any audio record plugins are enabled.
*
@@ -137,5 +148,5 @@ const getConvertEngine = function(convertEngine) {
};
export {
- getAudioEngine, isAudioPluginActive, getVideoEngine, getConvertEngine
+ getAudioEngine, isAudioPluginActive, getVideoEngine, getConvertEngine, getAnimationEngine
};
diff --git a/src/js/engine/record-engine.js b/src/js/engine/record-engine.js
index e002cbd4..22b3be9a 100644
--- a/src/js/engine/record-engine.js
+++ b/src/js/engine/record-engine.js
@@ -21,7 +21,8 @@ const LAMEJS = 'lamejs';
const OPUSRECORDER = 'opus-recorder';
const OPUSMEDIARECORDER = 'opus-media-recorder';
const VMSG = 'vmsg';
-
+// animation
+const GIFSHOT = 'gifshot';
// video
const WEBMWASM = 'webm-wasm';
@@ -33,9 +34,11 @@ const AUDIO_PLUGINS = [
// all video plugins
const VIDEO_PLUGINS = [WEBMWASM];
-// all record plugins
-const RECORD_PLUGINS = AUDIO_PLUGINS.concat(VIDEO_PLUGINS);
+// all animation plugins
+const ANIMATION_PLUGINS = [GIFSHOT];
+// all record plugins
+const RECORD_PLUGINS = AUDIO_PLUGINS.concat(VIDEO_PLUGINS).concat(ANIMATION_PLUGINS);
/**
* Base class for recorder backends.
@@ -130,6 +133,6 @@ Component.registerComponent('RecordEngine', RecordEngine);
export {
RecordEngine, RECORD_PLUGINS, AUDIO_PLUGINS, VIDEO_PLUGINS,
- RECORDRTC, LIBVORBISJS, RECORDERJS, LAMEJS, OPUSRECORDER,
- OPUSMEDIARECORDER, VMSG, WEBMWASM
+ ANIMATION_PLUGINS, RECORDRTC, LIBVORBISJS, RECORDERJS, LAMEJS,
+ OPUSRECORDER, OPUSMEDIARECORDER, VMSG, WEBMWASM, GIFSHOT
};
diff --git a/src/js/plugins/gifshot-plugin.js b/src/js/plugins/gifshot-plugin.js
new file mode 100644
index 00000000..c73be8f0
--- /dev/null
+++ b/src/js/plugins/gifshot-plugin.js
@@ -0,0 +1,98 @@
+/**
+ * @file gifshot-plugin.js
+ * @since x.x.x
+ */
+
+import gifshot from 'gifshot';
+
+const RecordEngine = videojs.getComponent('RecordEngine');
+
+/**
+ * Animated GIF engine for the Gifshot library.
+ *
+ * @class
+ * @augments videojs.RecordEngine
+ */
+class GifshotEngine extends RecordEngine {
+ /**
+ * Setup recording engine.
+ *
+ * @param {LocalMediaStream} stream - Media stream to record.
+ * @param {Object} mediaType - Object describing the media type of this
+ * engine.
+ * @param {Boolean} debug - Indicating whether or not debug messages should
+ * be printed in the console.
+ */
+ setup(stream, mediaType, debug) {
+ this.inputStream = stream;
+ this.mediaType = mediaType;
+ this.debug = debug;
+
+ this.defaultOptions = {
+ // Desired width of the image
+ gifWidth: this.options_.width,
+ // Desired height of the image
+ gifHeight: this.options_.height,
+
+ // Whether or not you would like the user's camera to stay on
+ // after the GIF is created.
+ // Note: The cameraStream Media object is passed back to you in
+ // the createGIF() callback function
+ keepCameraOn: true,
+ // Expects a cameraStream Media object
+ // Note: Passing an existing camera stream will allow you to
+ // create another GIF and/or snapshot without asking for the user's
+ // permission to access the camera again if you are not using SSL
+ cameraStream: this.inputStream,
+ // Whether or not you would like to save all of the canvas image
+ // binary data from your created GIF
+ // Note: This is particularly useful for when you want to re-use a
+ // GIF to add text to later
+ saveRenderingContexts: true
+ };
+ }
+
+ /**
+ * Start recording.
+ */
+ start() {
+ //console.log('started recording');
+
+ let opts = videojs.mergeOptions(this.defaultOptions,
+ this.animationOptions);
+
+ gifshot.createGIF(opts, this.onRecordingAvailable.bind(this));
+ }
+
+ /**
+ * Stop recording.
+ */
+ stop() {
+ //console.log('stopped recording');
+ }
+
+ /**
+ * @private
+ * @param {object} obj - TODO
+ */
+ onRecordingAvailable(obj) {
+ if (!obj.error) {
+ // save image data
+ this.recordedData = obj.image;
+
+ // save first frame
+ this.recordedFrames = obj.savedRenderingContexts;
+
+ // remove reference to recorded stream
+ this.dispose();
+
+ // notify listeners
+ this.trigger('recordComplete');
+ }
+ }
+}
+
+// expose plugin
+videojs.GifshotEngine = GifshotEngine;
+
+export default GifshotEngine;
diff --git a/src/js/videojs.record.js b/src/js/videojs.record.js
index 2ef90851..058d8d8b 100644
--- a/src/js/videojs.record.js
+++ b/src/js/videojs.record.js
@@ -23,7 +23,7 @@ import setSrcObject from './utils/browser-shim';
import compareVersion from './utils/compare-version';
import {detectBrowser} from './utils/detect-browser';
-import {getAudioEngine, isAudioPluginActive, getVideoEngine, getConvertEngine} from './engine/engine-loader';
+import {getAudioEngine, isAudioPluginActive, getVideoEngine, getConvertEngine, getAnimationEngine} from './engine/engine-loader';
import {IMAGE_ONLY, AUDIO_ONLY, VIDEO_ONLY, AUDIO_VIDEO, AUDIO_SCREEN, ANIMATION, SCREEN_ONLY, getRecorderMode} from './engine/record-mode';
const Plugin = videojs.getPlugin('plugin');
@@ -222,8 +222,7 @@ class Record extends Plugin {
this.imageOutputQuality = recordOptions.imageOutputQuality;
// animation settings
- this.animationFrameRate = recordOptions.animationFrameRate;
- this.animationQuality = recordOptions.animationQuality;
+ this.animationOptions = recordOptions.animationOptions;
}
/**
@@ -629,7 +628,9 @@ class Record extends Plugin {
// setup recording engine
if (this.getRecordType() !== IMAGE_ONLY) {
// currently record plugins are only supported in audio-only mode
- if (this.getRecordType() !== AUDIO_ONLY && isAudioPluginActive(this.audioEngine)) {
+ if (this.getRecordType() !== AUDIO_ONLY &&
+ isAudioPluginActive(this.audioEngine)
+ ) {
throw new Error('Currently ' + this.audioEngine +
' is only supported in audio-only mode.');
}
@@ -643,6 +644,12 @@ class Record extends Plugin {
engineType = this.audioEngine;
break;
+ case ANIMATION:
+ // get animation plugin engine class
+ EngineClass = getAnimationEngine();
+ engineType = ANIMATION;
+ break;
+
default:
// get video plugin engine class (or default recordrtc engine)
EngineClass = getVideoEngine(this.videoEngine);
@@ -693,8 +700,7 @@ class Record extends Plugin {
};
// animated GIF settings
- this.engine.quality = this.animationQuality;
- this.engine.frameRate = this.animationFrameRate;
+ this.engine.animationOptions = this.animationOptions;
// timeSlice
if (this.recordTimeSlice && this.recordTimeSlice > 0) {
@@ -859,7 +865,8 @@ class Record extends Plugin {
break;
case ANIMATION:
- // hide the first frame
+ // clear and hide the first frame
+ this.player.recordCanvas.clear();
this.player.recordCanvas.hide();
// hide the animation
@@ -868,13 +875,8 @@ class Record extends Plugin {
// show preview video
this.mediaElement.style.display = 'block';
- // for animations, capture the first frame
- // that can be displayed as soon as recording
- // is complete
- this.captureFrame().then((result) => {
- // start video preview **after** capturing first frame
- this.startVideoPreview();
- });
+ // preview video stream in video element
+ this.startVideoPreview();
break;
}
@@ -1136,6 +1138,7 @@ class Record extends Plugin {
this.mediaElement.style.display = 'none';
// show the first frame
+ this.player.recordCanvas.drawFrame(this.engine.recordedFrames[0]);
this.player.recordCanvas.show();
// pause player so user can start playback
@@ -1440,6 +1443,7 @@ class Record extends Plugin {
case ANIMATION:
// reset UI
this.player.recordCanvas.hide();
+ this.player.animationDisplay.hide();
this.player.cameraButton.hide();
break;
}
@@ -1628,7 +1632,7 @@ class Record extends Plugin {
// take picture
imageCapture.grabFrame().then((imageBitmap) => {
// get a frame and copy it onto the canvas
- this.drawCanvas(recordCanvas, imageBitmap);
+ this.player.recordCanvas.drawImage(imageBitmap);
// notify others
resolve(recordCanvas);
@@ -1640,27 +1644,13 @@ class Record extends Plugin {
// no ImageCapture available: do it the oldskool way
// get a frame and copy it onto the canvas
- this.drawCanvas(recordCanvas, this.mediaElement);
+ this.player.recordCanvas.drawImage(this.mediaElement);
// notify others
resolve(recordCanvas);
});
}
- /**
- * Draw image frame on canvas element.
- * @private
- * @param {HTMLCanvasElement} canvas - Canvas to draw on.
- * @param {HTMLElement} element - Element to draw onto the canvas.
- */
- drawCanvas(canvas, element) {
- canvas.getContext('2d').drawImage(
- element, 0, 0,
- canvas.width,
- canvas.height
- );
- }
-
/**
* Start preview of video stream.
* @private
@@ -1701,7 +1691,7 @@ class Record extends Plugin {
this.player.recordCanvas.hide();
// show the animation
- setSrcObject(this.player.recordedData, animationDisplay);
+ this.player.animationDisplay.load(this.player.recordedData);
this.player.animationDisplay.show();
}
diff --git a/test/defaults.spec.js b/test/defaults.spec.js
index 306f7d4d..7fd5e71e 100644
--- a/test/defaults.spec.js
+++ b/test/defaults.spec.js
@@ -46,6 +46,7 @@ describe('pluginDefaultOptions', () => {
audioWorkerURL: '',
audioWebAssemblyURL: '',
audioBufferUpdate: false,
+ animationOptions: {},
animationFrameRate: 200,
animationQuality: 10,
imageOutputType: 'dataURL',
diff --git a/test/engine/record-engine.spec.js b/test/engine/record-engine.spec.js
index ff9e3b04..5eb9cbd0 100644
--- a/test/engine/record-engine.spec.js
+++ b/test/engine/record-engine.spec.js
@@ -3,7 +3,7 @@
*/
import TestHelpers from '../test-helpers';
import Event from '../../src/js/event';
-import {RECORDRTC, LIBVORBISJS, RECORDERJS, LAMEJS, OPUSRECORDER, OPUSMEDIARECORDER, VMSG, WEBMWASM, RECORD_PLUGINS, AUDIO_PLUGINS, VIDEO_PLUGINS, RecordEngine} from '../../src/js/engine/record-engine';
+import {RECORDRTC, LIBVORBISJS, RECORDERJS, LAMEJS, OPUSRECORDER, OPUSMEDIARECORDER, VMSG, WEBMWASM, GIFSHOT, RECORD_PLUGINS, AUDIO_PLUGINS, VIDEO_PLUGINS, ANIMATION_PLUGINS, RecordEngine} from '../../src/js/engine/record-engine';
/** @test {record-engine} */
describe('engine.record-engine', () => {
@@ -40,6 +40,10 @@ describe('engine.record-engine', () => {
expect(VMSG).toEqual('vmsg');
expect(AUDIO_PLUGINS.length).toEqual(6);
+ // animation
+ expect(GIFSHOT).toEqual('gifshot');
+ expect(ANIMATION_PLUGINS.length).toEqual(1);
+
// video
expect(WEBMWASM).toEqual('webm-wasm');
expect(VIDEO_PLUGINS.length).toEqual(1);