-```
-## Otherwise, you can link specific files:
+
-* https://github.com/streamproc/MediaStreamRecorder/blob/master/How-to-Link-Specific-Files.md
+https://cdn.rawgit.com/streamproc/MediaStreamRecorder/master/MediaStreamRecorder.js
+```
## Record audio+video in Firefox in single WebM
@@ -175,6 +177,28 @@ function onMediaError(e) {
mediaRecorder.stop();
```
+## How to pause recordings?
+
+```javascript
+mediaRecorder.pause();
+```
+
+## How to resume recordings?
+
+```javascript
+mediaRecorder.resume();
+```
+
+## How to save recordings?
+
+```javascript
+// invoke save-as dialog for all recorded blobs
+mediaRecorder.save();
+
+// or pass external blob/file
+mediaRecorder.save(YourExternalBlob, 'FileName.webm');
+```
+
## How to upload recorded files using PHP?
**PHP code:**
@@ -225,7 +249,31 @@ function xhr(url, data, callback) {
# API Documentation
-## audioChannels
+## `recorderType`
+
+You can force StereoAudioRecorder or WhammyRecorder or similar records on Firefox or Edge; even on Chrome and Opera.
+
+All browsers will be using your specified recorder:
+
+```javascript
+// force WebAudio API on all browsers
+// it allows you record remote audio-streams in Firefox
+// it also works in Microsoft Edge
+mediaRecorder.type = StereoAudioRecorder;
+
+// force webp based webm encoder on all browsers
+mediaRecorder.type = WhammyRecorder;
+
+// force MediaRecorder API on all browsers
+// Chrome is going to implement MediaRecorder API soon;
+// so this property allows you force MediaRecorder in Chrome.
+mediaRecorder.type = MediaRecorderWrapper;
+
+// force GifRecorder in all browsers. Both WhammyRecorder and MediaRecorder API will be ignored.
+mediaRecorder.type = GifRecorder;
+```
+
+## `audioChannels`
It is an integer value that accepts either 1 or 2. "1" means record only left-channel and skip right-one. The default value is "2".
@@ -233,7 +281,7 @@ It is an integer value that accepts either 1 or 2. "1" means record only left-ch
mediaRecorder.audioChannels = 1;
```
-## bufferSize
+## `bufferSize`
You can set following audio-bufferSize values: 0, 256, 512, 1024, 2048, 4096, 8192, and 16384. "0" means: let chrome decide the device's default bufferSize. Default value is "2048".
@@ -241,7 +289,7 @@ You can set following audio-bufferSize values: 0, 256, 512, 1024, 2048, 4096, 81
mediaRecorder.bufferSize = 0;
```
-## sampleRate
+## `sampleRate`
Default "sampleRate" value is "44100". Currently you can't modify sample-rate in windows that's why this property isn't yet exposed to public API.
@@ -252,7 +300,7 @@ It accepts values only in range: 22050 to 96000
mediaRecorder.sampleRate = 96000;
```
-## video
+## `video`
It is recommended to pass your HTMLVideoElement to get most accurate result.
@@ -266,6 +314,42 @@ videoRecorder.onStartedDrawingNonBlankFrames = function() {
};
```
+## `stop`
+
+This method allows you stop recording.
+
+```javascript
+mediaRecorder.stop();
+```
+
+## `pause`
+
+This method allows you pause recording.
+
+```javascript
+mediaRecorder.pause();
+```
+
+## `resume`
+
+This method allows you resume recording.
+
+```javascript
+mediaRecorder.resume();
+```
+
+## `save`
+
+This method allows you save recording to disk (via save-as dialog).
+
+```javascript
+// invoke save-as dialog for all recorded blobs
+mediaRecorder.save();
+
+// or pass external blob/file
+mediaRecorder.save(YourExternalBlob, 'FileName.webm');
+```
+
## canvas
Using this property, you can pass video resolutions:
@@ -359,6 +443,7 @@ gifRecorder.mimeType = 'image/gif';
| Google Chrome | [Stable](https://www.google.com/intl/en_uk/chrome/browser/) / [Canary](https://www.google.com/intl/en/chrome/browser/canary.html) / [Beta](https://www.google.com/intl/en/chrome/browser/beta.html) / [Dev](https://www.google.com/intl/en/chrome/browser/index.html?extra=devchannel#eula) |
| Opera | [Stable](http://www.opera.com/) / [NEXT](http://www.opera.com/computer/next) |
| Android | [Chrome](https://play.google.com/store/apps/details?id=com.chrome.beta&hl=en) / [Firefox](https://play.google.com/store/apps/details?id=org.mozilla.firefox) / [Opera](https://play.google.com/store/apps/details?id=com.opera.browser) |
+| Microsoft Edge | [Normal Build](https://www.microsoft.com/en-us/windows/microsoft-edge) |
## Contributors
diff --git a/VideoStreamRecorder/GifRecorder.js b/VideoStreamRecorder/GifRecorder.js
index 16ca130..beaa9f6 100644
--- a/VideoStreamRecorder/GifRecorder.js
+++ b/VideoStreamRecorder/GifRecorder.js
@@ -1,11 +1,11 @@
-// Muaz Khan - https://github.com/muaz-khan
-// neizerth - https://github.com/neizerth
-// MIT License - https://www.webrtc-experiment.com/licence/
-// Documentation - https://github.com/streamproc/MediaStreamRecorder
-// ==========================================================
+// --------------
// GifRecorder.js
function GifRecorder(mediaStream) {
+ if (typeof GIFEncoder === 'undefined') {
+ throw 'Please link: https://cdn.webrtc-experiment.com/gif-recorder.js';
+ }
+
// void start(optional long timeSlice)
// timestamp to fire "ondataavailable"
this.start = function(timeSlice) {
@@ -47,6 +47,11 @@ function GifRecorder(mediaStream) {
startTime = Date.now();
function drawVideoFrame(time) {
+ if (isPaused) {
+ setTimeout(drawVideoFrame, 500, time);
+ return;
+ }
+
lastAnimationFrame = requestAnimationFrame(drawVideoFrame);
if (typeof lastFrameTime === undefined) {
@@ -54,7 +59,13 @@ function GifRecorder(mediaStream) {
}
// ~10 fps
- if (time - lastFrameTime < 90) return;
+ if (time - lastFrameTime < 90) {
+ return;
+ }
+
+ if (video.paused) {
+ video.play(); // Android
+ }
context.drawImage(video, 0, 0, imageWidth, imageHeight);
@@ -81,7 +92,7 @@ function GifRecorder(mediaStream) {
// todo: find a way to clear old recorded blobs
gifEncoder.stream().bin = [];
- };
+ }
this.stop = function() {
if (lastAnimationFrame) {
@@ -91,6 +102,16 @@ function GifRecorder(mediaStream) {
}
};
+ var isPaused = false;
+
+ this.pause = function() {
+ isPaused = true;
+ };
+
+ this.resume = function() {
+ isPaused = false;
+ };
+
this.ondataavailable = function() {};
this.onstop = function() {};
diff --git a/VideoStreamRecorder/WhammyRecorder.js b/VideoStreamRecorder/WhammyRecorder.js
index 0cdd5ec..5f01989 100644
--- a/VideoStreamRecorder/WhammyRecorder.js
+++ b/VideoStreamRecorder/WhammyRecorder.js
@@ -1,4 +1,4 @@
-// =================
+// ===================
// WhammyRecorder.js
function WhammyRecorder(mediaStream) {
@@ -35,6 +35,22 @@ function WhammyRecorder(mediaStream) {
}
};
+ this.pause = function() {
+ if (!mediaRecorder) {
+ return;
+ }
+
+ mediaRecorder.pause();
+ };
+
+ this.resume = function() {
+ if (!mediaRecorder) {
+ return;
+ }
+
+ mediaRecorder.resume();
+ };
+
this.ondataavailable = function() {};
// Reference to "WhammyRecorder" object
diff --git a/VideoStreamRecorder/WhammyRecorderHelper.js b/VideoStreamRecorder/WhammyRecorderHelper.js
index f269aef..9882044 100644
--- a/VideoStreamRecorder/WhammyRecorderHelper.js
+++ b/VideoStreamRecorder/WhammyRecorderHelper.js
@@ -1,14 +1,22 @@
-// =======================
+// ==========================
// WhammyRecorderHelper.js
function WhammyRecorderHelper(mediaStream, root) {
this.record = function(timeSlice) {
- if (!this.width) this.width = 320;
- if (!this.height) this.height = 240;
+ if (!this.width) {
+ this.width = 320;
+ }
+ if (!this.height) {
+ this.height = 240;
+ }
if (this.video && this.video instanceof HTMLVideoElement) {
- if (!this.width) this.width = video.videoWidth || video.clientWidth || 320;
- if (!this.height) this.height = video.videoHeight || video.clientHeight || 240;
+ if (!this.width) {
+ this.width = video.videoWidth || video.clientWidth || 320;
+ }
+ if (!this.height) {
+ this.height = video.videoHeight || video.clientHeight || 240;
+ }
}
if (!this.video) {
@@ -30,6 +38,7 @@ function WhammyRecorderHelper(mediaStream, root) {
// setting defaults
if (this.video && this.video instanceof HTMLVideoElement) {
+ this.isHTMLObject = true;
video = this.video.cloneNode();
} else {
video = document.createElement('video');
@@ -52,55 +61,77 @@ function WhammyRecorderHelper(mediaStream, root) {
};
this.clearOldRecordedFrames = function() {
- frames = [];
+ whammy.frames = [];
};
var requestDataInvoked = false;
this.requestData = function() {
- if (!frames.length) {
+ if (isPaused) {
+ return;
+ }
+
+ if (!whammy.frames.length) {
requestDataInvoked = false;
return;
}
requestDataInvoked = true;
// clone stuff
- var internal_frames = frames.slice(0);
+ var internalFrames = whammy.frames.slice(0);
// reset the frames for the new recording
- frames = [];
- whammy.frames = dropBlackFrames(internal_frames, -1);
+ whammy.frames = dropBlackFrames(internalFrames, -1);
- var WebM_Blob = whammy.compile();
- root.ondataavailable(WebM_Blob);
+ whammy.compile(function(whammyBlob) {
+ root.ondataavailable(whammyBlob);
+ console.debug('video recorded blob size:', bytesToSize(whammyBlob.size));
+ });
- console.debug('video recorded blob size:', bytesToSize(WebM_Blob.size));
+ whammy.frames = [];
requestDataInvoked = false;
};
- var frames = [];
-
var isOnStartedDrawingNonBlankFramesInvoked = false;
function drawFrames() {
- if (isStopDrawing) return;
+ if (isPaused) {
+ lastTime = new Date().getTime();
+ setTimeout(drawFrames, 500);
+ return;
+ }
+
+ if (isStopDrawing) {
+ return;
+ }
- if (requestDataInvoked) return setTimeout(drawFrames, 100);
+ if (requestDataInvoked) {
+ return setTimeout(drawFrames, 100);
+ }
var duration = new Date().getTime() - lastTime;
- if (!duration) return drawFrames();
+ if (!duration) {
+ return drawFrames();
+ }
// via webrtc-experiment#206, by Jack i.e. @Seymourr
lastTime = new Date().getTime();
+ if (!self.isHTMLObject && video.paused) {
+ video.play(); // Android
+ }
+
context.drawImage(video, 0, 0, canvas.width, canvas.height);
- !isStopDrawing && frames.push({
- duration: duration,
- image: canvas.toDataURL('image/webp')
- });
- if (!isOnStartedDrawingNonBlankFramesInvoked && !isBlankFrame(frames[frames.length - 1])) {
+ if (!isStopDrawing) {
+ whammy.frames.push({
+ duration: duration,
+ image: canvas.toDataURL('image/webp')
+ });
+ }
+
+ if (!isOnStartedDrawingNonBlankFramesInvoked && !isBlankFrame(whammy.frames[whammy.frames.length - 1])) {
isOnStartedDrawingNonBlankFramesInvoked = true;
root.onStartedDrawingNonBlankFrames();
}
@@ -252,4 +283,14 @@ function WhammyRecorderHelper(mediaStream, root) {
return resultFrames;
}
+
+ var isPaused = false;
+
+ this.pause = function() {
+ isPaused = true;
+ };
+
+ this.resume = function() {
+ isPaused = false;
+ };
}
diff --git a/VideoStreamRecorder/lib/whammy.js b/VideoStreamRecorder/lib/whammy.js
index e70a1e7..2c7b112 100644
--- a/VideoStreamRecorder/lib/whammy.js
+++ b/VideoStreamRecorder/lib/whammy.js
@@ -1,353 +1,430 @@
-// Muaz Khan - https://github.com/muaz-khan
-// neizerth - https://github.com/neizerth
-// MIT License - https://www.webrtc-experiment.com/licence/
-// Documentation - https://github.com/streamproc/MediaStreamRecorder
-
-// Note:
-// ==========================================================
-// whammy.js is an "external library"
-// and has its own copyrights. Taken from "Whammy" project.
-
-
// https://github.com/antimatter15/whammy/blob/master/LICENSE
-// =========
+// _________
// Whammy.js
// todo: Firefox now supports webp for webm containers!
// their MediaRecorder implementation works well!
// should we provide an option to record via Whammy.js or MediaRecorder API is a better solution?
+/**
+ * Whammy is a standalone class used by {@link RecordRTC} to bring video recording in Chrome. It is written by {@link https://github.com/antimatter15|antimatter15}
+ * @summary A real time javascript webm encoder based on a canvas hack.
+ * @typedef Whammy
+ * @class
+ * @example
+ * var recorder = new Whammy().Video(15);
+ * recorder.add(context || canvas || dataURL);
+ * var output = recorder.compile();
+ */
+
var Whammy = (function() {
+ // a more abstract-ish API
- function toWebM(frames) {
- var info = checkFrames(frames);
+ function WhammyVideo(duration) {
+ this.frames = [];
+ this.duration = duration || 1;
+ this.quality = 0.8;
+ }
- var CLUSTER_MAX_DURATION = 30000;
+ /**
+ * Pass Canvas or Context or image/webp(string) to {@link Whammy} encoder.
+ * @method
+ * @memberof Whammy
+ * @example
+ * recorder = new Whammy().Video(0.8, 100);
+ * recorder.add(canvas || context || 'image/webp');
+ * @param {string} frame - Canvas || Context || image/webp
+ * @param {number} duration - Stick a duration (in milliseconds)
+ */
+ WhammyVideo.prototype.add = function(frame, duration) {
+ if ('canvas' in frame) { //CanvasRenderingContext2D
+ frame = frame.canvas;
+ }
- var EBML = [{
- "id": 0x1a45dfa3, // EBML
- "data": [{
- "data": 1,
- "id": 0x4286 // EBMLVersion
- }, {
- "data": 1,
- "id": 0x42f7 // EBMLReadVersion
- }, {
- "data": 4,
- "id": 0x42f2 // EBMLMaxIDLength
- }, {
- "data": 8,
- "id": 0x42f3 // EBMLMaxSizeLength
- }, {
- "data": "webm",
- "id": 0x4282 // DocType
- }, {
- "data": 2,
- "id": 0x4287 // DocTypeVersion
- }, {
- "data": 2,
- "id": 0x4285 // DocTypeReadVersion
- }]
- }, {
- "id": 0x18538067, // Segment
- "data": [{
- "id": 0x1549a966, // Info
- "data": [{
- "data": 1e6, //do things in millisecs (num of nanosecs for duration scale)
- "id": 0x2ad7b1 // TimecodeScale
+ if ('toDataURL' in frame) {
+ frame = frame.toDataURL('image/webp', this.quality);
+ }
+
+ if (!(/^data:image\/webp;base64,/ig).test(frame)) {
+ throw 'Input must be formatted properly as a base64 encoded DataURI of type image/webp';
+ }
+ this.frames.push({
+ image: frame,
+ duration: duration || this.duration
+ });
+ };
+
+ function processInWebWorker(_function) {
+ var blob = URL.createObjectURL(new Blob([_function.toString(),
+ 'this.onmessage = function (e) {' + _function.name + '(e.data);}'
+ ], {
+ type: 'application/javascript'
+ }));
+
+ var worker = new Worker(blob);
+ URL.revokeObjectURL(blob);
+ return worker;
+ }
+
+ function whammyInWebWorker(frames) {
+ function ArrayToWebM(frames) {
+ var info = checkFrames(frames);
+ if (!info) {
+ return [];
+ }
+
+ var clusterMaxDuration = 30000;
+
+ var EBML = [{
+ 'id': 0x1a45dfa3, // EBML
+ 'data': [{
+ 'data': 1,
+ 'id': 0x4286 // EBMLVersion
+ }, {
+ 'data': 1,
+ 'id': 0x42f7 // EBMLReadVersion
}, {
- "data": "whammy",
- "id": 0x4d80 // MuxingApp
+ 'data': 4,
+ 'id': 0x42f2 // EBMLMaxIDLength
}, {
- "data": "whammy",
- "id": 0x5741 // WritingApp
+ 'data': 8,
+ 'id': 0x42f3 // EBMLMaxSizeLength
}, {
- "data": doubleToString(info.duration),
- "id": 0x4489 // Duration
+ 'data': 'webm',
+ 'id': 0x4282 // DocType
+ }, {
+ 'data': 2,
+ 'id': 0x4287 // DocTypeVersion
+ }, {
+ 'data': 2,
+ 'id': 0x4285 // DocTypeReadVersion
}]
}, {
- "id": 0x1654ae6b, // Tracks
- "data": [{
- "id": 0xae, // TrackEntry
- "data": [{
- "data": 1,
- "id": 0xd7 // TrackNumber
- }, {
- "data": 1,
- "id": 0x63c5 // TrackUID
- }, {
- "data": 0,
- "id": 0x9c // FlagLacing
+ 'id': 0x18538067, // Segment
+ 'data': [{
+ 'id': 0x1549a966, // Info
+ 'data': [{
+ 'data': 1e6, //do things in millisecs (num of nanosecs for duration scale)
+ 'id': 0x2ad7b1 // TimecodeScale
}, {
- "data": "und",
- "id": 0x22b59c // Language
+ 'data': 'whammy',
+ 'id': 0x4d80 // MuxingApp
}, {
- "data": "V_VP8",
- "id": 0x86 // CodecID
+ 'data': 'whammy',
+ 'id': 0x5741 // WritingApp
}, {
- "data": "VP8",
- "id": 0x258688 // CodecName
- }, {
- "data": 1,
- "id": 0x83 // TrackType
- }, {
- "id": 0xe0, // Video
- "data": [{
- "data": info.width,
- "id": 0xb0 // PixelWidth
+ 'data': doubleToString(info.duration),
+ 'id': 0x4489 // Duration
+ }]
+ }, {
+ 'id': 0x1654ae6b, // Tracks
+ 'data': [{
+ 'id': 0xae, // TrackEntry
+ 'data': [{
+ 'data': 1,
+ 'id': 0xd7 // TrackNumber
+ }, {
+ 'data': 1,
+ 'id': 0x73c5 // TrackUID
+ }, {
+ 'data': 0,
+ 'id': 0x9c // FlagLacing
+ }, {
+ 'data': 'und',
+ 'id': 0x22b59c // Language
}, {
- "data": info.height,
- "id": 0xba // PixelHeight
+ 'data': 'V_VP8',
+ 'id': 0x86 // CodecID
+ }, {
+ 'data': 'VP8',
+ 'id': 0x258688 // CodecName
+ }, {
+ 'data': 1,
+ 'id': 0x83 // TrackType
+ }, {
+ 'id': 0xe0, // Video
+ 'data': [{
+ 'data': info.width,
+ 'id': 0xb0 // PixelWidth
+ }, {
+ 'data': info.height,
+ 'id': 0xba // PixelHeight
+ }]
}]
}]
}]
- }]
- }];
-
- //Generate clusters (max duration)
- var frameNumber = 0;
- var clusterTimecode = 0;
- while (frameNumber < frames.length) {
-
- var clusterFrames = [];
- var clusterDuration = 0;
- do {
- clusterFrames.push(frames[frameNumber]);
- clusterDuration += frames[frameNumber].duration;
- frameNumber++;
- } while (frameNumber < frames.length && clusterDuration < CLUSTER_MAX_DURATION);
-
- var clusterCounter = 0;
- var cluster = {
- "id": 0x1f43b675, // Cluster
- "data": [{
- "data": clusterTimecode,
- "id": 0xe7 // Timecode
- }].concat(clusterFrames.map(function(webp) {
- var block = makeSimpleBlock({
- discardable: 0,
- frame: webp.data.slice(4),
- invisible: 0,
- keyframe: 1,
- lacing: 0,
- trackNum: 1,
- timecode: Math.round(clusterCounter)
- });
- clusterCounter += webp.duration;
- return {
- data: block,
- id: 0xa3
- };
- }))
- }; //Add cluster to segment
- EBML[1].data.push(cluster);
- clusterTimecode += clusterDuration;
+ }];
+
+ //Generate clusters (max duration)
+ var frameNumber = 0;
+ var clusterTimecode = 0;
+ while (frameNumber < frames.length) {
+
+ var clusterFrames = [];
+ var clusterDuration = 0;
+ do {
+ clusterFrames.push(frames[frameNumber]);
+ clusterDuration += frames[frameNumber].duration;
+ frameNumber++;
+ } while (frameNumber < frames.length && clusterDuration < clusterMaxDuration);
+
+ var clusterCounter = 0;
+ var cluster = {
+ 'id': 0x1f43b675, // Cluster
+ 'data': getClusterData(clusterTimecode, clusterCounter, clusterFrames)
+ }; //Add cluster to segment
+ EBML[1].data.push(cluster);
+ clusterTimecode += clusterDuration;
+ }
+
+ return generateEBML(EBML);
}
- return generateEBML(EBML);
- }
+ function getClusterData(clusterTimecode, clusterCounter, clusterFrames) {
+ return [{
+ 'data': clusterTimecode,
+ 'id': 0xe7 // Timecode
+ }].concat(clusterFrames.map(function(webp) {
+ var block = makeSimpleBlock({
+ discardable: 0,
+ frame: webp.data.slice(4),
+ invisible: 0,
+ keyframe: 1,
+ lacing: 0,
+ trackNum: 1,
+ timecode: Math.round(clusterCounter)
+ });
+ clusterCounter += webp.duration;
+ return {
+ data: block,
+ id: 0xa3
+ };
+ }));
+ }
- // sums the lengths of all the frames and gets the duration
+ // sums the lengths of all the frames and gets the duration
- function checkFrames(frames) {
- if (!frames[0]) {
- console.warn('Something went wrong. Maybe WebP format is not supported in the current browser.');
- return;
- }
+ function checkFrames(frames) {
+ if (!frames[0]) {
+ postMessage({
+ error: 'Something went wrong. Maybe WebP format is not supported in the current browser.'
+ });
+ return;
+ }
- var width = frames[0].width,
- height = frames[0].height,
- duration = frames[0].duration;
+ var width = frames[0].width,
+ height = frames[0].height,
+ duration = frames[0].duration;
- for (var i = 1; i < frames.length; i++) {
- duration += frames[i].duration;
+ for (var i = 1; i < frames.length; i++) {
+ duration += frames[i].duration;
+ }
+ return {
+ duration: duration,
+ width: width,
+ height: height
+ };
}
- return {
- duration: duration,
- width: width,
- height: height
- };
- }
- function numToBuffer(num) {
- var parts = [];
- while (num > 0) {
- parts.push(num & 0xff);
- num = num >> 8;
+ function numToBuffer(num) {
+ var parts = [];
+ while (num > 0) {
+ parts.push(num & 0xff);
+ num = num >> 8;
+ }
+ return new Uint8Array(parts.reverse());
}
- return new Uint8Array(parts.reverse());
- }
-
- function strToBuffer(str) {
- return new Uint8Array(str.split('').map(function(e) {
- return e.charCodeAt(0);
- }));
- }
- function bitsToBuffer(bits) {
- var data = [];
- var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : '';
- bits = pad + bits;
- for (var i = 0; i < bits.length; i += 8) {
- data.push(parseInt(bits.substr(i, 8), 2));
+ function strToBuffer(str) {
+ return new Uint8Array(str.split('').map(function(e) {
+ return e.charCodeAt(0);
+ }));
}
- return new Uint8Array(data);
- }
- function generateEBML(json) {
- var ebml = [];
- for (var i = 0; i < json.length; i++) {
- var data = json[i].data;
- if (typeof data == 'object') data = generateEBML(data);
- if (typeof data == 'number') data = bitsToBuffer(data.toString(2));
- if (typeof data == 'string') data = strToBuffer(data);
-
- var len = data.size || data.byteLength || data.length;
- var zeroes = Math.ceil(Math.ceil(Math.log(len) / Math.log(2)) / 8);
- var size_str = len.toString(2);
- var padded = (new Array((zeroes * 7 + 7 + 1) - size_str.length)).join('0') + size_str;
- var size = (new Array(zeroes)).join('0') + '1' + padded;
-
- ebml.push(numToBuffer(json[i].id));
- ebml.push(bitsToBuffer(size));
- ebml.push(data);
+ function bitsToBuffer(bits) {
+ var data = [];
+ var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : '';
+ bits = pad + bits;
+ for (var i = 0; i < bits.length; i += 8) {
+ data.push(parseInt(bits.substr(i, 8), 2));
+ }
+ return new Uint8Array(data);
}
- return new Blob(ebml, {
- type: "video/webm"
- });
- }
+ function generateEBML(json) {
+ var ebml = [];
+ for (var i = 0; i < json.length; i++) {
+ var data = json[i].data;
- function toBinStr_old(bits) {
- var data = '';
- var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : '';
- bits = pad + bits;
- for (var i = 0; i < bits.length; i += 8) {
- data += String.fromCharCode(parseInt(bits.substr(i, 8), 2));
- }
- return data;
- }
+ if (typeof data === 'object') {
+ data = generateEBML(data);
+ }
- function generateEBML_old(json) {
- var ebml = '';
- for (var i = 0; i < json.length; i++) {
- var data = json[i].data;
- if (typeof data == 'object') data = generateEBML_old(data);
- if (typeof data == 'number') data = toBinStr_old(data.toString(2));
+ if (typeof data === 'number') {
+ data = bitsToBuffer(data.toString(2));
+ }
- var len = data.length;
- var zeroes = Math.ceil(Math.ceil(Math.log(len) / Math.log(2)) / 8);
- var size_str = len.toString(2);
- var padded = (new Array((zeroes * 7 + 7 + 1) - size_str.length)).join('0') + size_str;
- var size = (new Array(zeroes)).join('0') + '1' + padded;
+ if (typeof data === 'string') {
+ data = strToBuffer(data);
+ }
- ebml += toBinStr_old(json[i].id.toString(2)) + toBinStr_old(size) + data;
+ var len = data.size || data.byteLength || data.length;
+ var zeroes = Math.ceil(Math.ceil(Math.log(len) / Math.log(2)) / 8);
+ var sizeToString = len.toString(2);
+ var padded = (new Array((zeroes * 7 + 7 + 1) - sizeToString.length)).join('0') + sizeToString;
+ var size = (new Array(zeroes)).join('0') + '1' + padded;
- }
- return ebml;
- }
+ ebml.push(numToBuffer(json[i].id));
+ ebml.push(bitsToBuffer(size));
+ ebml.push(data);
+ }
- function makeSimpleBlock(data) {
- var flags = 0;
- if (data.keyframe) flags |= 128;
- if (data.invisible) flags |= 8;
- if (data.lacing) flags |= (data.lacing << 1);
- if (data.discardable) flags |= 1;
- if (data.trackNum > 127) {
- throw "TrackNumber > 127 not supported";
+ return new Blob(ebml, {
+ type: 'video/webm'
+ });
}
- var out = [data.trackNum | 0x80, data.timecode >> 8, data.timecode & 0xff, flags].map(function(e) {
- return String.fromCharCode(e);
- }).join('') + data.frame;
- return out;
- }
+ function toBinStrOld(bits) {
+ var data = '';
+ var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : '';
+ bits = pad + bits;
+ for (var i = 0; i < bits.length; i += 8) {
+ data += String.fromCharCode(parseInt(bits.substr(i, 8), 2));
+ }
+ return data;
+ }
- function parseWebP(riff) {
- var VP8 = riff.RIFF[0].WEBP[0];
+ function makeSimpleBlock(data) {
+ var flags = 0;
- var frame_start = VP8.indexOf('\x9d\x01\x2a'); // A VP8 keyframe starts with the 0x9d012a header
- for (var i = 0, c = []; i < 4; i++) c[i] = VP8.charCodeAt(frame_start + 3 + i);
+ if (data.keyframe) {
+ flags |= 128;
+ }
- var width, height, tmp;
+ if (data.invisible) {
+ flags |= 8;
+ }
- //the code below is literally copied verbatim from the bitstream spec
- tmp = (c[1] << 8) | c[0];
- width = tmp & 0x3FFF;
- tmp = (c[3] << 8) | c[2];
- height = tmp & 0x3FFF;
- return {
- width: width,
- height: height,
- data: VP8,
- riff: riff
- };
- }
+ if (data.lacing) {
+ flags |= (data.lacing << 1);
+ }
- function parseRIFF(string) {
- var offset = 0;
- var chunks = {};
+ if (data.discardable) {
+ flags |= 1;
+ }
- while (offset < string.length) {
- var id = string.substr(offset, 4);
- var len = parseInt(string.substr(offset + 4, 4).split('').map(function(i) {
- var unpadded = i.charCodeAt(0).toString(2);
- return (new Array(8 - unpadded.length + 1)).join('0') + unpadded;
- }).join(''), 2);
- var data = string.substr(offset + 4 + 4, len);
- offset += 4 + 4 + len;
- chunks[id] = chunks[id] || [];
-
- if (id == 'RIFF' || id == 'LIST') {
- chunks[id].push(parseRIFF(data));
- } else {
- chunks[id].push(data);
+ if (data.trackNum > 127) {
+ throw 'TrackNumber > 127 not supported';
}
+
+ var out = [data.trackNum | 0x80, data.timecode >> 8, data.timecode & 0xff, flags].map(function(e) {
+ return String.fromCharCode(e);
+ }).join('') + data.frame;
+
+ return out;
}
- return chunks;
- }
- function doubleToString(num) {
- return [].slice.call(
- new Uint8Array((new Float64Array([num])).buffer), 0).map(function(e) {
- return String.fromCharCode(e);
- }).reverse().join('');
- }
+ function parseWebP(riff) {
+ var VP8 = riff.RIFF[0].WEBP[0];
- // a more abstract-ish API
+ var frameStart = VP8.indexOf('\x9d\x01\x2a'); // A VP8 keyframe starts with the 0x9d012a header
+ for (var i = 0, c = []; i < 4; i++) {
+ c[i] = VP8.charCodeAt(frameStart + 3 + i);
+ }
- function WhammyVideo(duration) {
- this.frames = [];
- this.duration = duration || 1;
- this.quality = 100;
- }
+ var width, height, tmp;
+
+ //the code below is literally copied verbatim from the bitstream spec
+ tmp = (c[1] << 8) | c[0];
+ width = tmp & 0x3FFF;
+ tmp = (c[3] << 8) | c[2];
+ height = tmp & 0x3FFF;
+ return {
+ width: width,
+ height: height,
+ data: VP8,
+ riff: riff
+ };
+ }
- WhammyVideo.prototype.add = function(frame, duration) {
- if ('canvas' in frame) { //CanvasRenderingContext2D
- frame = frame.canvas;
+ function getStrLength(string, offset) {
+ return parseInt(string.substr(offset + 4, 4).split('').map(function(i) {
+ var unpadded = i.charCodeAt(0).toString(2);
+ return (new Array(8 - unpadded.length + 1)).join('0') + unpadded;
+ }).join(''), 2);
}
- if ('toDataURL' in frame) {
- frame = frame.toDataURL('image/webp', this.quality);
+ function parseRIFF(string) {
+ var offset = 0;
+ var chunks = {};
+
+ while (offset < string.length) {
+ var id = string.substr(offset, 4);
+ var len = getStrLength(string, offset);
+ var data = string.substr(offset + 4 + 4, len);
+ offset += 4 + 4 + len;
+ chunks[id] = chunks[id] || [];
+
+ if (id === 'RIFF' || id === 'LIST') {
+ chunks[id].push(parseRIFF(data));
+ } else {
+ chunks[id].push(data);
+ }
+ }
+ return chunks;
}
- if (!(/^data:image\/webp;base64,/ig).test(frame)) {
- throw "Input must be formatted properly as a base64 encoded DataURI of type image/webp";
+ function doubleToString(num) {
+ return [].slice.call(
+ new Uint8Array((new Float64Array([num])).buffer), 0).map(function(e) {
+ return String.fromCharCode(e);
+ }).reverse().join('');
}
- this.frames.push({
- image: frame,
- duration: duration || this.duration
- });
- };
- WhammyVideo.prototype.compile = function() {
- return new toWebM(this.frames.map(function(frame) {
+
+ var webm = new ArrayToWebM(frames.map(function(frame) {
var webp = parseWebP(parseRIFF(atob(frame.image.slice(23))));
webp.duration = frame.duration;
return webp;
}));
+
+ postMessage(webm);
+ }
+
+ /**
+ * Encodes frames in WebM container. It uses WebWorkinvoke to invoke 'ArrayToWebM' method.
+ * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee.
+ * @method
+ * @memberof Whammy
+ * @example
+ * recorder = new Whammy().Video(0.8, 100);
+ * recorder.compile(function(blob) {
+ * // blob.size - blob.type
+ * });
+ */
+ WhammyVideo.prototype.compile = function(callback) {
+ var webWorker = processInWebWorker(whammyInWebWorker);
+
+ webWorker.onmessage = function(event) {
+ if (event.data.error) {
+ console.error(event.data.error);
+ return;
+ }
+ callback(event.data);
+ };
+
+ webWorker.postMessage(this.frames);
};
+
return {
- Video: WhammyVideo,
- toWebM: toWebM
+ /**
+ * A more abstract-ish API.
+ * @method
+ * @memberof Whammy
+ * @example
+ * recorder = new Whammy().Video(0.8, 100);
+ * @param {?number} speed - 0.8
+ * @param {?number} quality - 100
+ */
+ Video: WhammyVideo
};
})();
diff --git a/bug.md b/bug.md
deleted file mode 100644
index 0639f42..0000000
--- a/bug.md
+++ /dev/null
@@ -1,28 +0,0 @@
-## List of all bugs in MediaStreamRecorder.js
-
-It is a list of all bugs need to be fixed.
-
-=
-
-##### AudioStreamRecorder/MediaRecorder.js `Line 27`
-
-1. It is not a valid behavior
-2. Mozilla Firefox Nightly crashes
-3. Redundant memory usage
-
-=
-
-##### VideoStreamRecorder/GifRecorder.js `Line 79`
-
-1. Must be able to clear old recorded GIFs
-2. Both WebM and Gif recorders must work fine on Firefox
-
-`context.drawImage` seems throwing error on Firefox: `NS_ERROR_NOT_AVAILABLE: Component is not available`.
-
-Also, `uncaught exception: Input must be formatted properly as a base64 encoded DataURI of type image/webp`.
-
-=
-
-##### License
-
-[MediaStreamRecorder.js](https://github.com/streamproc/MediaStreamRecorder) library is released under [MIT licence](https://www.webrtc-experiment.com/licence/) . Copyright (c) 2013 [Muaz Khan](https://github.com/muaz-khan) and [neizerth](https://github.com/neizerth).
diff --git a/common/Cross-Browser-Declarations.js b/common/Cross-Browser-Declarations.js
index 0f985ee..5e6eb56 100644
--- a/common/Cross-Browser-Declarations.js
+++ b/common/Cross-Browser-Declarations.js
@@ -1,30 +1,46 @@
-// Muaz Khan - www.MuazKhan.com
-// MIT License - www.webrtc-experiment.com/licence
-// Documentation - github.com/streamproc/MediaStreamRecorder
-
// _____________________________
// Cross-Browser-Declarations.js
-// animation-frame used in WebM recording
-if (!window.requestAnimationFrame) {
- requestAnimationFrame = window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame;
-}
+// WebAudio API representer
+if (typeof AudioContext !== 'undefined') {
+ if (typeof webkitAudioContext !== 'undefined') {
+ /*global AudioContext:true*/
+ var AudioContext = webkitAudioContext;
+ }
-if (!window.cancelAnimationFrame) {
- cancelAnimationFrame = window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame;
+ if (typeof mozAudioContext !== 'undefined') {
+ /*global AudioContext:true*/
+ var AudioContext = mozAudioContext;
+ }
}
-// WebAudio API representer
-if (!window.AudioContext) {
- window.AudioContext = window.webkitAudioContext || window.mozAudioContext;
+if (typeof URL !== 'undefined' && typeof webkitURL !== 'undefined') {
+ /*global URL:true*/
+ var URL = webkitURL;
}
-URL = window.URL || window.webkitURL;
-navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
+var IsEdge = navigator.userAgent.indexOf('Edge') !== -1 && (!!navigator.msSaveBlob || !!navigator.msSaveOrOpenBlob);
+var IsOpera = !!window.opera || navigator.userAgent.indexOf('OPR/') !== -1;
+var IsChrome = !IsEdge && !IsEdge && !!navigator.webkitGetUserMedia;
+
+if (typeof navigator !== 'undefined') {
+ if (typeof navigator.webkitGetUserMedia !== 'undefined') {
+ navigator.getUserMedia = navigator.webkitGetUserMedia;
+ }
-if (window.webkitMediaStream) window.MediaStream = window.webkitMediaStream;
+ if (typeof navigator.mozGetUserMedia !== 'undefined') {
+ navigator.getUserMedia = navigator.mozGetUserMedia;
+ }
+} else {
+ /*global navigator:true */
+ var navigator = {
+ getUserMedia: {}
+ };
+}
-IsChrome = !!navigator.webkitGetUserMedia;
+if (typeof webkitMediaStream !== 'undefined') {
+ var MediaStream = webkitMediaStream;
+}
// Merge all other data-types except "function"
@@ -41,11 +57,13 @@ function mergeProps(mergein, mergeto) {
function reformatProps(obj) {
var output = {};
for (var o in obj) {
- if (o.indexOf('-') != -1) {
+ if (o.indexOf('-') !== -1) {
var splitted = o.split('-');
var name = splitted[0] + splitted[1].split('')[0].toUpperCase() + splitted[1].substr(1);
output[name] = obj[o];
- } else output[o] = obj[o];
+ } else {
+ output[o] = obj[o];
+ }
}
return output;
}
@@ -58,6 +76,56 @@ function dropFirstFrame(arr) {
return arr;
}
+function invokeSaveAsDialog(file, fileName) {
+ if (!file) {
+ throw 'Blob object is required.';
+ }
+
+ if (!file.type) {
+ file.type = 'video/webm';
+ }
+
+ var fileExtension = file.type.split('/')[1];
+
+ if (fileName && fileName.indexOf('.') !== -1) {
+ var splitted = fileName.split('.');
+ fileName = splitted[0];
+ fileExtension = splitted[1];
+ }
+
+ var fileFullName = (fileName || (Math.round(Math.random() * 9999999999) + 888888888)) + '.' + fileExtension;
+
+ if (typeof navigator.msSaveOrOpenBlob !== 'undefined') {
+ return navigator.msSaveOrOpenBlob(file, fileFullName);
+ } else if (typeof navigator.msSaveBlob !== 'undefined') {
+ return navigator.msSaveBlob(file, fileFullName);
+ }
+
+ var hyperlink = document.createElement('a');
+ hyperlink.href = URL.createObjectURL(file);
+ hyperlink.target = '_blank';
+ hyperlink.download = fileFullName;
+
+ if (!!navigator.mozGetUserMedia) {
+ hyperlink.onclick = function() {
+ (document.body || document.documentElement).removeChild(hyperlink);
+ };
+ (document.body || document.documentElement).appendChild(hyperlink);
+ }
+
+ var evt = new MouseEvent('click', {
+ view: window,
+ bubbles: true,
+ cancelable: true
+ });
+
+ hyperlink.dispatchEvent(evt);
+
+ if (!navigator.mozGetUserMedia) {
+ URL.revokeObjectURL(hyperlink.href);
+ }
+}
+
function bytesToSize(bytes) {
var k = 1000;
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
@@ -67,3 +135,9 @@ function bytesToSize(bytes) {
var i = parseInt(Math.floor(Math.log(bytes) / Math.log(k)), 10);
return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i];
}
+
+// ______________ (used to handle stuff like http://goo.gl/xmE5eg) issue #129
+// ObjectStore.js
+var ObjectStore = {
+ AudioContext: window.AudioContext || window.webkitAudioContext
+};
diff --git a/common/MediaStreamRecorder.js b/common/MediaStreamRecorder.js
new file mode 100644
index 0000000..e160a61
--- /dev/null
+++ b/common/MediaStreamRecorder.js
@@ -0,0 +1,107 @@
+// ______________________
+// MediaStreamRecorder.js
+
+function MediaStreamRecorder(mediaStream) {
+ if (!mediaStream) {
+ throw 'MediaStream is mandatory.';
+ }
+
+ // void start(optional long timeSlice)
+ // timestamp to fire "ondataavailable"
+ this.start = function(timeSlice) {
+ // Media Stream Recording API has not been implemented in chrome yet;
+ // That's why using WebAudio API to record stereo audio in WAV format
+ var Recorder = IsChrome || IsEdge || IsOpera ? window.StereoAudioRecorder || IsEdge || IsOpera : window.MediaRecorderWrapper;
+
+ // video recorder (in WebM format)
+ if (this.mimeType.indexOf('video') !== -1) {
+ Recorder = IsChrome || IsEdge || IsOpera ? window.WhammyRecorder : window.MediaRecorderWrapper;
+ }
+
+ // video recorder (in GIF format)
+ if (this.mimeType === 'image/gif') {
+ Recorder = window.GifRecorder;
+ }
+
+ // allows forcing StereoAudioRecorder.js on Edge/Firefox
+ if (this.recorderType) {
+ Recorder = this.recorderType;
+ }
+
+ mediaRecorder = new Recorder(mediaStream);
+ mediaRecorder.blobs = [];
+
+ var self = this;
+ mediaRecorder.ondataavailable = function(data) {
+ mediaRecorder.blobs.push(data);
+ self.ondataavailable(data);
+ };
+ mediaRecorder.onstop = this.onstop;
+ mediaRecorder.onStartedDrawingNonBlankFrames = this.onStartedDrawingNonBlankFrames;
+
+ // Merge all data-types except "function"
+ mediaRecorder = mergeProps(mediaRecorder, this);
+
+ mediaRecorder.start(timeSlice);
+ };
+
+ this.onStartedDrawingNonBlankFrames = function() {};
+ this.clearOldRecordedFrames = function() {
+ if (!mediaRecorder) {
+ return;
+ }
+
+ mediaRecorder.clearOldRecordedFrames();
+ };
+
+ this.stop = function() {
+ if (mediaRecorder) {
+ mediaRecorder.stop();
+ }
+ };
+
+ this.ondataavailable = function(blob) {
+ console.log('ondataavailable..', blob);
+ };
+
+ this.onstop = function(error) {
+ console.warn('stopped..', error);
+ };
+
+ this.save = function(file, fileName) {
+ if (!file) {
+ if (!mediaRecorder) {
+ return;
+ }
+
+ var bigBlob = new Blob(mediaRecorder.blobs, {
+ type: mediaRecorder.blobs[0].type || this.mimeType
+ });
+
+ invokeSaveAsDialog(bigBlob);
+ return;
+ }
+ invokeSaveAsDialog(file, fileName);
+ };
+
+ this.pause = function() {
+ if (!mediaRecorder) {
+ return;
+ }
+ mediaRecorder.pause();
+ console.log('Paused recording.', this.mimeType || mediaRecorder.mimeType);
+ };
+
+ this.resume = function() {
+ if (!mediaRecorder) {
+ return;
+ }
+ mediaRecorder.resume();
+ console.log('Resumed recording.', this.mimeType || mediaRecorder.mimeType);
+ };
+
+ this.recorderType = null; // StereoAudioRecorder || WhammyRecorder || MediaRecorderWrapper || GifRecorder
+
+ // Reference to "MediaRecorder.js"
+ var mediaRecorder;
+}
diff --git a/MultiStreamRecorder.js b/common/MultiStreamRecorder.js
similarity index 83%
rename from MultiStreamRecorder.js
rename to common/MultiStreamRecorder.js
index f5d4eca..233d7f1 100644
--- a/MultiStreamRecorder.js
+++ b/common/MultiStreamRecorder.js
@@ -1,12 +1,10 @@
-// Muaz Khan - www.MuazKhan.com
-// MIT License - www.webrtc-experiment.com/licence
-// Documentation - github.com/streamproc/MediaStreamRecorder
-
// ______________________
// MultiStreamRecorder.js
function MultiStreamRecorder(mediaStream) {
- if (!mediaStream) throw 'MediaStream is mandatory.';
+ if (!mediaStream) {
+ throw 'MediaStream is mandatory.';
+ }
var self = this;
var isFirefox = !!navigator.mozGetUserMedia;
@@ -83,8 +81,12 @@ function MultiStreamRecorder(mediaStream) {
};
this.stop = function() {
- if (audioRecorder) audioRecorder.stop();
- if (videoRecorder) videoRecorder.stop();
+ if (audioRecorder) {
+ audioRecorder.stop();
+ }
+ if (videoRecorder) {
+ videoRecorder.stop();
+ }
};
this.ondataavailable = function(blob) {
@@ -95,6 +97,24 @@ function MultiStreamRecorder(mediaStream) {
console.warn('stopped..', error);
};
+ this.pause = function() {
+ if (audioRecorder) {
+ audioRecorder.pause();
+ }
+ if (videoRecorder) {
+ videoRecorder.pause();
+ }
+ };
+
+ this.resume = function() {
+ if (audioRecorder) {
+ audioRecorder.resume();
+ }
+ if (videoRecorder) {
+ videoRecorder.resume();
+ }
+ };
+
var audioRecorder;
var videoRecorder;
diff --git a/common/OpentTokStreamRecorder.js b/common/OpentTokStreamRecorder.js
new file mode 100644
index 0000000..7a25e6c
--- /dev/null
+++ b/common/OpentTokStreamRecorder.js
@@ -0,0 +1,333 @@
+// muazkh - github.com/muaz-khan
+// @neizerth - github.com/neizerth
+// MIT License - https://webrtc-experiment.appspot.com/licence/
+// Documentation - https://github.com/streamproc/MediaStreamRecorder
+// ==========================================================
+// OpentTokStreamRecorder.js
+
+function OpenTokStreamRecorder(o) {
+ var defaults = {
+ webrtc: false,
+ apiKey: null,
+ remoteApiKey: null,
+ sessionId: '',
+ token: null,
+ remoteToken: null,
+ remoteArchive: null,
+ saveAterStop: true,
+ container: null,
+ debug: false,
+ pollWhileProcessing: true,
+ style: null, // null | 'blank' | object (see http://www.tokbox.com/opentok/docs/js/reference/Recorder.html)
+ loadingDuration: 2000, // dirty hack while opentox doesnt support onready event
+ onerror: function() {
+
+ },
+ onstart: function() {
+
+ },
+ onstop: function() {
+
+ },
+ ondataavailable: function() {
+
+ },
+ onready: function() {
+
+ },
+ onsavecomplete: function() {
+
+ }
+ },
+ remoteDefaults = {
+ url: window.location,
+ method: 'GET',
+ data: {},
+ success: function() {},
+ error: function() {}
+ },
+ baseUrl = getBaseUrl(),
+ self = this,
+ _initialized = false,
+ _startRequest = false,
+ options = extend(defaults, o),
+ recorder;
+
+ include('https://swww.tokbox.com/v1.1/js/TB.min.js', init);
+
+ function init() {
+ if (options.remoteApiKey != null) {
+ getUrlData(options.remoteApiKey, function(key) {
+ options.apiKey = key;
+ createRecorder();
+ });
+ }
+ if (options.remoteToken != null) {
+ getUrlData(options.remoteToken, function(token) {
+ options.token = token;
+ createRecorder();
+ });
+ }
+ createRecorder();
+ }
+
+ function start() {
+ _startRequest = true;
+ if (_initialized) {
+ _startRequest = false;
+ if (self.state != 'inactive') {
+ var obj = {
+ message: 'The object is in an invalid state',
+ code: DOMException.INVALID_STATE_ERR
+ }
+ self.onerror(obj);
+ throw obj;
+ }
+ self.state = 'recording';
+
+ recorder.startRecording();
+ }
+ }
+
+ function stop() {
+ if (self.state == 'inactive') {
+ var obj = {
+ message: 'The object is in an invalid state',
+ code: DOMException.INVALID_STATE_ERR
+ }
+ self.onerror(obj);
+ throw obj;
+ }
+ self.state = 'inactive';
+
+ recorder.stopRecording();
+
+ if (options.saveAterStop) {
+ save();
+ };
+ }
+
+ function save() {
+ recorder.saveArchive();
+ }
+
+ function createRecorder() {
+ if (options.apiKey != null && options.token != null && !_initialized) {
+ _initialized = true;
+ if (options.debug) {
+ TB.setLogLevel(TB.DEBUG); // Prints out logging messages in console
+ }
+ var recorderManager = TB.initRecorderManager(options.apiKey),
+ recDiv = document.createElement('div'),
+ container = o.container == null ? document.body : o.container;
+
+ recDiv.setAttribute('id', 'recorderElement');
+ container.appendChild(recDiv);
+ recorder = recorderManager.displayRecorder(options.token, recDiv.id);
+ recorder.addEventListener('recordingStarted', self.onstart);
+ recorder.addEventListener('recordingStopped', self.onstop);
+ recorder.addEventListener('archiveSaved', onArchiveSaved);
+
+ if (options.style != null) {
+ var style = {};
+ if (options.style == 'blank') {
+ style = {
+ buttonDisplayMode: 'off',
+ showControlBar: false,
+ showMicButton: false,
+ showRecordButton: false,
+ showRecordCounter: false,
+ showRecordStopButton: false,
+ showReRecordButton: false,
+ showPlayButton: false,
+ showPlayCounter: false,
+ showPlayStopButton: false,
+ showSaveButton: false,
+ showSettingsButton: false
+ };
+ }
+ recorder.setStyle(style);
+ }
+
+ if (_startRequest) {
+ setTimeout(function() {
+ _initialized = true;
+ start();
+ }, options.loadingDuration); // i have no idea how to handle ready event
+ } else {
+ _initialized = true;
+ }
+ self.onready();
+
+ }
+ }
+
+ function onArchiveSaved(e) {
+ self.onsavecomplete();
+
+ if (options.remoteArchive != null) {
+ var params = {},
+ url;
+
+ if (typeof options.remoteArchive == 'function') {
+ params.url = options.remoteArchive(e);
+ } else if (typeof options.remoteArchive == 'string') {
+ params.url = options.remoteArchive;
+ } else if (typeof options.remoteArchive == 'object') {
+ params = options.remoteArchive;
+ }
+
+ getVideoURL(params);
+
+ } else {
+ self.ondataavailable({
+ data: e,
+ dataType: 'raw'
+ });
+ }
+
+ }
+
+ function getVideoURL(params) {
+ getUrlData(params,
+ function(answer) {
+ if (answer == 'not_ready' && self.pollWhileProcessing) {
+ getVideoURL(params)
+ } else {
+ self.ondataavailable({
+ data: answer,
+ dataType: 'url'
+ });
+ }
+ }
+ );
+ }
+
+ function getUrlData(params, callback) {
+ if (typeof params == 'string') {
+ params = {
+ url: params
+ }
+ }
+ var options = extend(remoteDefaults, params);
+
+ options.success = callback;
+ options.error = self.onerror;
+
+ if (window.jQuery != null) {
+ jQuery.ajax(options);
+ } else {
+ include(baseUrl + 'lib/AjaxRequest/AjaxRequest.js', function() {
+ switch (options.method) {
+ case 'GET':
+ AjaxRequest.get({
+ url: options.url,
+ parameters: options.data,
+ onSuccess: function(obj) {
+ options.success(obj.responseText)
+ },
+ onError: options.error
+ })
+ break;
+ case 'POST':
+ AjaxRequest.post({
+ url: options.url,
+ parameters: options.data,
+ onSuccess: function(obj) {
+ options.success(obj.responseText)
+ },
+ onError: options.error
+ })
+ break;
+ }
+ });
+ }
+ }
+
+ function getBaseUrl() {
+ var scripts = document.head.getElementsByTagName("script");
+ var loc = scripts[scripts.length - 1].src;
+ return loc.substring(0, loc.lastIndexOf('/')) + '/';
+ }
+ // extending user options
+ function extend(o1, o2) {
+ var obj = {};
+ for (var i in o1) {
+ if (o2[i] != null) {
+ if (typeof o2[i] == "object") {
+ obj[i] = extend(o2[i], {});
+ } else {
+ obj[i] = o2[i];
+ }
+ } else {
+ if (typeof o2[i] == "object") {
+ obj[i] = extend(o1[i], {});
+ } else {
+ obj[i] = o1[i];
+ }
+ }
+ }
+ return obj;
+ }
+
+ function include(src, callback) {
+ var scripts = document.getElementsByTagName('script'),
+ runCallback = false;
+
+ for (var i = 0, len = scripts.length; i < len; i++) {
+ if (scripts[i].getAttribute('src') == src) {
+ runCallback = true;
+ if (scripts[i].getAttribute('loading') == 'loading') {
+ runCallback = false;
+ if (typeof callback == 'function') {
+ var onload = scripts[i].onload;
+ scripts[i].onload = function() {
+ callback();
+ if (typeof onload == 'function') {
+ onload();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (runCallback) {
+ if (typeof callback == 'function') {
+ callback();
+ }
+ } else {
+ var js = document.createElement("script");
+
+ js.type = "text/javascript";
+ js.src = src;
+ js.setAttribute('loading', 'loading');
+ if (typeof callback == 'function') {
+ js.onload = function() {
+ callback()
+ js.removeAttribute('loading');
+ }
+ } else {
+ js.onload = function() {
+ js.removeAttribute('loading');
+ }
+ }
+
+ document.body.appendChild(js);
+ }
+ }
+ this.baseUrl = baseUrl;
+ this.onstart = options.onstart;
+ this.onstop = options.onstop;
+ this.onerror = options.onerror;
+ this.onready = options.onready;
+ this.onsavecomplete = options.onsavecomplete;
+ this.start = start;
+ this.stop = stop;
+ this.save = save;
+ this.state = 'inactive';
+ this.ondataavailable = options.ondataavailable;
+ this.getInterface = function() {
+ return recorder
+ };
+}
diff --git a/common/head.js b/common/head.js
new file mode 100644
index 0000000..0af4701
--- /dev/null
+++ b/common/head.js
@@ -0,0 +1,30 @@
+// Last time updated at September 19, 2015
+
+// links:
+// Open-Sourced: https://github.com/streamproc/MediaStreamRecorder
+// https://cdn.WebRTC-Experiment.com/MediaStreamRecorder.js
+// https://www.WebRTC-Experiment.com/MediaStreamRecorder.js
+// npm install msr
+
+// updates?
+/*
+-. this.recorderType = StereoAudioRecorder;
+*/
+
+//------------------------------------
+
+// Browsers Support::
+// Chrome (all versions) [ audio/video separately ]
+// Firefox ( >= 29 ) [ audio/video in single webm/mp4 container or only audio in ogg ]
+// Opera (all versions) [ same as chrome ]
+// Android (Chrome) [ only video ]
+// Android (Opera) [ only video ]
+// Android (Firefox) [ only video ]
+// Microsoft Edge (Only Audio & Gif)
+
+//------------------------------------
+// Muaz Khan - www.MuazKhan.com
+// MIT License - www.WebRTC-Experiment.com/licence
+//------------------------------------
+
+'use strict';
diff --git a/demos/MultiStreamRecorder.html b/demos/MultiStreamRecorder.html
index abaf3e9..03e657f 100644
--- a/demos/MultiStreamRecorder.html
+++ b/demos/MultiStreamRecorder.html
@@ -7,182 +7,239 @@
-->
-
- MultiStreamRecorder.js & MediaStreamRecorder
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ Getting both audio/video blobs in single "ondataavailable" event using MediaStreamRecorder
+
+
+
+
+ ms
+
+
+
+
+
+
+
+
+
+
+
-
-
+ // get blob after specific time interval
+ multiStreamRecorder.start(timeInterval);
+
+ document.querySelector('#stop-recording').disabled = false;
+ document.querySelector('#pause-recording').disabled = false;
+ }, false);
+
+ video.play();
+
+ container.appendChild(video);
+ container.appendChild(document.createElement('hr'));
+ }
+
+ function onMediaError(e) {
+ console.error('media error', e);
+ }
+
+ var container = document.getElementById('container');
+ var index = 1;
+
+ // below function via: http://goo.gl/B3ae8c
+ function bytesToSize(bytes) {
+ var k = 1000;
+ var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
+ if (bytes === 0) return '0 Bytes';
+ var i = parseInt(Math.floor(Math.log(bytes) / Math.log(k)), 10);
+ return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i];
+ }
+
+ // below function via: http://goo.gl/6QNDcI
+ function getTimeLength(milliseconds) {
+ var data = new Date(milliseconds);
+ return data.getUTCHours() + " hours, " + data.getUTCMinutes() + " minutes and " + data.getUTCSeconds() + " second(s)";
+ }
+
+ window.onbeforeunload = function() {
+ document.querySelector('#start-recording').disabled = false;
+ };
+
+
+
// cdn.webrtc-experiment.com/MediaStreamRecorder.js
var mediaConstraints = {
audio: true,
@@ -204,7 +261,9 @@
console.error('media error', e);
}
-
- ← WebRTC Experiments
-
-
+
+ MediaStreamRecorder Demos
+
+