rtcmulticonnection
Version:
RTCMultiConnection is a WebRTC JavaScript wrapper library runs top over RTCPeerConnection API to support all possible peer-to-peer features.
1,649 lines (1,310 loc) • 84.3 kB
JavaScript
'use strict';
// Last time updated: 2017-08-31 4:03:22 AM UTC
// __________________________
// MediaStreamRecorder v1.3.4
// Open-Sourced: https://github.com/streamproc/MediaStreamRecorder
// --------------------------------------------------
// Muaz Khan - www.MuazKhan.com
// MIT License - www.WebRTC-Experiment.com/licence
// --------------------------------------------------
// ______________________
// MediaStreamRecorder.js
function MediaStreamRecorder(mediaStream) {
if (!mediaStream) {
throw 'MediaStream is mandatory.';
}
// void start(optional long timeSlice)
// timestamp to fire "ondataavailable"
this.start = function(timeSlice) {
var Recorder;
if (typeof MediaRecorder !== 'undefined') {
Recorder = MediaRecorderWrapper;
} else if (IsChrome || IsOpera || IsEdge) {
if (this.mimeType.indexOf('video') !== -1) {
Recorder = WhammyRecorder;
} else if (this.mimeType.indexOf('audio') !== -1) {
Recorder = StereoAudioRecorder;
}
}
// video recorder (in GIF format)
if (this.mimeType === 'image/gif') {
Recorder = GifRecorder;
}
// audio/wav is supported only via StereoAudioRecorder
// audio/pcm (int16) is supported only via StereoAudioRecorder
if (this.mimeType === 'audio/wav' || this.mimeType === 'audio/pcm') {
Recorder = StereoAudioRecorder;
}
// 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) {
if (this.disableLogs) return;
console.log('ondataavailable..', blob);
};
this.onstop = function(error) {
console.warn('stopped..', error);
};
this.save = function(file, fileName) {
if (!file) {
if (!mediaRecorder) {
return;
}
ConcatenateBlobs(mediaRecorder.blobs, mediaRecorder.blobs[0].type, function(concatenatedBlob) {
invokeSaveAsDialog(concatenatedBlob);
});
return;
}
invokeSaveAsDialog(file, fileName);
};
this.pause = function() {
if (!mediaRecorder) {
return;
}
mediaRecorder.pause();
if (this.disableLogs) return;
console.log('Paused recording.', this.mimeType || mediaRecorder.mimeType);
};
this.resume = function() {
if (!mediaRecorder) {
return;
}
mediaRecorder.resume();
if (this.disableLogs) return;
console.log('Resumed recording.', this.mimeType || mediaRecorder.mimeType);
};
// StereoAudioRecorder || WhammyRecorder || MediaRecorderWrapper || GifRecorder
this.recorderType = null;
// video/webm or audio/webm or audio/ogg or audio/wav
this.mimeType = 'video/webm';
// logs are enabled by default
this.disableLogs = false;
// Reference to "MediaRecorder.js"
var mediaRecorder;
}
// ______________________
// MultiStreamRecorder.js
function MultiStreamRecorder(arrayOfMediaStreams, options) {
arrayOfMediaStreams = arrayOfMediaStreams || [];
if (arrayOfMediaStreams instanceof MediaStream) {
arrayOfMediaStreams = [arrayOfMediaStreams];
}
var self = this;
var mixer;
var mediaRecorder;
options = options || {
mimeType: 'video/webm',
video: {
width: 360,
height: 240
}
};
if (!options.frameInterval) {
options.frameInterval = 10;
}
if (!options.video) {
options.video = {};
}
if (!options.video.width) {
options.video.width = 360;
}
if (!options.video.height) {
options.video.height = 240;
}
this.start = function(timeSlice) {
// github/muaz-khan/MultiStreamsMixer
mixer = new MultiStreamsMixer(arrayOfMediaStreams);
if (getVideoTracks().length) {
mixer.frameInterval = options.frameInterval || 10;
mixer.width = options.video.width || 360;
mixer.height = options.video.height || 240;
mixer.startDrawingFrames();
}
if (typeof self.previewStream === 'function') {
self.previewStream(mixer.getMixedStream());
}
// record using MediaRecorder API
mediaRecorder = new MediaStreamRecorder(mixer.getMixedStream());
for (var prop in self) {
if (typeof self[prop] !== 'function') {
mediaRecorder[prop] = self[prop];
}
}
mediaRecorder.ondataavailable = function(blob) {
self.ondataavailable(blob);
};
mediaRecorder.onstop = self.onstop;
mediaRecorder.start(timeSlice);
};
function getVideoTracks() {
var tracks = [];
arrayOfMediaStreams.forEach(function(stream) {
stream.getVideoTracks().forEach(function(track) {
tracks.push(track);
});
});
return tracks;
}
this.stop = function(callback) {
if (!mediaRecorder) {
return;
}
mediaRecorder.stop(function(blob) {
callback(blob);
});
};
this.pause = function() {
if (mediaRecorder) {
mediaRecorder.pause();
}
};
this.resume = function() {
if (mediaRecorder) {
mediaRecorder.resume();
}
};
this.clearRecordedData = function() {
if (mediaRecorder) {
mediaRecorder.clearRecordedData();
mediaRecorder = null;
}
if (mixer) {
mixer.releaseStreams();
mixer = null;
}
};
this.addStreams = this.addStream = function(streams) {
if (!streams) {
throw 'First parameter is required.';
}
if (!(streams instanceof Array)) {
streams = [streams];
}
arrayOfMediaStreams.concat(streams);
if (!mediaRecorder || !mixer) {
return;
}
mixer.appendStreams(streams);
};
this.resetVideoStreams = function(streams) {
if (!mixer) {
return;
}
if (streams && !(streams instanceof Array)) {
streams = [streams];
}
mixer.resetVideoStreams(streams);
};
this.ondataavailable = function(blob) {
if (self.disableLogs) {
return;
}
console.log('ondataavailable', blob);
};
this.onstop = function() {};
// for debugging
this.name = 'MultiStreamRecorder';
this.toString = function() {
return this.name;
};
}
if (typeof MediaStreamRecorder !== 'undefined') {
MediaStreamRecorder.MultiStreamRecorder = MultiStreamRecorder;
}
// Last time updated: 2017-08-31 2:56:12 AM UTC
// ________________________
// MultiStreamsMixer v1.0.2
// Open-Sourced: https://github.com/muaz-khan/MultiStreamsMixer
// --------------------------------------------------
// Muaz Khan - www.MuazKhan.com
// MIT License - www.WebRTC-Experiment.com/licence
// --------------------------------------------------
function MultiStreamsMixer(arrayOfMediaStreams) {
// requires: chrome://flags/#enable-experimental-web-platform-features
var videos = [];
var isStopDrawingFrames = false;
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
canvas.style = 'opacity:0;position:absolute;z-index:-1;top: -100000000;left:-1000000000; margin-top:-1000000000;margin-left:-1000000000;';
(document.body || document.documentElement).appendChild(canvas);
this.disableLogs = false;
this.frameInterval = 10;
this.width = 360;
this.height = 240;
// use gain node to prevent echo
this.useGainNode = true;
var self = this;
// _____________________________
// Cross-Browser-Declarations.js
// WebAudio API representer
var AudioContext = window.AudioContext;
if (typeof AudioContext === 'undefined') {
if (typeof webkitAudioContext !== 'undefined') {
/*global AudioContext:true */
AudioContext = webkitAudioContext;
}
if (typeof mozAudioContext !== 'undefined') {
/*global AudioContext:true */
AudioContext = mozAudioContext;
}
}
/*jshint -W079 */
var URL = window.URL;
if (typeof URL === 'undefined' && typeof webkitURL !== 'undefined') {
/*global URL:true */
URL = webkitURL;
}
if (typeof navigator !== 'undefined' && typeof navigator.getUserMedia === 'undefined') { // maybe window.navigator?
if (typeof navigator.webkitGetUserMedia !== 'undefined') {
navigator.getUserMedia = navigator.webkitGetUserMedia;
}
if (typeof navigator.mozGetUserMedia !== 'undefined') {
navigator.getUserMedia = navigator.mozGetUserMedia;
}
}
var MediaStream = window.MediaStream;
if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') {
MediaStream = webkitMediaStream;
}
/*global MediaStream:true */
if (typeof MediaStream !== 'undefined') {
if (!('getVideoTracks' in MediaStream.prototype)) {
MediaStream.prototype.getVideoTracks = function() {
if (!this.getTracks) {
return [];
}
var tracks = [];
this.getTracks.forEach(function(track) {
if (track.kind.toString().indexOf('video') !== -1) {
tracks.push(track);
}
});
return tracks;
};
MediaStream.prototype.getAudioTracks = function() {
if (!this.getTracks) {
return [];
}
var tracks = [];
this.getTracks.forEach(function(track) {
if (track.kind.toString().indexOf('audio') !== -1) {
tracks.push(track);
}
});
return tracks;
};
}
// override "stop" method for all browsers
if (typeof MediaStream.prototype.stop === 'undefined') {
MediaStream.prototype.stop = function() {
this.getTracks().forEach(function(track) {
track.stop();
});
};
}
}
var Storage = {};
if (typeof AudioContext !== 'undefined') {
Storage.AudioContext = AudioContext;
} else if (typeof webkitAudioContext !== 'undefined') {
Storage.AudioContext = webkitAudioContext;
}
this.startDrawingFrames = function() {
drawVideosToCanvas();
};
function drawVideosToCanvas() {
if (isStopDrawingFrames) {
return;
}
var videosLength = videos.length;
var fullcanvas = false;
var remaining = [];
videos.forEach(function(video) {
if (!video.stream) {
video.stream = {};
}
if (video.stream.fullcanvas) {
fullcanvas = video;
} else {
remaining.push(video);
}
});
if (fullcanvas) {
canvas.width = fullcanvas.stream.width;
canvas.height = fullcanvas.stream.height;
} else if (remaining.length) {
canvas.width = videosLength > 1 ? remaining[0].width * 2 : remaining[0].width;
canvas.height = videosLength > 2 ? remaining[0].height * 2 : remaining[0].height;
} else {
canvas.width = self.width || 360;
canvas.height = self.height || 240;
}
if (fullcanvas && fullcanvas instanceof HTMLVideoElement) {
drawImage(fullcanvas);
}
remaining.forEach(function(video, idx) {
drawImage(video, idx);
});
setTimeout(drawVideosToCanvas, self.frameInterval);
}
function drawImage(video, idx) {
if (isStopDrawingFrames) {
return;
}
var x = 0;
var y = 0;
var width = video.width;
var height = video.height;
if (idx === 1) {
x = video.width;
}
if (idx === 2) {
y = video.height;
}
if (idx === 3) {
x = video.width;
y = video.height;
}
if (typeof video.stream.left !== 'undefined') {
x = video.stream.left;
}
if (typeof video.stream.top !== 'undefined') {
y = video.stream.top;
}
if (typeof video.stream.width !== 'undefined') {
width = video.stream.width;
}
if (typeof video.stream.height !== 'undefined') {
height = video.stream.height;
}
context.drawImage(video, x, y, width, height);
if (typeof video.stream.onRender === 'function') {
video.stream.onRender(context, x, y, width, height, idx);
}
}
function getMixedStream() {
isStopDrawingFrames = false;
var mixedVideoStream = getMixedVideoStream();
var mixedAudioStream = getMixedAudioStream();
if (mixedAudioStream) {
mixedAudioStream.getAudioTracks().forEach(function(track) {
mixedVideoStream.addTrack(track);
});
}
var fullcanvas;
arrayOfMediaStreams.forEach(function(stream) {
if (stream.fullcanvas) {
fullcanvas = true;
}
});
return mixedVideoStream;
}
function getMixedVideoStream() {
resetVideoStreams();
var capturedStream;
if ('captureStream' in canvas) {
capturedStream = canvas.captureStream();
} else if ('mozCaptureStream' in canvas) {
capturedStream = canvas.mozCaptureStream();
} else if (!self.disableLogs) {
console.error('Upgrade to latest Chrome or otherwise enable this flag: chrome://flags/#enable-experimental-web-platform-features');
}
var videoStream = new MediaStream();
capturedStream.getVideoTracks().forEach(function(track) {
videoStream.addTrack(track);
});
canvas.stream = videoStream;
return videoStream;
}
function getMixedAudioStream() {
// via: @pehrsons
if (!Storage.AudioContextConstructor) {
Storage.AudioContextConstructor = new Storage.AudioContext();
}
self.audioContext = Storage.AudioContextConstructor;
self.audioSources = [];
if (self.useGainNode === true) {
self.gainNode = self.audioContext.createGain();
self.gainNode.connect(self.audioContext.destination);
self.gainNode.gain.value = 0; // don't hear self
}
var audioTracksLength = 0;
arrayOfMediaStreams.forEach(function(stream) {
if (!stream.getAudioTracks().length) {
return;
}
audioTracksLength++;
var audioSource = self.audioContext.createMediaStreamSource(stream);
if (self.useGainNode === true) {
audioSource.connect(self.gainNode);
}
self.audioSources.push(audioSource);
});
if (!audioTracksLength) {
return;
}
self.audioDestination = self.audioContext.createMediaStreamDestination();
self.audioSources.forEach(function(audioSource) {
audioSource.connect(self.audioDestination);
});
return self.audioDestination.stream;
}
function getVideo(stream) {
var video = document.createElement('video');
if ('srcObject' in video) {
video.srcObject = stream;
} else {
video.src = URL.createObjectURL(stream);
}
video.muted = true;
video.volume = 0;
video.width = stream.width || self.width || 360;
video.height = stream.height || self.height || 240;
video.play();
return video;
}
this.appendStreams = function(streams) {
if (!streams) {
throw 'First parameter is required.';
}
if (!(streams instanceof Array)) {
streams = [streams];
}
arrayOfMediaStreams.concat(streams);
streams.forEach(function(stream) {
if (stream.getVideoTracks().length) {
var video = getVideo(stream);
video.stream = stream;
videos.push(video);
}
if (stream.getAudioTracks().length && self.audioContext) {
var audioSource = self.audioContext.createMediaStreamSource(stream);
audioSource.connect(self.audioDestination);
self.audioSources.push(audioSource);
}
});
};
this.releaseStreams = function() {
videos = [];
isStopDrawingFrames = true;
if (self.gainNode) {
self.gainNode.disconnect();
self.gainNode = null;
}
if (self.audioSources.length) {
self.audioSources.forEach(function(source) {
source.disconnect();
});
self.audioSources = [];
}
if (self.audioDestination) {
self.audioDestination.disconnect();
self.audioDestination = null;
}
self.audioContext = null;
context.clearRect(0, 0, canvas.width, canvas.height);
if (canvas.stream) {
canvas.stream.stop();
canvas.stream = null;
}
};
this.resetVideoStreams = function(streams) {
if (streams && !(streams instanceof Array)) {
streams = [streams];
}
resetVideoStreams(streams);
};
function resetVideoStreams(streams) {
videos = [];
streams = streams || arrayOfMediaStreams;
// via: @adrian-ber
streams.forEach(function(stream) {
if (!stream.getVideoTracks().length) {
return;
}
var video = getVideo(stream);
video.stream = stream;
videos.push(video);
});
}
// for debugging
this.name = 'MultiStreamsMixer';
this.toString = function() {
return this.name;
};
this.getMixedStream = getMixedStream;
}
// _____________________________
// Cross-Browser-Declarations.js
var browserFakeUserAgent = 'Fake/5.0 (FakeOS) AppleWebKit/123 (KHTML, like Gecko) Fake/12.3.4567.89 Fake/123.45';
(function(that) {
if (typeof window !== 'undefined') {
return;
}
if (typeof window === 'undefined' && typeof global !== 'undefined') {
global.navigator = {
userAgent: browserFakeUserAgent,
getUserMedia: function() {}
};
/*global window:true */
that.window = global;
} else if (typeof window === 'undefined') {
// window = this;
}
if (typeof document === 'undefined') {
/*global document:true */
that.document = {};
document.createElement = document.captureStream = document.mozCaptureStream = function() {
return {};
};
}
if (typeof location === 'undefined') {
/*global location:true */
that.location = {
protocol: 'file:',
href: '',
hash: ''
};
}
if (typeof screen === 'undefined') {
/*global screen:true */
that.screen = {
width: 0,
height: 0
};
}
})(typeof global !== 'undefined' ? global : window);
// WebAudio API representer
var AudioContext = window.AudioContext;
if (typeof AudioContext === 'undefined') {
if (typeof webkitAudioContext !== 'undefined') {
/*global AudioContext:true */
AudioContext = webkitAudioContext;
}
if (typeof mozAudioContext !== 'undefined') {
/*global AudioContext:true */
AudioContext = mozAudioContext;
}
}
if (typeof window === 'undefined') {
/*jshint -W020 */
window = {};
}
// WebAudio API representer
var AudioContext = window.AudioContext;
if (typeof AudioContext === 'undefined') {
if (typeof webkitAudioContext !== 'undefined') {
/*global AudioContext:true */
AudioContext = webkitAudioContext;
}
if (typeof mozAudioContext !== 'undefined') {
/*global AudioContext:true */
AudioContext = mozAudioContext;
}
}
/*jshint -W079 */
var URL = window.URL;
if (typeof URL === 'undefined' && typeof webkitURL !== 'undefined') {
/*global URL:true */
URL = webkitURL;
}
if (typeof navigator !== 'undefined') {
if (typeof navigator.webkitGetUserMedia !== 'undefined') {
navigator.getUserMedia = navigator.webkitGetUserMedia;
}
if (typeof navigator.mozGetUserMedia !== 'undefined') {
navigator.getUserMedia = navigator.mozGetUserMedia;
}
} else {
navigator = {
getUserMedia: function() {},
userAgent: browserFakeUserAgent
};
}
var IsEdge = navigator.userAgent.indexOf('Edge') !== -1 && (!!navigator.msSaveBlob || !!navigator.msSaveOrOpenBlob);
var IsOpera = false;
if (typeof opera !== 'undefined' && navigator.userAgent && navigator.userAgent.indexOf('OPR/') !== -1) {
IsOpera = true;
}
var IsChrome = !IsEdge && !IsEdge && !!navigator.webkitGetUserMedia;
var MediaStream = window.MediaStream;
if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') {
MediaStream = webkitMediaStream;
}
/*global MediaStream:true */
if (typeof MediaStream !== 'undefined') {
if (!('getVideoTracks' in MediaStream.prototype)) {
MediaStream.prototype.getVideoTracks = function() {
if (!this.getTracks) {
return [];
}
var tracks = [];
this.getTracks.forEach(function(track) {
if (track.kind.toString().indexOf('video') !== -1) {
tracks.push(track);
}
});
return tracks;
};
MediaStream.prototype.getAudioTracks = function() {
if (!this.getTracks) {
return [];
}
var tracks = [];
this.getTracks.forEach(function(track) {
if (track.kind.toString().indexOf('audio') !== -1) {
tracks.push(track);
}
});
return tracks;
};
}
if (!('stop' in MediaStream.prototype)) {
MediaStream.prototype.stop = function() {
this.getAudioTracks().forEach(function(track) {
if (!!track.stop) {
track.stop();
}
});
this.getVideoTracks().forEach(function(track) {
if (!!track.stop) {
track.stop();
}
});
};
}
}
if (typeof location !== 'undefined') {
if (location.href.indexOf('file:') === 0) {
console.error('Please load this HTML file on HTTP or HTTPS.');
}
}
// Merge all other data-types except "function"
function mergeProps(mergein, mergeto) {
for (var t in mergeto) {
if (typeof mergeto[t] !== 'function') {
mergein[t] = mergeto[t];
}
}
return mergein;
}
// "dropFirstFrame" has been added by Graham Roth
// https://github.com/gsroth
function dropFirstFrame(arr) {
arr.shift();
return arr;
}
/**
* @param {Blob} file - File or Blob object. This parameter is required.
* @param {string} fileName - Optional file name e.g. "Recorded-Video.webm"
* @example
* invokeSaveAsDialog(blob or file, [optional] fileName);
* @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
*/
function invokeSaveAsDialog(file, fileName) {
if (!file) {
throw 'Blob object is required.';
}
if (!file.type) {
try {
file.type = 'video/webm';
} catch (e) {}
}
var fileExtension = (file.type || 'video/webm').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'];
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];
}
// ______________ (used to handle stuff like http://goo.gl/xmE5eg) issue #129
// ObjectStore.js
var ObjectStore = {
AudioContext: AudioContext
};
function isMediaRecorderCompatible() {
var isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
var isChrome = !!window.chrome && !isOpera;
var isFirefox = typeof window.InstallTrigger !== 'undefined';
if (isFirefox) {
return true;
}
if (!isChrome) {
return false;
}
var nVer = navigator.appVersion;
var nAgt = navigator.userAgent;
var fullVersion = '' + parseFloat(navigator.appVersion);
var majorVersion = parseInt(navigator.appVersion, 10);
var nameOffset, verOffset, ix;
if (isChrome) {
verOffset = nAgt.indexOf('Chrome');
fullVersion = nAgt.substring(verOffset + 7);
}
// trim the fullVersion string at semicolon/space if present
if ((ix = fullVersion.indexOf(';')) !== -1) {
fullVersion = fullVersion.substring(0, ix);
}
if ((ix = fullVersion.indexOf(' ')) !== -1) {
fullVersion = fullVersion.substring(0, ix);
}
majorVersion = parseInt('' + fullVersion, 10);
if (isNaN(majorVersion)) {
fullVersion = '' + parseFloat(navigator.appVersion);
majorVersion = parseInt(navigator.appVersion, 10);
}
return majorVersion >= 49;
}
// ==================
// MediaRecorder.js
/**
* Implementation of https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-capture/MediaRecorder.html
* The MediaRecorder accepts a mediaStream as input source passed from UA. When recorder starts,
* a MediaEncoder will be created and accept the mediaStream as input source.
* Encoder will get the raw data by track data changes, encode it by selected MIME Type, then store the encoded in EncodedBufferCache object.
* The encoded data will be extracted on every timeslice passed from Start function call or by RequestData function.
* Thread model:
* When the recorder starts, it creates a "Media Encoder" thread to read data from MediaEncoder object and store buffer in EncodedBufferCache object.
* Also extract the encoded data and create blobs on every timeslice passed from start function or RequestData function called by UA.
*/
function MediaRecorderWrapper(mediaStream) {
var self = this;
/**
* This method records MediaStream.
* @method
* @memberof MediaStreamRecorder
* @example
* recorder.start(5000);
*/
this.start = function(timeSlice, __disableLogs) {
this.timeSlice = timeSlice || 5000;
if (!self.mimeType) {
self.mimeType = 'video/webm';
}
if (self.mimeType.indexOf('audio') !== -1) {
if (mediaStream.getVideoTracks().length && mediaStream.getAudioTracks().length) {
var stream;
if (!!navigator.mozGetUserMedia) {
stream = new MediaStream();
stream.addTrack(mediaStream.getAudioTracks()[0]);
} else {
// webkitMediaStream
stream = new MediaStream(mediaStream.getAudioTracks());
}
mediaStream = stream;
}
}
if (self.mimeType.indexOf('audio') !== -1) {
self.mimeType = IsChrome ? 'audio/webm' : 'audio/ogg';
}
self.dontFireOnDataAvailableEvent = false;
var recorderHints = {
mimeType: self.mimeType
};
if (!self.disableLogs && !__disableLogs) {
console.log('Passing following params over MediaRecorder API.', recorderHints);
}
if (mediaRecorder) {
// mandatory to make sure Firefox doesn't fails to record streams 3-4 times without reloading the page.
mediaRecorder = null;
}
if (IsChrome && !isMediaRecorderCompatible()) {
// to support video-only recording on stable
recorderHints = 'video/vp8';
}
// http://dxr.mozilla.org/mozilla-central/source/content/media/MediaRecorder.cpp
// https://wiki.mozilla.org/Gecko:MediaRecorder
// https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-capture/MediaRecorder.html
// starting a recording session; which will initiate "Reading Thread"
// "Reading Thread" are used to prevent main-thread blocking scenarios
try {
mediaRecorder = new MediaRecorder(mediaStream, recorderHints);
} catch (e) {
// if someone passed NON_supported mimeType
// or if Firefox on Android
mediaRecorder = new MediaRecorder(mediaStream);
}
if ('canRecordMimeType' in mediaRecorder && mediaRecorder.canRecordMimeType(self.mimeType) === false) {
if (!self.disableLogs) {
console.warn('MediaRecorder API seems unable to record mimeType:', self.mimeType);
}
}
// i.e. stop recording when <video> is paused by the user; and auto restart recording
// when video is resumed. E.g. yourStream.getVideoTracks()[0].muted = true; // it will auto-stop recording.
if (self.ignoreMutedMedia === true) {
mediaRecorder.ignoreMutedMedia = true;
}
var firedOnDataAvailableOnce = false;
// Dispatching OnDataAvailable Handler
mediaRecorder.ondataavailable = function(e) {
// how to fix FF-corrupt-webm issues?
// should we leave this? e.data.size < 26800
if (!e.data || !e.data.size || e.data.size < 26800 || firedOnDataAvailableOnce) {
return;
}
firedOnDataAvailableOnce = true;
var blob = self.getNativeBlob ? e.data : new Blob([e.data], {
type: self.mimeType || 'video/webm'
});
self.ondataavailable(blob);
// self.dontFireOnDataAvailableEvent = true;
if (!!mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
}
mediaRecorder = null;
if (self.dontFireOnDataAvailableEvent) {
return;
}
// record next interval
self.start(timeSlice, '__disableLogs');
};
mediaRecorder.onerror = function(error) {
if (!self.disableLogs) {
if (error.name === 'InvalidState') {
console.error('The MediaRecorder is not in a state in which the proposed operation is allowed to be executed.');
} else if (error.name === 'OutOfMemory') {
console.error('The UA has exhaused the available memory. User agents SHOULD provide as much additional information as possible in the message attribute.');
} else if (error.name === 'IllegalStreamModification') {
console.error('A modification to the stream has occurred that makes it impossible to continue recording. An example would be the addition of a Track while recording is occurring. User agents SHOULD provide as much additional information as possible in the message attribute.');
} else if (error.name === 'OtherRecordingError') {
console.error('Used for an fatal error other than those listed above. User agents SHOULD provide as much additional information as possible in the message attribute.');
} else if (error.name === 'GenericError') {
console.error('The UA cannot provide the codec or recording option that has been requested.', error);
} else {
console.error('MediaRecorder Error', error);
}
}
// When the stream is "ended" set recording to 'inactive'
// and stop gathering data. Callers should not rely on
// exactness of the timeSlice value, especially
// if the timeSlice value is small. Callers should
// consider timeSlice as a minimum value
if (!!mediaRecorder && mediaRecorder.state !== 'inactive' && mediaRecorder.state !== 'stopped') {
mediaRecorder.stop();
}
};
// void start(optional long mTimeSlice)
// The interval of passing encoded data from EncodedBufferCache to onDataAvailable
// handler. "mTimeSlice < 0" means Session object does not push encoded data to
// onDataAvailable, instead, it passive wait the client side pull encoded data
// by calling requestData API.
try {
mediaRecorder.start(3.6e+6);
} catch (e) {
mediaRecorder = null;
}
setTimeout(function() {
if (!mediaRecorder) {
return;
}
if (mediaRecorder.state === 'recording') {
// "stop" method auto invokes "requestData"!
mediaRecorder.requestData();
// mediaRecorder.stop();
}
}, timeSlice);
// Start recording. If timeSlice has been provided, mediaRecorder will
// raise a dataavailable event containing the Blob of collected data on every timeSlice milliseconds.
// If timeSlice isn't provided, UA should call the RequestData to obtain the Blob data, also set the mTimeSlice to zero.
};
/**
* This method stops recording MediaStream.
* @param {function} callback - Callback function, that is used to pass recorded blob back to the callee.
* @method
* @memberof MediaStreamRecorder
* @example
* recorder.stop(function(blob) {
* video.src = URL.createObjectURL(blob);
* });
*/
this.stop = function(callback) {
if (!mediaRecorder) {
return;
}
// mediaRecorder.state === 'recording' means that media recorder is associated with "session"
// mediaRecorder.state === 'stopped' means that media recorder is detached from the "session" ... in this case; "session" will also be deleted.
if (mediaRecorder.state === 'recording') {
// "stop" method auto invokes "requestData"!
mediaRecorder.requestData();
setTimeout(function() {
self.dontFireOnDataAvailableEvent = true;
if (!!mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
}
mediaRecorder = null;
self.onstop();
}, 2000);
}
};
/**
* This method pauses the recording process.
* @method
* @memberof MediaStreamRecorder
* @example
* recorder.pause();
*/
this.pause = function() {
if (!mediaRecorder) {
return;
}
if (mediaRecorder.state === 'recording') {
mediaRecorder.pause();
}
this.dontFireOnDataAvailableEvent = true;
};
/**
* The recorded blobs are passed over this event.
* @event
* @memberof MediaStreamRecorder
* @example
* recorder.ondataavailable = function(data) {};
*/
this.ondataavailable = function(blob) {
console.log('recorded-blob', blob);
};
/**
* This method resumes the recording process.
* @method
* @memberof MediaStreamRecorder
* @example
* recorder.resume();
*/
this.resume = function() {
if (this.dontFireOnDataAvailableEvent) {
this.dontFireOnDataAvailableEvent = false;
var disableLogs = self.disableLogs;
self.disableLogs = true;
this.start(this.timeslice || 5000);
self.disableLogs = disableLogs;
return;
}
if (!mediaRecorder) {
return;
}
if (mediaRecorder.state === 'paused') {
mediaRecorder.resume();
}
};
/**
* This method resets currently recorded data.
* @method
* @memberof MediaStreamRecorder
* @example
* recorder.clearRecordedData();
*/
this.clearRecordedData = function() {
if (!mediaRecorder) {
return;
}
this.pause();
this.dontFireOnDataAvailableEvent = true;
this.stop();
};
this.onstop = function() {};
// Reference to "MediaRecorder" object
var mediaRecorder;
function isMediaStreamActive() {
if ('active' in mediaStream) {
if (!mediaStream.active) {
return false;
}
} else if ('ended' in mediaStream) { // old hack
if (mediaStream.ended) {
return false;
}
}
return true;
}
// this method checks if media stream is stopped
// or any track is ended.
(function looper() {
if (!mediaRecorder) {
return;
}
if (isMediaStreamActive() === false) {
self.stop();
return;
}
setTimeout(looper, 1000); // check every second
})();
}
if (typeof MediaStreamRecorder !== 'undefined') {
MediaStreamRecorder.MediaRecorderWrapper = MediaRecorderWrapper;
}
// ======================
// StereoAudioRecorder.js
function StereoAudioRecorder(mediaStream) {
// void start(optional long timeSlice)
// timestamp to fire "ondataavailable"
this.start = function(timeSlice) {
timeSlice = timeSlice || 1000;
mediaRecorder = new StereoAudioRecorderHelper(mediaStream, this);
mediaRecorder.record();
timeout = setInterval(function() {
mediaRecorder.requestData();
}, timeSlice);
};
this.stop = function() {
if (mediaRecorder) {
mediaRecorder.stop();
clearTimeout(timeout);
this.onstop();
}
};
this.pause = function() {
if (!mediaRecorder) {
return;
}
mediaRecorder.pause();
};
this.resume = function() {
if (!mediaRecorder) {
return;
}
mediaRecorder.resume();
};
this.ondataavailable = function() {};
this.onstop = function() {};
// Reference to "StereoAudioRecorder" object
var mediaRecorder;
var timeout;
}
if (typeof MediaStreamRecorder !== 'undefined') {
MediaStreamRecorder.StereoAudioRecorder = StereoAudioRecorder;
}
// ============================
// StereoAudioRecorderHelper.js
// source code from: http://typedarray.org/wp-content/projects/WebAudioRecorder/script.js
function StereoAudioRecorderHelper(mediaStream, root) {
// variables
var deviceSampleRate = 44100; // range: 22050 to 96000
if (!ObjectStore.AudioContextConstructor) {
ObjectStore.AudioContextConstructor = new ObjectStore.AudioContext();
}
// check device sample rate
deviceSampleRate = ObjectStore.AudioContextConstructor.sampleRate;
var leftchannel = [];
var rightchannel = [];
var scriptprocessornode;
var recording = false;
var recordingLength = 0;
var volume;
var audioInput;
var sampleRate = root.sampleRate || deviceSampleRate;
var mimeType = root.mimeType || 'audio/wav';
var isPCM = mimeType.indexOf('audio/pcm') > -1;
var context;
var numChannels = root.audioChannels || 2;
this.record = function() {
recording = true;
// reset the buffers for the new recording
leftchannel.length = rightchannel.length = 0;
recordingLength = 0;
};
this.requestData = function() {
if (isPaused) {
return;
}
if (recordingLength === 0) {
requestDataInvoked = false;
return;
}
requestDataInvoked = true;
// clone stuff
var internalLeftChannel = leftchannel.slice(0);
var internalRightChannel = rightchannel.slice(0);
var internalRecordingLength = recordingLength;
// reset the buffers for the new recording
leftchannel.length = rightchannel.length = [];
recordingLength = 0;
requestDataInvoked = false;
// we flat the left and right channels down
var leftBuffer = mergeBuffers(internalLeftChannel, internalRecordingLength);
var interleaved = leftBuffer;
// we interleave both channels together
if (numChannels === 2) {
var rightBuffer = mergeBuffers(internalRightChannel, internalRecordingLength); // bug fixed via #70,#71
interleaved = interleave(leftBuffer, rightBuffer);
}
if (isPCM) {
// our final binary blob
var blob = new Blob([convertoFloat32ToInt16(interleaved)], {
type: 'audio/pcm'
});
console.debug('audio recorded blob size:', bytesToSize(blob.size));
root.ondataavailable(blob);
return;
}
// we create our wav file
var buffer = new ArrayBuffer(44 + interleaved.length * 2);
var view = new DataView(buffer);
// RIFF chunk descriptor
writeUTFBytes(view, 0, 'RIFF');
// -8 (via #97)
view.setUint32(4, 44 + interleaved.length * 2 - 8, true);
writeUTFBytes(view, 8, 'WAVE');
// FMT sub-chunk
writeUTFBytes(view, 12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
// stereo (2 channels)
view.setUint16(22, numChannels, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * numChannels * 2, true); // numChannels * 2 (via #71)
view.setUint16(32, numChannels * 2, true);
view.setUint16(34, 16, true);
// data sub-chunk
writeUTFBytes(view, 36, 'data');
view.setUint32(40, interleaved.length * 2, true);
// write the PCM samples
var lng = interleaved.length;
var index = 44;
var volume = 1;
for (var i = 0; i < lng; i++) {
view.setInt16(index, interleaved[i] * (0x7FFF * volume), true);
index += 2;
}
// our final binary blob
var blob = new Blob([view], {
type: 'audio/wav'
});
console.debug('audio recorded blob size:', bytesToSize(blob.size));
root.ondataavailable(blob);
};
this.stop = function() {
// we stop recording
recording = false;
this.requestData();
audioInput.disconnect();
this.onstop();
};
function interleave(leftChannel, rightChannel) {
var length = leftChannel.length + rightChannel.length;
var result = new Float32Array(length);
var inputIndex = 0;
for (var index = 0; index < length;) {
result[index++] = leftChannel[inputIndex];
result[index++] = rightChannel[inputIndex];
inputIndex++;
}
return result;
}
function mergeBuffers(channelBuffer, recordingLength) {
var result = new Float32Array(recordingLength);
var offset = 0;
var lng = channelBuffer.length;
for (var i = 0; i < lng; i++) {
var buffer = channelBuffer[i];
result.set(buffer, offset);
offset += buffer.length;
}
return result;
}
function writeUTFBytes(view, offset, string) {
var lng = string.length;
for (var i = 0; i < lng; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
function convertoFloat32ToInt16(buffer) {
var l = buffer.length;
var buf = new Int16Array(l)
while (l--) {
buf[l] = buffer[l] * 0xFFFF; //convert to 16 bit
}
return buf.buffer
}
// creates the audio context
var context = ObjectStore.AudioContextConstructor;
// creates a gain node
ObjectStore.VolumeGainNode = context.createGain();
var volume = ObjectStore.VolumeGainNode;
// creates an audio node from the microphone incoming stream
ObjectStore.AudioInput = context.createMediaStreamSource(mediaStream);
// creates an audio node from the microphone incoming stream
var audioInput = ObjectStore.AudioInput;
// connect the stream to the gain node
audioInput.connect(volume);
/* From the spec: This value controls how frequently the audioprocess event is
dispatched and how many sample-frames need to be processed each call.
Lower values for buffer size will result in a lower (better) latency.
Higher values will be necessary to avoid audio breakup and glitches
Legal values are 256, 512, 1024, 2048, 4096, 8192, and 16384.*/
var bufferSize = root.bufferSize || 2048;
if (root.bufferSize === 0) {
bufferSize = 0;
}
if (context.createJavaScriptNode) {
scriptprocessornode = context.createJavaScriptNode(bufferSize, numChannels, numChannels);
} else if (context.createScriptProcessor) {
scriptprocessornode = context.createScriptProcessor(bufferSize, numChannels, numChannels);
} else {
throw 'WebAudio API has no support on this browser.';
}
bufferSize = scriptprocessornode.bufferSize;
console.debug('using audio buffer-size:', bufferSize);
var requestDataInvoked = false;
// sometimes "scriptprocessornode" disconnects from he destination-node
// and there is no exception thrown in this case.
// and obviously no further "ondataavailable" events will be emitted.
// below global-scope variable is added to debug such unexp