UNPKG

nile.js

Version:

A tool for scalable peer-to-peer video streaming using WebTorrent

1,581 lines (1,300 loc) 464 kB
(function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define("Broadcaster", [], factory); else if(typeof exports === 'object') exports["Broadcaster"] = factory(); else root["Broadcaster"] = factory(); })(this, function() { return /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // identity function for calling harmony imports with the correct context /******/ __webpack_require__.i = function(value) { return value; }; /******/ /******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { /******/ configurable: false, /******/ enumerable: true, /******/ get: getter /******/ }); /******/ } /******/ }; /******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __webpack_require__.n = function(module) { /******/ var getter = module && module.__esModule ? /******/ function getDefault() { return module['default']; } : /******/ function getModuleExports() { return module; }; /******/ __webpack_require__.d(getter, 'a', getter); /******/ return getter; /******/ }; /******/ /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = "dist"; /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 33); /******/ }) /************************************************************************/ /******/ ({ /***/ 0: /***/ (function(module, exports, __webpack_require__) { "use strict"; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var g; // This works in non-strict mode g = function () { return this; }(); try { // This works if eval is allowed (see CSP) g = g || Function("return this")() || (1, eval)("this"); } catch (e) { // This works if the window reference is available if ((typeof window === "undefined" ? "undefined" : _typeof(window)) === "object") g = window; } // g can still be undefined, but nothing to do about it... // We return undefined, instead of nothing here, so it's // easier to handle this case. if(!global) { ...} module.exports = g; /***/ }), /***/ 12: /***/ (function(module, exports, __webpack_require__) { "use strict"; /* WEBPACK VAR INJECTION */(function(global, process) { (function (global, undefined) { "use strict"; if (global.setImmediate) { return; } var nextHandle = 1; // Spec says greater than zero var tasksByHandle = {}; var currentlyRunningATask = false; var doc = global.document; var registerImmediate; function setImmediate(callback) { // Callback can either be a function or a string if (typeof callback !== "function") { callback = new Function("" + callback); } // Copy function arguments var args = new Array(arguments.length - 1); for (var i = 0; i < args.length; i++) { args[i] = arguments[i + 1]; } // Store and register the task var task = { callback: callback, args: args }; tasksByHandle[nextHandle] = task; registerImmediate(nextHandle); return nextHandle++; } function clearImmediate(handle) { delete tasksByHandle[handle]; } function run(task) { var callback = task.callback; var args = task.args; switch (args.length) { case 0: callback(); break; case 1: callback(args[0]); break; case 2: callback(args[0], args[1]); break; case 3: callback(args[0], args[1], args[2]); break; default: callback.apply(undefined, args); break; } } function runIfPresent(handle) { // From the spec: "Wait until any invocations of this algorithm started before this one have completed." // So if we're currently running a task, we'll need to delay this invocation. if (currentlyRunningATask) { // Delay by doing a setTimeout. setImmediate was tried instead, but in Firefox 7 it generated a // "too much recursion" error. setTimeout(runIfPresent, 0, handle); } else { var task = tasksByHandle[handle]; if (task) { currentlyRunningATask = true; try { run(task); } finally { clearImmediate(handle); currentlyRunningATask = false; } } } } function installNextTickImplementation() { registerImmediate = function registerImmediate(handle) { process.nextTick(function () { runIfPresent(handle); }); }; } function canUsePostMessage() { // The test against `importScripts` prevents this implementation from being installed inside a web worker, // where `global.postMessage` means something completely different and can't be used for this purpose. if (global.postMessage && !global.importScripts) { var postMessageIsAsynchronous = true; var oldOnMessage = global.onmessage; global.onmessage = function () { postMessageIsAsynchronous = false; }; global.postMessage("", "*"); global.onmessage = oldOnMessage; return postMessageIsAsynchronous; } } function installPostMessageImplementation() { // Installs an event handler on `global` for the `message` event: see // * https://developer.mozilla.org/en/DOM/window.postMessage // * http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html#crossDocumentMessages var messagePrefix = "setImmediate$" + Math.random() + "$"; var onGlobalMessage = function onGlobalMessage(event) { if (event.source === global && typeof event.data === "string" && event.data.indexOf(messagePrefix) === 0) { runIfPresent(+event.data.slice(messagePrefix.length)); } }; if (global.addEventListener) { global.addEventListener("message", onGlobalMessage, false); } else { global.attachEvent("onmessage", onGlobalMessage); } registerImmediate = function registerImmediate(handle) { global.postMessage(messagePrefix + handle, "*"); }; } function installMessageChannelImplementation() { var channel = new MessageChannel(); channel.port1.onmessage = function (event) { var handle = event.data; runIfPresent(handle); }; registerImmediate = function registerImmediate(handle) { channel.port2.postMessage(handle); }; } function installReadyStateChangeImplementation() { var html = doc.documentElement; registerImmediate = function registerImmediate(handle) { // Create a <script> element; its readystatechange event will be fired asynchronously once it is inserted // into the document. Do so, thus queuing up the task. Remember to clean up once it's been called. var script = doc.createElement("script"); script.onreadystatechange = function () { runIfPresent(handle); script.onreadystatechange = null; html.removeChild(script); script = null; }; html.appendChild(script); }; } function installSetTimeoutImplementation() { registerImmediate = function registerImmediate(handle) { setTimeout(runIfPresent, 0, handle); }; } // If supported, we should attach to the prototype of global, since that is where setTimeout et al. live. var attachTo = Object.getPrototypeOf && Object.getPrototypeOf(global); attachTo = attachTo && attachTo.setTimeout ? attachTo : global; // Don't get fooled by e.g. browserify environments. if ({}.toString.call(global.process) === "[object process]") { // For Node.js before 0.9 installNextTickImplementation(); } else if (canUsePostMessage()) { // For non-IE10 modern browsers installPostMessageImplementation(); } else if (global.MessageChannel) { // For web workers, where supported installMessageChannelImplementation(); } else if (doc && "onreadystatechange" in doc.createElement("script")) { // For IE 6–8 installReadyStateChangeImplementation(); } else { // For older browsers installSetTimeoutImplementation(); } attachTo.setImmediate = setImmediate; attachTo.clearImmediate = clearImmediate; })(typeof self === "undefined" ? typeof global === "undefined" ? undefined : global : self); /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(0), __webpack_require__(4))) /***/ }), /***/ 14: /***/ (function(module, exports, __webpack_require__) { "use strict"; var apply = Function.prototype.apply; // DOM APIs, for completeness exports.setTimeout = function () { return new Timeout(apply.call(setTimeout, window, arguments), clearTimeout); }; exports.setInterval = function () { return new Timeout(apply.call(setInterval, window, arguments), clearInterval); }; exports.clearTimeout = exports.clearInterval = function (timeout) { if (timeout) { timeout.close(); } }; function Timeout(id, clearFn) { this._id = id; this._clearFn = clearFn; } Timeout.prototype.unref = Timeout.prototype.ref = function () {}; Timeout.prototype.close = function () { this._clearFn.call(window, this._id); }; // Does not start the time, just sets up the members needed. exports.enroll = function (item, msecs) { clearTimeout(item._idleTimeoutId); item._idleTimeout = msecs; }; exports.unenroll = function (item) { clearTimeout(item._idleTimeoutId); item._idleTimeout = -1; }; exports._unrefActive = exports.active = function (item) { clearTimeout(item._idleTimeoutId); var msecs = item._idleTimeout; if (msecs >= 0) { item._idleTimeoutId = setTimeout(function onTimeout() { if (item._onTimeout) item._onTimeout(); }, msecs); } }; // setimmediate attaches itself to the global object __webpack_require__(12); exports.setImmediate = setImmediate; exports.clearImmediate = clearImmediate; /***/ }), /***/ 30: /***/ (function(module, exports, __webpack_require__) { "use strict"; /* WEBPACK VAR INJECTION */(function(global) {var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; // Last time updated: 2016-07-03 8:51:35 AM UTC // 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 //------------------------------------ // 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 //------------------------------------ // ______________________ // 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) { 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(); console.log('Paused recording.', this.mimeType || mediaRecorder.mimeType); }; this.resume = function () { if (!mediaRecorder) { return; } mediaRecorder.resume(); 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(mediaStream) { if (!mediaStream) { throw 'MediaStream is mandatory.'; } var self = this; var isMediaRecorder = isMediaRecorderCompatible(); this.stream = mediaStream; // void start(optional long timeSlice) // timestamp to fire "ondataavailable" this.start = function (timeSlice) { audioRecorder = new MediaStreamRecorder(mediaStream); videoRecorder = new MediaStreamRecorder(mediaStream); audioRecorder.mimeType = 'audio/ogg'; videoRecorder.mimeType = 'video/webm'; for (var prop in this) { if (typeof this[prop] !== 'function') { audioRecorder[prop] = videoRecorder[prop] = this[prop]; } } audioRecorder.ondataavailable = function (blob) { if (!audioVideoBlobs[recordingInterval]) { audioVideoBlobs[recordingInterval] = {}; } audioVideoBlobs[recordingInterval].audio = blob; if (audioVideoBlobs[recordingInterval].video && !audioVideoBlobs[recordingInterval].onDataAvailableEventFired) { audioVideoBlobs[recordingInterval].onDataAvailableEventFired = true; fireOnDataAvailableEvent(audioVideoBlobs[recordingInterval]); } }; videoRecorder.ondataavailable = function (blob) { if (isMediaRecorder) { return self.ondataavailable({ video: blob, audio: blob }); } if (!audioVideoBlobs[recordingInterval]) { audioVideoBlobs[recordingInterval] = {}; } audioVideoBlobs[recordingInterval].video = blob; if (audioVideoBlobs[recordingInterval].audio && !audioVideoBlobs[recordingInterval].onDataAvailableEventFired) { audioVideoBlobs[recordingInterval].onDataAvailableEventFired = true; fireOnDataAvailableEvent(audioVideoBlobs[recordingInterval]); } }; function fireOnDataAvailableEvent(blobs) { recordingInterval++; self.ondataavailable(blobs); } videoRecorder.onstop = audioRecorder.onstop = function (error) { self.onstop(error); }; if (!isMediaRecorder) { // to make sure both audio/video are synced. videoRecorder.onStartedDrawingNonBlankFrames = function () { videoRecorder.clearOldRecordedFrames(); audioRecorder.start(timeSlice); }; videoRecorder.start(timeSlice); } else { videoRecorder.start(timeSlice); } }; this.stop = function () { if (audioRecorder) { audioRecorder.stop(); } if (videoRecorder) { videoRecorder.stop(); } }; this.ondataavailable = function (blob) { console.log('ondataavailable..', blob); }; this.onstop = function (error) { 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; var audioVideoBlobs = {}; var recordingInterval = 0; } if (typeof MediaStreamRecorder !== 'undefined') { MediaStreamRecorder.MultiStreamRecorder = MultiStreamRecorder; } // _____________________________ // 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 getUserMedia() {} }; /*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 getUserMedia() {}, 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; } // ______________ (used to handle stuff like http://goo.gl/xmE5eg) issue #129 // ObjectStore.js var ObjectStore = { AudioContext: window.AudioContext || window.webkitAudioContext }; // ================== // 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.record(); */ this.start = function (timeSlice, __disableLogs) { 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. mediaRecorder.ignoreMutedMedia = self.ignoreMutedMedia || false; var firedOnDataAvailableOnce = false; // Dispatching OnDataAvailable Handler mediaRecorder.ondataavailable = function (e) { if (self.dontFireOnDataAvailableEvent) { return; } // 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.stop(); mediaRecorder = null; } // 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; }, 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(); } }; /** * 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.record(); 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(); }; // 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.pause = function () { if (!mediaRecorder) { return; } mediaRecorder.pause(); }; this.resume = function () { if (!mediaRecorder) { return; } mediaRecorder.resume(); }; this.ondataavailable = 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(); }; 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 unexpected but "rare" cases. window.scriptprocessornode = scriptprocessornode; if (numChannels === 1) { console.debug('All right-channels are skipped.'); } var isPaused = false; this.pause = function () { isPaused = true; }; this.resume = function () { isPaused = false; }; // http://webaudio.github.io/web-audio-api/#the-scriptprocessornode-interface scriptprocessornode.onaudioprocess = function (e) { if (!recording || requestDataInvoked || isPaused) {