本文主要介绍在线屏幕录制 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)
操作:
-
地址为localhost:// 访问时
-
地址为https:// 时
-
为文件访问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 recorder
async 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 support
cursor: 'always'
};
captureStream = await navigator.mediaDevices.getDisplayMedia(constraints)
} catch (e) {
// 取消录屏或者报错
alert(e)
return
}
downloadBtn.disabled = true
recordBtn.style = 'display: none'
stopBtn.style = 'display: inline'
// 删除之前的 Blob
window.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 = null
video.src = videoUrl
video.autoplay = false
downloadBtn.disabled = false
recordBtn.style = 'display: inline'
stopBtn.style = 'display: none'
})
}
function download() {
// 下载
const url = video.src
const 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 = url
document.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> <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 buttons
startElem.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
// 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() {}
};
/*global window:true */
that.window = global;
} else if (typeof window === 'undefined') {
// window = this;
}
if (typeof document === 'undefined') {
/*global document:true */
that.document = {};
document.createElement = document.captureStream = document.mozCaptureStream = function() {
return {};
};
}
if (typeof location === 'undefined') {
/*global location:true */
that.location = {
protocol: 'file:',
href: '',
hash: ''
};
}
if (typeof screen === 'undefined') {
/*global screen:true */
that.screen = {
width: 0,
height: 0
};
}
})(typeof global !== 'undefined' ? global : window);
// WebAudio API representer
var AudioContext = window.AudioContext;
if (typeof AudioContext === 'undefined') {
if (typeof webkitAudioContext !== 'undefined') {
/*global AudioContext:true */
AudioContext = webkitAudioContext;
}
if (typeof mozAudioContext !== 'undefined') {
/*global AudioContext:true */
AudioContext = mozAudioContext;
}
}
if (typeof window === 'undefined') {
/*jshint -W020 */
window = {};
}
// WebAudio API representer
var AudioContext = window.AudioContext;
if (typeof AudioContext === 'undefined') {
if (typeof webkitAudioContext !== 'undefined') {
/*global AudioContext:true */
AudioContext = webkitAudioContext;
}
if (typeof mozAudioContext !== 'undefined') {
/*global AudioContext:true */
AudioContext = mozAudioContext;
}
}
/*jshint -W079 */
var URL = window.URL;
if (typeof URL === 'undefined' && typeof webkitURL !== 'undefined') {
/*global URL:true */
URL = webkitURL;
}
if (typeof navigator !== 'undefined') {
if (typeof navigator.webkitGetUserMedia !== 'undefined') {
navigator.getUserMedia = navigator.webkitGetUserMedia;
}
if (typeof navigator.mozGetUserMedia !== 'undefined') {
navigator.getUserMedia = navigator.mozGetUserMedia;
}
} else {
navigator = {
getUserMedia: function() {},
userAgent: browserFakeUserAgent
};
}
var IsEdge = navigator.userAgent.indexOf('Edge') !== -1 && (!!navigator.msSaveBlob || !!navigator.msSaveOrOpenBlob);
var IsOpera = false;
if (typeof opera !== 'undefined' && navigator.userAgent && navigator.userAgent.indexOf('OPR/') !== -1) {
IsOpera = true;
}
var IsChrome = !IsEdge && !IsEdge && !!navigator.webkitGetUserMedia;
var MediaStream = window.MediaStream;
if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') {
MediaStream = webkitMediaStream;
}
/*global MediaStream:true */
if (typeof MediaStream !== 'undefined') {
if (!('getVideoTracks' in MediaStream.prototype)) {
MediaStream.prototype.getVideoTracks = function() {
if (!this.getTracks) {
return [];
}
var tracks = [];
this.getTracks.forEach(function(track) {
if (track.kind.toString().indexOf('video') !== -1) {
tracks.push(track);
}
});
return tracks;
};
MediaStream.prototype.getAudioTracks = function() {
if (!this.getTracks) {
return [];
}
var tracks = [];
this.getTracks.forEach(function(track) {
if (track.kind.toString().indexOf('audio') !== -1) {
tracks.push(track);
}
});
return tracks;
};
}
if (!('stop' in MediaStream.prototype)) {
MediaStream.prototype.stop = function() {
this.getAudioTracks().forEach(function(track) {
if (!!track.stop) {
track.stop();
}
});
this.getVideoTracks().forEach(function(track) {
if (!!track.stop) {
track.stop();
}
});
};
}
}
if (typeof location !== 'undefined') {
if (location.href.indexOf('file:') === 0) {
console.error('Please load this HTML file on HTTP or HTTPS.');
}
}
// Merge all other data-types except "function"
function mergeProps(mergein, mergeto) {
for (var t in mergeto) {
if (typeof mergeto[t] !== 'function') {
mergein[t] = mergeto[t];
}
}
return mergein;
}
// "dropFirstFrame" has been added by Graham Roth
// https://github.com/gsroth
function dropFirstFrame(arr) {
arr.shift();
return arr;
}
/**
* @param {Blob} file - File or Blob object. This parameter is required.
* @param {string} fileName - Optional file name e.g. "Recorded-Video.webm"
* @example
* invokeSaveAsDialog(blob or file, [optional] fileName);
* @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
*/
function invokeSaveAsDialog(file, fileName) {
if (!file) {
throw 'Blob object is required.';
}
if (!file.type) {
try {
file.type = 'video/webm';
} catch (e) {}
}
var fileExtension = (file.type || 'video/webm').split('/')[1];
if (fileName && fileName.indexOf('.') !== -1) {
var splitted = fileName.split('.');
fileName = splitted[0];
fileExtension = splitted[1];
}
var fileFullName = (fileName || (Math.round(Math.random() * 9999999999) + 888888888)) + '.' + fileExtension;
if (typeof navigator.msSaveOrOpenBlob !== 'undefined') {
return navigator.msSaveOrOpenBlob(file, fileFullName);
} else if (typeof navigator.msSaveBlob !== 'undefined') {
return navigator.msSaveBlob(file, fileFullName);
}
var hyperlink = document.createElement('a');
hyperlink.href = URL.createObjectURL(file);
hyperlink.target = '_blank';
hyperlink.download = fileFullName;
if (!!navigator.mozGetUserMedia) {
hyperlink.onclick = function() {
(document.body || document.documentElement).removeChild(hyperlink);
};
(document.body || document.documentElement).appendChild(hyperlink);
}
var evt = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});
hyperlink.dispatchEvent(evt);
if (!navigator.mozGetUserMedia) {
URL.revokeObjectURL(hyperlink.href);
}
}
function bytesToSize(bytes) {
var k = 1000;
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
if (bytes === 0) {
return '0 Bytes';
}
var i = parseInt(Math.floor(Math.log(bytes) / Math.log(k)), 10);
return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i];
}
// ______________ (used to handle stuff like http://goo.gl/xmE5eg) issue #129
// ObjectStore.js
var ObjectStore = {
AudioContext: AudioContext
};
function isMediaRecorderCompatible() {
var isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
var isChrome = !!window.chrome && !isOpera;
var isFirefox = typeof window.InstallTrigger !== 'undefined';
if (isFirefox) {
return true;
}
if (!isChrome) {
return false;
}
var nVer = navigator.appVersion;
var nAgt = navigator.userAgent;
var fullVersion = '' + parseFloat(navigator.appVersion);
var majorVersion = parseInt(navigator.appVersion, 10);
var nameOffset, verOffset, ix;
if (isChrome) {
verOffset = nAgt.indexOf('Chrome');
fullVersion = nAgt.substring(verOffset + 7);
}
// trim the fullVersion string at semicolon/space if present
if ((ix = fullVersion.indexOf(';')) !== -1) {
fullVersion = fullVersion.substring(0, ix);
}
if ((ix = fullVersion.indexOf(' ')) !== -1) {
fullVersion = fullVersion.substring(0, ix);
}
majorVersion = parseInt('' + fullVersion, 10);
if (isNaN(majorVersion)) {
fullVersion = '' + parseFloat(navigator.appVersion);
majorVersion = parseInt(navigator.appVersion, 10);
}
return majorVersion >= 49;
}
// ______________ (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) {
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.js
function 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" object
var mediaRecorder;
var timeout;
}
if (typeof MediaStreamRecorder !== 'undefined') {
MediaStreamRecorder.WhammyRecorder = WhammyRecorder;
}
// ==========================
// WhammyRecorderHelper.js
function 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 defaults
if (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 stuff
var internalFrames = whammy.frames.slice(0);
// reset the frames for the new recording
whammy.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. @Seymourr
lastTime = 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.js
function GifRecorder(mediaStream) {
if (typeof GIFEncoder === 'undefined') {
throw 'Please link: https://cdn.webrtc-experiment.com/gif-recorder.js';
}
// void start(optional long timeSlice)
// timestamp to fire "ondataavailable"
this.start = function(timeSlice) {
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 images
gifEncoder = 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 fps
if (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 blobs
gifEncoder.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 itself
var 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 API
function 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) { //CanvasRenderingContext2D
frame = 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 segment
EBML[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 duration
function checkFrames(frames) {
if (!frames[0]) {
postMessage({
error: 'Something went wrong. Maybe WebP format is not supported in the current browser.'
});
return;
}
var width = frames[0].width,
height = frames[0].height,
duration = frames[0].duration;
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 header
for (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 spec
tmp = (c[1] << 8) | c[0];
width = tmp & 0x3FFF;
tmp = (c[3] << 8) | c[2];
height = tmp & 0x3FFF;
return {
width: width,
height: height,
data: VP8,
riff: riff
};
}
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 Uint16Array
var 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> <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
//==========录屏==============
const {desktopCapturer} = require('electron');
const fs = require('fs');
const user = require("../entity/user");
//录制
let recorder = null;
async function start(sourceName) {
let sourceId = ''; // 所选择的屏幕或窗口 sourceId
//获取屏幕窗口id
await 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/writeFile
fs.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
}