JavaScript实现在线屏幕录制

ops/2024/9/23 20:17:31/

本文主要介绍在线屏幕录制 Demo

Its sole method is MediaDevices.getDisplayMedia()

!移动端暂不支持

环境要求

  • 新版本 Chrome,Edge,Firefox 桌面浏览器

常见问题

1. navigator.mediaDevices为undefined

在不安全的情况下,navigator.mediaDevices是undefined,安全上下文是使用 HTTPS 或file:///URL 方案加载的页面,或者从 localhost. (MediaDevices: getUserMedia() method - Web APIs | MDN)

操作:

  1. 地址为localhost:// 访问时

  2. 地址为https:// 时

  3. 为文件访问file:///

预览地址

https://xmmorz.github.io/screenCapture/

 参考代码1:

<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="utf-8"><title>在线屏幕录制</title><style>body {font-family: Arial;margin: 4vh auto;width: 90vw;max-width: 600px;text-align: center;}#controls {text-align: center;}.btn {margin: 10px 5px;padding: 15px;background-color: #2bcbba;border: none;color: white;font-weight: bold;border-radius: 6px;outline: none;font-size: 1.2em;width: 120px;height: 50px;}.btn:hover {background-color: #26de81;cursor: hand;}.btn:disabled {background-color: #2bcbba80;}#stop {background-color: #fc5c65;}#video {margin-top: 10px;margin-bottom: 20px;border: 12px solid #a5adb0;border-radius: 15px;outline: none;width: 100%;height: 400px;background-color: black;}h1 {color: #2bcbba;letter-spacing: -2.5px;line-height: 30px;}.created {color: lightgrey;letter-spacing: -0.7px;font-size: 1em;margin-top: 40px;}.created>a {color: #4b7bec;text-decoration: none;}</style>
</head><body><h1><u style='color:#fc5c65'>在线屏幕录制</u><br>支持 :新版本 Chrome,Edge,Firefox 桌面浏览器 <br></h1><video autoplay='true' id='video' controls='true' controlsList='nodownload'></video><button class='btn' id='record' onclick='record()'>录制</button><button style='display: none' class='btn' id='stop' onclick='stop()'>停止</button><button disabled='true' class='btn' id='download' onclick='download()'>下载</button><div class='created'> </div>
</body>
<script>const video = document.getElementById('video')const downloadBtn = document.getElementById('download')const recordBtn = document.getElementById('record')const stopBtn = document.getElementById('stop')let recorderasync function record() {// 开始录屏let captureStream//     if (navigator.getUserMedia) {//       captureStream = await navigator.getDisplayMedia({//         video: true,//         // audio: true,   not support//         cursor: 'always'//       })//     } else if (navigator.getDisplayMedia && navigator.mediaDevices.getDisplayMedia) {//       captureStream = await navigator.mediaDevices.getDisplayMedia({//        //       })//     } else {//       var error = 'getDisplayMedia API are not supported in this browser.';//       console.log('error', error);//       alert(error);//       return//     }try {var constraints = {video: true,// audio: true,   not supportcursor: 'always'};captureStream = await navigator.mediaDevices.getDisplayMedia(constraints)} catch (e) {// 取消录屏或者报错alert(e)return}downloadBtn.disabled = truerecordBtn.style = 'display: none'stopBtn.style = 'display: inline'// 删除之前的 Blobwindow.URL.revokeObjectURL(video.src)video.autoplay = true// 实时的播放录屏video.srcObject = captureStream// new 一个媒体记录recorder = new MediaRecorder(captureStream)recorder.start()captureStream.getVideoTracks()[0].onended = () => {// 录屏结束完成recorder.stop()}recorder.addEventListener("dataavailable", event => {// 录屏结束,并且数据可用console.log("dataavailable------------")console.log(event)let videoUrl = URL.createObjectURL(event.data, { type: 'video/ogg' })video.srcObject = nullvideo.src = videoUrlvideo.autoplay = falsedownloadBtn.disabled = falserecordBtn.style = 'display: inline'stopBtn.style = 'display: none'})}function download() {// 下载const url = video.srcconst name = new Date().toISOString().slice(0, 19).replace('T', ' ').replace(" ", "_").replace(/:/g, "-")const a = document.createElement('a')a.style = 'display: none'a.download = `${name}.ogg`a.href = urldocument.body.appendChild(a)a.click()}function stop() {let tracks = video.srcObject.getTracks()tracks.forEach(track => track.stop())recorder.stop()}
</script></html>

参考代码2

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>录屏</title><script src="./MediaStreamRecorder.js"></script><style>#video {border: 1px solid #999;width: 98%;max-width: 860px;}.error {color: red;}.warn {color: orange;}.info {color: darkgreen;}</style>
</head>
<body><p>This example shows you the contents of the selected part of your display.Click the Start Capture button to begin.</p><p><button id="start">Start Capture</button>&nbsp;<button id="stop">Stop Capture</button></p><video id="video" autoplay></video>
<br><strong>Log:</strong>
<br>
<pre id="log"></pre><script>const videoElem = document.getElementById("video");const logElem = document.getElementById("log");const startElem = document.getElementById("start");const stopElem = document.getElementById("stop");// Options for getDisplayMedia()var displayMediaOptions = {video: {cursor: "always"},audio: false};async function startCapture() {logElem.innerHTML = "";try {videoElem.srcObject = await navigator.mediaDevices.getDisplayMedia(displayMediaOptions);createRecorder(videoElem.srcObject); // createRecorder() 函数实现见下文dumpOptionsInfo();} catch(err) {console.error("Error: " + err);}}//信息function dumpOptionsInfo() {const videoTrack = videoElem.srcObject.getVideoTracks()[0];console.info("Track settings:");console.info(JSON.stringify(videoTrack.getSettings(), null, 2));console.info("Track constraints:");console.info(JSON.stringify(videoTrack.getConstraints(), null, 2));}let recorder = null;function createRecorder(stream) {recorder = new MediaRecorder(stream);recorder.start();// 如果 start 没设置 timeslice,ondataavailable 在 stop 时会触发recorder.ondataavailable = event => {let blob = new Blob([event.data], {type: 'video/mp4',});console.log("已下载至本地")let fileName='video-'+Math.floor(Math.random()*100000)+'.mp4';let file = new File([blob], fileName, {type: 'video/mp4'});console.log("file.path");console.log(file.path);saveAs(file);};recorder.onerror = err => {console.error(err);};}function stopCapture(evt) {let tracks = videoElem.srcObject.getTracks();tracks.forEach(track => track.stop());videoElem.srcObject = null;}// Set event listeners for the start and stop buttonsstartElem.addEventListener("click", function(evt) {startCapture();}, false);stopElem.addEventListener("click", function(evt) {stopCapture();}, false);console.log = msg => logElem.innerHTML += `${msg}<br>`;console.error = msg => logElem.innerHTML += `<span class="error">${msg}</span><br>`;console.warn = msg => logElem.innerHTML += `<span class="warn">${msg}<span><br>`;console.info = msg => logElem.innerHTML += `<span class="info">${msg}</span><br>`;</script>
</body>
</html>

MediaStreamRecorder.js 

javascript">// 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.jsfunction 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 StereoAudioRecorderif (this.mimeType === 'audio/wav' || this.mimeType === 'audio/pcm') {Recorder = StereoAudioRecorder;}// allows forcing StereoAudioRecorder.js on Edge/Firefoxif (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 || GifRecorderthis.recorderType = null;// video/webm or audio/webm or audio/ogg or audio/wavthis.mimeType = 'video/webm';// logs are enabled by defaultthis.disableLogs = false;// Reference to "MediaRecorder.js"var mediaRecorder;
}// ______________________
// MultiStreamRecorder.jsfunction 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.jsvar 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/gsrothfunction 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 presentif ((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 {// webkitMediaStreamstream = 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 stablerecorderHints = '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 scenariostry {mediaRecorder = new MediaRecorder(mediaStream, recorderHints);} catch (e) {// if someone passed NON_supported mimeType// or if Firefox on AndroidmediaRecorder = 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 HandlermediaRecorder.ondataavailable = function(e) {if (self.dontFireOnDataAvailableEvent) {return;}// how to fix FF-corrupt-webm issues?// should we leave this?          e.data.size < 26800if (!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 intervalself.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 valueif (!!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" objectvar mediaRecorder;function isMediaStreamActive() {if ('active' in mediaStream) {if (!mediaStream.active) {return false;}} else if ('ended' in mediaStream) { // old hackif (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.jsfunction 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" objectvar mediaRecorder;var timeout;
}if (typeof MediaStreamRecorder !== 'undefined') {MediaStreamRecorder.StereoAudioRecorder = StereoAudioRecorder;
}// ============================
// StereoAudioRecorderHelper.js// source code from: http://typedarray.org/wp-content/projects/WebAudioRecorder/script.jsfunction StereoAudioRecorderHelper(mediaStream, root) {// variables    var deviceSampleRate = 44100; // range: 22050 to 96000if (!ObjectStore.AudioContextConstructor) {ObjectStore.AudioContextConstructor = new ObjectStore.AudioContext();}// check device sample ratedeviceSampleRate = 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 recordingleftchannel.length = rightchannel.length = 0;recordingLength = 0;};this.requestData = function() {if (isPaused) {return;}if (recordingLength === 0) {requestDataInvoked = false;return;}requestDataInvoked = true;// clone stuffvar internalLeftChannel = leftchannel.slice(0);var internalRightChannel = rightchannel.slice(0);var internalRecordingLength = recordingLength;// reset the buffers for the new recordingleftchannel.length = rightchannel.length = [];recordingLength = 0;requestDataInvoked = false;// we flat the left and right channels downvar leftBuffer = mergeBuffers(internalLeftChannel, internalRecordingLength);var interleaved = leftBuffer;// we interleave both channels togetherif (numChannels === 2) {var rightBuffer = mergeBuffers(internalRightChannel, internalRecordingLength); // bug fixed via #70,#71interleaved = interleave(leftBuffer, rightBuffer);}if (isPCM) {// our final binary blobvar 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 filevar buffer = new ArrayBuffer(44 + interleaved.length * 2);var view = new DataView(buffer);// RIFF chunk descriptorwriteUTFBytes(view, 0, 'RIFF');// -8 (via #97)view.setUint32(4, 44 + interleaved.length * 2 - 8, true);writeUTFBytes(view, 8, 'WAVE');// FMT sub-chunkwriteUTFBytes(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-chunkwriteUTFBytes(view, 36, 'data');view.setUint32(40, interleaved.length * 2, true);// write the PCM samplesvar 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 blobvar blob = new Blob([view], {type: 'audio/wav'});console.debug('audio recorded blob size:', bytesToSize(blob.size));root.ondataavailable(blob);};this.stop = function() {// we stop recordingrecording = 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 contextvar context = ObjectStore.AudioContextConstructor;// creates a gain nodeObjectStore.VolumeGainNode = context.createGain();var volume = ObjectStore.VolumeGainNode;// creates an audio node from the microphone incoming streamObjectStore.AudioInput = context.createMediaStreamSource(mediaStream);// creates an audio node from the microphone incoming streamvar audioInput = ObjectStore.AudioInput;// connect the stream to the gain nodeaudioInput.connect(volume);/* From the spec: This value controls how frequently the audioprocess event isdispatched 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-interfacescriptprocessornode.onaudioprocess = function(e) {if (!recording || requestDataInvoked || isPaused) {return;}var left = e.inputBuffer.getChannelData(0);leftchannel.push(new Float32Array(left));if (numChannels === 2) {var right = e.inputBuffer.getChannelData(1);rightchannel.push(new Float32Array(right));}recordingLength += bufferSize;};volume.connect(scriptprocessornode);scriptprocessornode.connect(context.destination);
}if (typeof MediaStreamRecorder !== 'undefined') {MediaStreamRecorder.StereoAudioRecorderHelper = StereoAudioRecorderHelper;
}// ===================
// WhammyRecorder.jsfunction WhammyRecorder(mediaStream) {// void start(optional long timeSlice)// timestamp to fire "ondataavailable"this.start = function(timeSlice) {timeSlice = timeSlice || 1000;mediaRecorder = new WhammyRecorderHelper(mediaStream, this);for (var prop in this) {if (typeof this[prop] !== 'function') {mediaRecorder[prop] = this[prop];}}mediaRecorder.record();timeout = setInterval(function() {mediaRecorder.requestData();}, timeSlice);};this.stop = function() {if (mediaRecorder) {mediaRecorder.stop();clearTimeout(timeout);}};this.clearOldRecordedFrames = function() {if (mediaRecorder) {mediaRecorder.clearOldRecordedFrames();}};this.pause = function() {if (!mediaRecorder) {return;}mediaRecorder.pause();};this.resume = function() {if (!mediaRecorder) {return;}mediaRecorder.resume();};this.ondataavailable = function() {};// Reference to "WhammyRecorder" objectvar mediaRecorder;var timeout;
}if (typeof MediaStreamRecorder !== 'undefined') {MediaStreamRecorder.WhammyRecorder = WhammyRecorder;
}// ==========================
// WhammyRecorderHelper.jsfunction WhammyRecorderHelper(mediaStream, root) {this.record = function(timeSlice) {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.video) {this.video = {width: this.width,height: this.height};}if (!this.canvas || !this.canvas.width || !this.canvas.height) {this.canvas = {width: this.width,height: this.height};}canvas.width = this.canvas.width;canvas.height = this.canvas.height;// setting defaultsif (this.video && this.video instanceof HTMLVideoElement) {this.isHTMLObject = true;video = this.video.cloneNode();} else {video = document.createElement('video');video.src = URL.createObjectURL(mediaStream);video.width = this.video.width;video.height = this.video.height;}video.muted = true;video.play();lastTime = new Date().getTime();whammy = new Whammy.Video(root.speed, root.quality);console.log('canvas resolutions', canvas.width, '*', canvas.height);console.log('video width/height', video.width || canvas.width, '*', video.height || canvas.height);drawFrames();};this.clearOldRecordedFrames = function() {whammy.frames = [];};var requestDataInvoked = false;this.requestData = function() {if (isPaused) {return;}if (!whammy.frames.length) {requestDataInvoked = false;return;}requestDataInvoked = true;// clone stuffvar internalFrames = whammy.frames.slice(0);// reset the frames for the new recordingwhammy.frames = dropBlackFrames(internalFrames, -1);whammy.compile(function(whammyBlob) {root.ondataavailable(whammyBlob);console.debug('video recorded blob size:', bytesToSize(whammyBlob.size));});whammy.frames = [];requestDataInvoked = false;};var isOnStartedDrawingNonBlankFramesInvoked = false;function drawFrames() {if (isPaused) {lastTime = new Date().getTime();setTimeout(drawFrames, 500);return;}if (isStopDrawing) {return;}if (requestDataInvoked) {return setTimeout(drawFrames, 100);}var duration = new Date().getTime() - lastTime;if (!duration) {return drawFrames();}// via webrtc-experiment#206, by Jack i.e. @SeymourrlastTime = new Date().getTime();if (!self.isHTMLObject && video.paused) {video.play(); // Android}context.drawImage(video, 0, 0, canvas.width, canvas.height);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();}setTimeout(drawFrames, 10);}var isStopDrawing = false;this.stop = function() {isStopDrawing = true;this.requestData();};var canvas = document.createElement('canvas');var context = canvas.getContext('2d');var video;var lastTime;var whammy;var self = this;function isBlankFrame(frame, _pixTolerance, _frameTolerance) {var localCanvas = document.createElement('canvas');localCanvas.width = canvas.width;localCanvas.height = canvas.height;var context2d = localCanvas.getContext('2d');var sampleColor = {r: 0,g: 0,b: 0};var maxColorDifference = Math.sqrt(Math.pow(255, 2) +Math.pow(255, 2) +Math.pow(255, 2));var pixTolerance = _pixTolerance && _pixTolerance >= 0 && _pixTolerance <= 1 ? _pixTolerance : 0;var frameTolerance = _frameTolerance && _frameTolerance >= 0 && _frameTolerance <= 1 ? _frameTolerance : 0;var matchPixCount, endPixCheck, maxPixCount;var image = new Image();image.src = frame.image;context2d.drawImage(image, 0, 0, canvas.width, canvas.height);var imageData = context2d.getImageData(0, 0, canvas.width, canvas.height);matchPixCount = 0;endPixCheck = imageData.data.length;maxPixCount = imageData.data.length / 4;for (var pix = 0; pix < endPixCheck; pix += 4) {var currentColor = {r: imageData.data[pix],g: imageData.data[pix + 1],b: imageData.data[pix + 2]};var colorDifference = Math.sqrt(Math.pow(currentColor.r - sampleColor.r, 2) +Math.pow(currentColor.g - sampleColor.g, 2) +Math.pow(currentColor.b - sampleColor.b, 2));// difference in color it is difference in color vectors (r1,g1,b1) <=> (r2,g2,b2)if (colorDifference <= maxColorDifference * pixTolerance) {matchPixCount++;}}if (maxPixCount - matchPixCount <= maxPixCount * frameTolerance) {return false;} else {return true;}}function dropBlackFrames(_frames, _framesToCheck, _pixTolerance, _frameTolerance) {var localCanvas = document.createElement('canvas');localCanvas.width = canvas.width;localCanvas.height = canvas.height;var context2d = localCanvas.getContext('2d');var resultFrames = [];var checkUntilNotBlack = _framesToCheck === -1;var endCheckFrame = (_framesToCheck && _framesToCheck > 0 && _framesToCheck <= _frames.length) ?_framesToCheck : _frames.length;var sampleColor = {r: 0,g: 0,b: 0};var maxColorDifference = Math.sqrt(Math.pow(255, 2) +Math.pow(255, 2) +Math.pow(255, 2));var pixTolerance = _pixTolerance && _pixTolerance >= 0 && _pixTolerance <= 1 ? _pixTolerance : 0;var frameTolerance = _frameTolerance && _frameTolerance >= 0 && _frameTolerance <= 1 ? _frameTolerance : 0;var doNotCheckNext = false;for (var f = 0; f < endCheckFrame; f++) {var matchPixCount, endPixCheck, maxPixCount;if (!doNotCheckNext) {var image = new Image();image.src = _frames[f].image;context2d.drawImage(image, 0, 0, canvas.width, canvas.height);var imageData = context2d.getImageData(0, 0, canvas.width, canvas.height);matchPixCount = 0;endPixCheck = imageData.data.length;maxPixCount = imageData.data.length / 4;for (var pix = 0; pix < endPixCheck; pix += 4) {var currentColor = {r: imageData.data[pix],g: imageData.data[pix + 1],b: imageData.data[pix + 2]};var colorDifference = Math.sqrt(Math.pow(currentColor.r - sampleColor.r, 2) +Math.pow(currentColor.g - sampleColor.g, 2) +Math.pow(currentColor.b - sampleColor.b, 2));// difference in color it is difference in color vectors (r1,g1,b1) <=> (r2,g2,b2)if (colorDifference <= maxColorDifference * pixTolerance) {matchPixCount++;}}}if (!doNotCheckNext && maxPixCount - matchPixCount <= maxPixCount * frameTolerance) {// console.log('removed black frame : ' + f + ' ; frame duration ' + _frames[f].duration);} else {// console.log('frame is passed : ' + f);if (checkUntilNotBlack) {doNotCheckNext = true;}resultFrames.push(_frames[f]);}}resultFrames = resultFrames.concat(_frames.slice(endCheckFrame));if (resultFrames.length <= 0) {// at least one last frame should be available for next manipulation// if total duration of all frames will be < 1000 than ffmpeg doesn't work well...resultFrames.push(_frames[_frames.length - 1]);}return resultFrames;}var isPaused = false;this.pause = function() {isPaused = true;};this.resume = function() {isPaused = false;};
}if (typeof MediaStreamRecorder !== 'undefined') {MediaStreamRecorder.WhammyRecorderHelper = WhammyRecorderHelper;
}// --------------
// GifRecorder.jsfunction 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) {timeSlice = timeSlice || 1000;var imageWidth = this.videoWidth || 320;var imageHeight = this.videoHeight || 240;canvas.width = video.width = imageWidth;canvas.height = video.height = imageHeight;// external library to record as GIF imagesgifEncoder = new GIFEncoder();// void setRepeat(int iter)// Sets the number of times the set of GIF frames should be played.// Default is 1; 0 means play indefinitely.gifEncoder.setRepeat(0);// void setFrameRate(Number fps)// Sets frame rate in frames per second.// Equivalent to setDelay(1000/fps).// Using "setDelay" instead of "setFrameRate"gifEncoder.setDelay(this.frameRate || this.speed || 200);// void setQuality(int quality)// Sets quality of color quantization (conversion of images to the// maximum 256 colors allowed by the GIF specification).// Lower values (minimum = 1) produce better colors,// but slow processing significantly. 10 is the default,// and produces good color mapping at reasonable speeds.// Values greater than 20 do not yield significant improvements in speed.gifEncoder.setQuality(this.quality || 1);// Boolean start()// This writes the GIF Header and returns false if it fails.gifEncoder.start();startTime = Date.now();function drawVideoFrame(time) {if (isPaused) {setTimeout(drawVideoFrame, 500, time);return;}lastAnimationFrame = requestAnimationFrame(drawVideoFrame);if (typeof lastFrameTime === undefined) {lastFrameTime = time;}// ~10 fpsif (time - lastFrameTime < 90) {return;}if (video.paused) {video.play(); // Android}context.drawImage(video, 0, 0, imageWidth, imageHeight);gifEncoder.addFrame(context);// console.log('Recording...' + Math.round((Date.now() - startTime) / 1000) + 's');// console.log("fps: ", 1000 / (time - lastFrameTime));lastFrameTime = time;}lastAnimationFrame = requestAnimationFrame(drawVideoFrame);timeout = setTimeout(doneRecording, timeSlice);};function doneRecording() {endTime = Date.now();var gifBlob = new Blob([new Uint8Array(gifEncoder.stream().bin)], {type: 'image/gif'});self.ondataavailable(gifBlob);// todo: find a way to clear old recorded blobsgifEncoder.stream().bin = [];}this.stop = function() {if (lastAnimationFrame) {cancelAnimationFrame(lastAnimationFrame);clearTimeout(timeout);doneRecording();}};var isPaused = false;this.pause = function() {isPaused = true;};this.resume = function() {isPaused = false;};this.ondataavailable = function() {};this.onstop = function() {};// Reference to itselfvar self = this;var canvas = document.createElement('canvas');var context = canvas.getContext('2d');var video = document.createElement('video');video.muted = true;video.autoplay = true;video.src = URL.createObjectURL(mediaStream);video.play();var lastAnimationFrame = null;var startTime, endTime, lastFrameTime;var gifEncoder;var timeout;
}if (typeof MediaStreamRecorder !== 'undefined') {MediaStreamRecorder.GifRecorder = GifRecorder;
}// 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 APIfunction WhammyVideo(duration, quality) {this.frames = [];if (!duration) {duration = 1;}this.duration = 1000 / duration;this.quality = quality || 0.8;}/*** 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) { //CanvasRenderingContext2Dframe = frame.canvas;}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': 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}, {'data': 'whammy','id': 0x4d80 // MuxingApp}, {'data': 'whammy','id': 0x5741 // WritingApp}, {'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': '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 < clusterMaxDuration);var clusterCounter = 0;var cluster = {'id': 0x1f43b675, // Cluster'data': getClusterData(clusterTimecode, clusterCounter, clusterFrames)}; //Add cluster to segmentEBML[1].data.push(cluster);clusterTimecode += clusterDuration;}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 durationfunction 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;for (var i = 1; i < frames.length; i++) {duration += frames[i].duration;}return {duration: duration,width: width,height: height};}function numToBuffer(num) {var parts = [];while (num > 0) {parts.push(num & 0xff);num = num >> 8;}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));}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 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;ebml.push(numToBuffer(json[i].id));ebml.push(bitsToBuffer(size));ebml.push(data);}return new Blob(ebml, {type: 'video/webm'});}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 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';}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 parseWebP(riff) {var VP8 = riff.RIFF[0].WEBP[0];var frameStart = VP8.indexOf('\x9d\x01\x2a'); // A VP8 keyframe starts with the 0x9d012a headerfor (var i = 0, c = []; i < 4; i++) {c[i] = VP8.charCodeAt(frameStart + 3 + i);}var width, height, tmp;//the code below is literally copied verbatim from the bitstream spectmp = (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};}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);}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;}function doubleToString(num) {return [].slice.call(new Uint8Array((new Float64Array([num])).buffer), 0).map(function(e) {return String.fromCharCode(e);}).reverse().join('');}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 {/*** 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};
})();if (typeof MediaStreamRecorder !== 'undefined') {MediaStreamRecorder.Whammy = Whammy;
}// Last time updated at Nov 18, 2014, 08:32:23// Latest file can be found here: https://cdn.webrtc-experiment.com/ConcatenateBlobs.js// Muaz Khan    - www.MuazKhan.com
// MIT License  - www.WebRTC-Experiment.com/licence
// Source Code  - https://github.com/muaz-khan/ConcatenateBlobs
// Demo         - https://www.WebRTC-Experiment.com/ConcatenateBlobs/// ___________________
// ConcatenateBlobs.js// Simply pass array of blobs.
// This javascript library will concatenate all blobs in single "Blob" object.(function() {window.ConcatenateBlobs = function(blobs, type, callback) {var buffers = [];var index = 0;function readAsArrayBuffer() {if (!blobs[index]) {return concatenateBuffers();}var reader = new FileReader();reader.onload = function(event) {buffers.push(event.target.result);index++;readAsArrayBuffer();};reader.readAsArrayBuffer(blobs[index]);}readAsArrayBuffer();function concatenateBuffers() {var byteLength = 0;buffers.forEach(function(buffer) {byteLength += buffer.byteLength;});var tmp = new Uint16Array(byteLength);var lastOffset = 0;buffers.forEach(function(buffer) {// BYTES_PER_ELEMENT == 2 for Uint16Arrayvar reusableByteLength = buffer.byteLength;if (reusableByteLength % 2 != 0) {buffer = buffer.slice(0, reusableByteLength - 1)}tmp.set(new Uint16Array(buffer), lastOffset);lastOffset += reusableByteLength;});var blob = new Blob([tmp.buffer], {type: type});callback(blob);}};
})();// https://github.com/streamproc/MediaStreamRecorder/issues/42
if (typeof module !== 'undefined' /* && !!module.exports*/ ) {module.exports = MediaStreamRecorder;
}if (typeof define === 'function' && define.amd) {define('MediaStreamRecorder', [], function() {return MediaStreamRecorder;});
}

参考代码3

<!DOCTYPE html>
<html lang="en">
<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><title>视频流采集</title>
</head>
<body>
<video id="video" autoplay></video>
<p><button id="start">Start Capture</button>&nbsp;<button id="stop">Stop Capture</button></p>
<script>const startElem = document.getElementById("start");const stopElem = document.getElementById("stop");
let a = navigator.mediaDevices.getDisplayMedia({audio: false,video: true}, gotStream, getUserMediaError);var recorder = null;function gotStream(stream) {document.querySelector('video').src = URL.createObjectURL(stream);const options = {audioBitsPerSecond: 128000,videoBitsPerSecond: 2500000,mimeType: "video/mp4",};recorder = new MediaRecorder(stream, options);recorder.start();// 如果 start 没设置 timeslice,ondataavailable 在 stop 时会触发recorder.ondataavailable = event => {let blob = new Blob([event.data], {type: 'video/mp4',});console.log("已下载至本地")let fileName='video-'+Math.floor(Math.random()*100000)+'.mp4';let file = new File([blob], fileName, {type: 'video/mp4'});console.log("file.path");console.log(file.path);saveAs(file);};}function getUserMediaError(e) {console.log('getUserMediaError');}// Set event listeners for the start and stop buttons
startElem.addEventListener("click", function(evt) {}, false);stopElem.addEventListener("click", function(evt) {recorder.stop()
}, false);
</script>
</body>
</html>

参考代码4

javascript">//==========录屏==============
const {desktopCapturer} = require('electron');
const fs = require('fs');
const user = require("../entity/user");//录制
let recorder = null;async function start(sourceName) {let sourceId = ''; // 所选择的屏幕或窗口 sourceId//获取屏幕窗口idawait desktopCapturer.getSources({types: ['window', 'screen']}).then( sources => {for (const source of sources) {if (source.name === sourceName) {sourceId = source.id// var mainBrowserWindow = user.getMainBrowserWindow();// mainBrowserWindow.webContents.send('SET_SOURCE', source.id)//mainWindow.webContents.send('SET_SOURCE', source.id)console.log(source.id)return}}})//获取视频流console.log(sourceId)let stream = await navigator.mediaDevices.getUserMedia({audio: false,video: {mandatory: {chromeMediaSource: 'desktop',chromeMediaSourceId: sourceId,// minWidth: 1280,// maxWidth: 1280,// minHeight: 720,// maxHeight: 720}},cursor: 'always' //鼠标});//初始化录制recorder = new MediaRecorder(stream);recorder.start();// 如果 start 没设置 timeslice,ondataavailable 在 stop 时会触发recorder.ondataavailable = event => {let blob = new Blob([event.data], {type: 'video/mp4',});saveMedia(blob);};recorder.onerror = err => {console.error(err);};//每秒存一次setInterval(() => {requestData();}, 1000)}function saveMedia(blob) {console.log(blob.type)let formData = new FormData();formData.append('file', blob, 'video.mp4');//上传服务器$.ajax({url: 'http://localhost:8080/upload',type: 'POST',cache: false,data: formData,processData: false,contentType: false,}).done(function(res){console.log(res)}).fail(function(res) {console.log(res)});let reader = new FileReader();reader.onload = () => {let buffer = new Buffer(reader.result);//appendFile/writeFilefs.appendFile('D:\\test.mp4', buffer, {}, (err, res) => {if (err) return console.error(err);});};reader.onerror = err => console.error(err);reader.readAsArrayBuffer(blob);console.log("保存完成")
}function requestData(){recorder.requestData();console.log("requestData")
}//结束并保存本地
function stopRecord() {recorder.stop();
}module.exports = {start,stopRecord
}


http://www.ppmy.cn/ops/24933.html

相关文章

二分法(Java实现)

二分法&#xff08;也称为二分查找法或折半查找法&#xff09;是一种在有序数组中查找某一特定元素的搜索算法。搜索过程从数组的中间元素开始&#xff0c;如果中间元素正好是要查找的元素&#xff0c;则搜索过程结束&#xff1b;如果某一特定元素大于或者小于中间元素&#xf…

[iOS]使用CocoaPods发布私有库

1.创建私有 Spec 仓库 首先&#xff0c;需要一个私有的 Git 仓库来存放你的 Podspec 文件&#xff0c;这个仓库用于索引你所有的私有 Pods。 在 GitHub 或其他 Git 服务上创建一个新的私有仓库&#xff0c;例如&#xff0c;名为 PrivatePodSpecs。克隆这个仓库到本地&#xf…

观成科技:蔓灵花组织加密通信研究分析总结

1.概述 蔓灵花&#xff0c;又名"Bitter"&#xff0c;常对南亚周边及孟加拉湾海域的相关国家发起网络攻击&#xff0c;主要针对巴基斯坦和中国两国。其攻击目标主要包括政府部门、核工业、能源、国防、军工、船舶工业、航空工业以及海运等行业&#xff0c;其主要意图…

解决Linux CentOS 7安装了vim编辑器却vim编辑器不起作用、无任何反应

文章目录 前言一、解决vim不起作用&#xff08;卸载重新安装&#xff09;1.重新安装vim2.测试vim是否能正常使用 二、解决vim: error while loading shared libraries: /lib64/libgpm.so.2: file too short报错三、解决vim编辑器不能使用方向键和退格键问题 remove vim-common …

未来已来:PostCSS插件让你提前使用CSS新特性

PostCSS是一个用JavaScript工具和插件生态系统来转换CSS代码的工具。它允许开发者使用现代CSS语法来编写样式&#xff0c;然后自动将它们转换为大多数浏览器能够理解的格式。 PostCSS的主要功能包括&#xff1a; 当然&#xff0c;让我们更详细地了解PostCSS的每个功能点&…

合泰杯(HT32F52352)RTC的应用(计时)--->掉电不丢失VBAT(代码已经实现附带源码)

摘要 在HT32F52352合泰单片机开发中&#xff0c;rtc在网上还是挺少人应用的&#xff0c;找了很久没什么资料&#xff0c;现在我根据手册和官方的代码进行配置理解。 RTC在嵌入式单片机中是一个很重要的应用资源。 记录事件时间戳&#xff1a;RTC可以记录事件发生的精确时间&…

亚马逊又撕开了一个新的流量大口子 | 最新快讯

文蓝海亿官网 亚马逊在美国电商市场中一马当先&#xff0c;占比 40%&#xff0c;可谓妥妥的龙头老大。 不过&#xff0c;中国“出海四小龙”&#xff08;Temu、SHEIN、TikTok、阿里速卖通&#xff09;逐渐逼近&#xff0c;它们的 GMV 加起来已经接近美国电商市场份额的 10%。 从…

【深度学习】概率图模型理论简介

概率图模型 1 概率图模型2 模型表示2.1 有向图模型(Bayesian networks 贝叶斯网络)2.2 无向图模型(Markov random fields 马尔可夫网络)参考概率图模型(Probabilistic Graphical Model,PGM)是一种用图结构来表示和推断多元随机变量之间条件独立性的概率模型。图模型提供…