|
|
@@ -0,0 +1,429 @@
|
|
|
+import { videoConstraints } from './videoConstraints';
|
|
|
+import { isSupport } from './isSupport';
|
|
|
+import { getVideo } from './videoElement';
|
|
|
+import { EventEmitter } from 'eventemitter3';
|
|
|
+export var RecorderStatusType;
|
|
|
+(function (RecorderStatusType) {
|
|
|
+ RecorderStatusType[RecorderStatusType["init"] = 0] = "init";
|
|
|
+ RecorderStatusType[RecorderStatusType["start"] = 1] = "start";
|
|
|
+ RecorderStatusType[RecorderStatusType["hold"] = 2] = "hold";
|
|
|
+ RecorderStatusType[RecorderStatusType["end"] = 3] = "end";
|
|
|
+})(RecorderStatusType || (RecorderStatusType = {}));
|
|
|
+function getSupportedMimeType() {
|
|
|
+ const firefoxMimeTypes = [
|
|
|
+ 'video/webm;codecs=vp8,opus',
|
|
|
+ 'video/webm'
|
|
|
+ ];
|
|
|
+ const defaultMimeTypes = [
|
|
|
+ 'video/webm;codecs=vp9,opus',
|
|
|
+ 'video/webm;codecs=vp8,opus',
|
|
|
+ 'video/webm',
|
|
|
+ 'video/mp4'
|
|
|
+ ];
|
|
|
+ const isFirefox = /firefox/i.test(navigator.userAgent);
|
|
|
+ const mimeTypes = isFirefox ? firefoxMimeTypes : defaultMimeTypes;
|
|
|
+
|
|
|
+ for (const type of mimeTypes) {
|
|
|
+ if (MediaRecorder && MediaRecorder.isTypeSupported(type)) {
|
|
|
+ return type;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+export class BasicSimaqRecorder extends EventEmitter {
|
|
|
+ // private codecH264Type = 'video/mp4;codecs=h264,opus';
|
|
|
+ constructor(arg) {
|
|
|
+ super();
|
|
|
+ this.displayMediaStreamConstraints = {
|
|
|
+ video: videoConstraints.getValue(),
|
|
|
+ // audio: audioConstraints.getValue(),
|
|
|
+ preferCurrentTab: true,
|
|
|
+ audio: false,
|
|
|
+ // selfBrowserSurface: 'exclude',
|
|
|
+ systemAudio: 'exclude',
|
|
|
+ };
|
|
|
+ this.isStartRecoding = false;
|
|
|
+ this.status = 0;
|
|
|
+ // public record = new BehaviorSubject<Blob>(null);
|
|
|
+ this.recordChunks = [];
|
|
|
+ this.autoDownload = false;
|
|
|
+ this.passiveEnd = false;
|
|
|
+ this.disabledAudio = false;
|
|
|
+ this.systemAudio = false;
|
|
|
+ this.debug = false;
|
|
|
+ this.codecType = getSupportedMimeType() || 'video/webm;codecs=vp9,opus';
|
|
|
+ this.codecFileType = 'video/webm';
|
|
|
+ this.sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
+ this.autoDownload = arg.autoDownload;
|
|
|
+ this.platform = arg.platform;
|
|
|
+ this.platformConfig = arg.config;
|
|
|
+ this.uploadUrl = arg.uploadUrl;
|
|
|
+ this.disabledAudio = arg.disabledAudio;
|
|
|
+ this.debug = arg.debug;
|
|
|
+ this.systemAudio = arg.systemAudio;
|
|
|
+ this.setSystemAudio();
|
|
|
+ this.initParams(arg);
|
|
|
+ videoConstraints.subscribe((value) => {
|
|
|
+ this.debug && console.log('subscribe', value);
|
|
|
+ this.displayMediaStreamConstraints.video = Object.assign({}, this.displayMediaStreamConstraints.video, value);
|
|
|
+ });
|
|
|
+ this.debug && console.log('arg', arg);
|
|
|
+ }
|
|
|
+ get isElectron() {
|
|
|
+ return this.platform === 'electron';
|
|
|
+ }
|
|
|
+ get isWeb() {
|
|
|
+ return this.platform === 'web';
|
|
|
+ }
|
|
|
+ get isCanvas() {
|
|
|
+ return this.platform === 'canvas';
|
|
|
+ }
|
|
|
+ get canvasElement() {
|
|
|
+ // return document.getElementById(this.canvasId);
|
|
|
+ return document.querySelector(this.canvasId);
|
|
|
+ }
|
|
|
+ set canvasElement(canvas) {
|
|
|
+ this.canvasElement = canvas;
|
|
|
+ }
|
|
|
+ initParams(arg) {
|
|
|
+ switch (arg.platform) {
|
|
|
+ case 'web':
|
|
|
+ if (arg.config.frameRate) {
|
|
|
+ videoConstraints.value.frameRate = arg.config.frameRate;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case 'electron':
|
|
|
+ this.chromeMediaSourceId = arg.config.chromeMediaSourceId;
|
|
|
+ break;
|
|
|
+ case 'canvas':
|
|
|
+ this.canvasId = arg.config.canvasId;
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ setSystemAudio() {
|
|
|
+ if (this.systemAudio) {
|
|
|
+ this.displayMediaStreamConstraints.systemAudio = 'include';
|
|
|
+ this.displayMediaStreamConstraints.audio = true;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ this.displayMediaStreamConstraints.systemAudio = 'exclude';
|
|
|
+ this.displayMediaStreamConstraints.audio = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ async startRecord() {
|
|
|
+ try {
|
|
|
+ if (!this.isStartRecoding) {
|
|
|
+ this.debug && console.log('开始录屏!', isSupport());
|
|
|
+ if (!isSupport()) {
|
|
|
+ console.error('当前浏览器不支持录屏或不存在https环境');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // const media = this.isElectron
|
|
|
+ // ? await this.getEletronDisplayMedia()
|
|
|
+ // : await this.getDisplayMedia();
|
|
|
+ const media = await this.getDefaultMedia();
|
|
|
+ this.debug && console.log('media', media);
|
|
|
+ if (media) {
|
|
|
+ this.emit('startRecord');
|
|
|
+ this.isStartRecoding = true;
|
|
|
+ this.status = RecorderStatusType.start;
|
|
|
+ // console.log('media', media);
|
|
|
+ const video = getVideo();
|
|
|
+ if (video) {
|
|
|
+ // console.log('video', video);
|
|
|
+ video.srcObject = media;
|
|
|
+ this.stream = media;
|
|
|
+ await this.createMediaRecoder();
|
|
|
+ this.mediaRecorder.start();
|
|
|
+ this.stream.getVideoTracks()[0].onended = () => {
|
|
|
+ this.debug && console.log('stop-share');
|
|
|
+ this.endRecord();
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ this.streamStop();
|
|
|
+ this.isStartRecoding = false;
|
|
|
+ this.status = RecorderStatusType.end;
|
|
|
+ this.emit('cancelRecord');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ console.error('startRecord::', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ getDefaultMedia() {
|
|
|
+ return new Promise(async (resolve) => {
|
|
|
+ switch (this.platform) {
|
|
|
+ case 'web':
|
|
|
+ return resolve(await this.getDisplayMedia());
|
|
|
+ case 'canvas':
|
|
|
+ return resolve(await this.getCanvasSteam());
|
|
|
+ case 'electron':
|
|
|
+ return resolve(await this.getEletronDisplayMedia());
|
|
|
+ default:
|
|
|
+ return resolve(await this.getDisplayMedia());
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ getCanvasSteam() {
|
|
|
+ return new Promise(async (resolve) => {
|
|
|
+ try {
|
|
|
+ const audioInput = await this.getDeaultAudio();
|
|
|
+ if (audioInput) {
|
|
|
+ this.audioInput = audioInput;
|
|
|
+ }
|
|
|
+ this.debug && console.log('audioInput', audioInput);
|
|
|
+ console.log('this.canvasElement', this.canvasElement);
|
|
|
+ let defaultFrame = 30;
|
|
|
+ if (typeof this.platformConfig.frameRate == 'number') {
|
|
|
+ defaultFrame = this.platformConfig.frameRate;
|
|
|
+ }
|
|
|
+ const stream = this.canvasElement.captureStream(defaultFrame);
|
|
|
+ // debugger;
|
|
|
+ if (stream) {
|
|
|
+ const videoTrack = stream.getVideoTracks()[0];
|
|
|
+ if (videoTrack) {
|
|
|
+ videoTrack.applyConstraints({
|
|
|
+ advanced: [
|
|
|
+ {
|
|
|
+ width: videoConstraints.value.width,
|
|
|
+ height: videoConstraints.value.height,
|
|
|
+ frameRate: videoConstraints.value.frameRate,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return resolve(stream);
|
|
|
+ }
|
|
|
+ return resolve(null);
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ return resolve(null);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ getEletronDisplayMedia() {
|
|
|
+ return new Promise(async (resolve) => {
|
|
|
+ try {
|
|
|
+ const audioInput = await this.getEletronDeaultAudio();
|
|
|
+ if (audioInput) {
|
|
|
+ this.audioInput = audioInput;
|
|
|
+ }
|
|
|
+ console.log('eletron-audioInput', audioInput);
|
|
|
+ if (navigator.mediaDevices.getDisplayMedia) {
|
|
|
+ const videoConfig = {
|
|
|
+ mandatory: {
|
|
|
+ chromeMediaSource: 'desktop',
|
|
|
+ chromeMediaSourceId: this.chromeMediaSourceId,
|
|
|
+ minWidth: this.platformConfig.minWidth || 1280,
|
|
|
+ maxWidth: this.platformConfig.maxWidth || 3840,
|
|
|
+ minHeight: this.platformConfig.minHeight || 720,
|
|
|
+ maxHeight: this.platformConfig.maxHeight || 2160,
|
|
|
+ maxFrameRate: this.platformConfig.maxFrameRate || 30,
|
|
|
+ minFrameRate: this.platformConfig.minFrameRate || 30,
|
|
|
+ },
|
|
|
+ };
|
|
|
+ this.debug && console.log('videoConfig', videoConfig);
|
|
|
+ const res = await navigator.mediaDevices.getUserMedia({
|
|
|
+ audio: false,
|
|
|
+ video: videoConfig,
|
|
|
+ });
|
|
|
+ return resolve(res);
|
|
|
+ }
|
|
|
+ return resolve(null);
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ return resolve(null);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ getDisplayMedia() {
|
|
|
+ return new Promise(async (resolve) => {
|
|
|
+ try {
|
|
|
+ const audioInput = await this.getDeaultAudio();
|
|
|
+ if (audioInput) {
|
|
|
+ this.audioInput = audioInput;
|
|
|
+ }
|
|
|
+ this.debug && console.log('audioInput', audioInput);
|
|
|
+ if (navigator.mediaDevices.getDisplayMedia) {
|
|
|
+ if (this.debug) {
|
|
|
+ console.log('displayMediaStreamConstraints', this.displayMediaStreamConstraints);
|
|
|
+ }
|
|
|
+ const res = await navigator.mediaDevices.getDisplayMedia(this.displayMediaStreamConstraints);
|
|
|
+ return resolve(res);
|
|
|
+ }
|
|
|
+ return resolve(null);
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ return resolve(null);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ async getDeaultAudio() {
|
|
|
+ return new Promise(async (resolve) => {
|
|
|
+ try {
|
|
|
+ const audioConfig = {
|
|
|
+ echoCancellation: true,
|
|
|
+ autoGainControl: true,
|
|
|
+ noiseSuppression: true,
|
|
|
+ latency: 0,
|
|
|
+ };
|
|
|
+ console.log('audioConfig', audioConfig);
|
|
|
+ if (navigator.mediaDevices.getUserMedia) {
|
|
|
+ const res = await navigator.mediaDevices.getUserMedia({
|
|
|
+ audio: audioConfig,
|
|
|
+ video: false,
|
|
|
+ });
|
|
|
+ return resolve(res);
|
|
|
+ }
|
|
|
+ return resolve(null);
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ return resolve(null);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ async getEletronDeaultAudio() {
|
|
|
+ return new Promise(async (resolve) => {
|
|
|
+ try {
|
|
|
+ if (navigator.mediaDevices.getUserMedia) {
|
|
|
+ const res = await navigator.mediaDevices.getUserMedia({
|
|
|
+ video: false,
|
|
|
+ audio: { deviceId: 'default' },
|
|
|
+ });
|
|
|
+ return resolve(res);
|
|
|
+ }
|
|
|
+ return resolve(null);
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ return resolve(null);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ holdRecord() {
|
|
|
+ this.isStartRecoding = false;
|
|
|
+ this.status = RecorderStatusType.hold;
|
|
|
+ this.streamStop();
|
|
|
+ }
|
|
|
+ async endRecord() {
|
|
|
+ return new Promise(async (resolve) => {
|
|
|
+ try {
|
|
|
+ this.streamStop();
|
|
|
+ await this.sleep(1000);
|
|
|
+ this.isStartRecoding = false;
|
|
|
+ this.status = RecorderStatusType.end;
|
|
|
+ const blobs = this.recordChunks.slice();
|
|
|
+ console.log('last-dump', blobs);
|
|
|
+ if (this.autoDownload) {
|
|
|
+ (blobs === null || blobs === void 0 ? void 0 : blobs.length) && this.handleAutoDownload(blobs);
|
|
|
+ }
|
|
|
+ this.emit('endRecord', blobs);
|
|
|
+ this.passiveEnd = false;
|
|
|
+ this.recordChunks = [];
|
|
|
+ resolve(blobs);
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ this.emit('endRecord', null);
|
|
|
+ resolve([]);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ // public async setMuteMode() {
|
|
|
+ // }
|
|
|
+ streamStop() {
|
|
|
+ if (this.stream) {
|
|
|
+ this.stream.getTracks().forEach((track) => track.stop());
|
|
|
+ }
|
|
|
+ if (this.audioInput) {
|
|
|
+ this.audioInput.getTracks().forEach((track) => track.stop());
|
|
|
+ }
|
|
|
+ if (this.mediaRecorder) {
|
|
|
+ this.mediaRecorder.stop();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ async createMediaRecoder() {
|
|
|
+ // let mergeSteam: MediaStream;
|
|
|
+ try {
|
|
|
+ let audioTrack, videoTrack;
|
|
|
+ if (this.audioInput) {
|
|
|
+ [videoTrack] = this.stream.getVideoTracks();
|
|
|
+ [audioTrack] = this.audioInput.getAudioTracks();
|
|
|
+ this.stream = new MediaStream([videoTrack, audioTrack]);
|
|
|
+ }
|
|
|
+ const globalAudioInstance = window.Howler;
|
|
|
+ if ((globalAudioInstance === null || globalAudioInstance === void 0 ? void 0 : globalAudioInstance.ctx) && !this.disabledAudio) {
|
|
|
+ const streamAudio = globalAudioInstance.ctx.createMediaStreamDestination();
|
|
|
+ globalAudioInstance.masterGain.connect(streamAudio);
|
|
|
+ console.log('streamDest', streamAudio);
|
|
|
+ window.streamAudio = streamAudio;
|
|
|
+ [videoTrack] = this.stream.getVideoTracks();
|
|
|
+ [audioTrack] = streamAudio.stream.getAudioTracks();
|
|
|
+ console.log('audioTrack', audioTrack);
|
|
|
+ audioTrack.onended = (event) => {
|
|
|
+ console.log('audioTrack-end', event);
|
|
|
+ this.mediaRecorder.stop();
|
|
|
+ };
|
|
|
+ this.stream = new MediaStream([videoTrack, audioTrack]);
|
|
|
+ }
|
|
|
+ const mediaRecorder = new MediaRecorder(this.stream, {
|
|
|
+ mimeType: this.codecType,
|
|
|
+ audioBitsPerSecond: videoConstraints.value.audioBitsPerSecond,
|
|
|
+ videoBitsPerSecond: videoConstraints.value.videoBitsPerSecond,
|
|
|
+ });
|
|
|
+ this.mediaRecorder = mediaRecorder;
|
|
|
+ // if (this.debug) {
|
|
|
+ window.__SIMAQ__ = mediaRecorder;
|
|
|
+ // }
|
|
|
+ this.mediaRecorder.ondataavailable = (event) => {
|
|
|
+ this.recordChunks.push(event.data);
|
|
|
+ this.emit('record', new Blob([event.data], {
|
|
|
+ type: this.codecFileType,
|
|
|
+ }));
|
|
|
+ };
|
|
|
+ this.mediaRecorder.stop = () => {
|
|
|
+ // setTimeout(() => {
|
|
|
+ // this.handleAutoDownload();
|
|
|
+ // }, 1000);
|
|
|
+ };
|
|
|
+ this.mediaRecorder.onerror = (event) => {
|
|
|
+ console.error(`onerror stream: ${event.error.name},`, event);
|
|
|
+ };
|
|
|
+ this.mediaRecorder.onstop = (event) => {
|
|
|
+ console.warn(`onstop stream:`, event);
|
|
|
+ };
|
|
|
+ this.mediaRecorder.onpause = (event) => {
|
|
|
+ console.warn(`onpause stream: `, event);
|
|
|
+ };
|
|
|
+ this.mediaRecorder.onresume = (event) => {
|
|
|
+ console.warn(`onresume stream: `, event);
|
|
|
+ };
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ console.error('error', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ handleAutoDownload(chunks) {
|
|
|
+ const downloadBlob = new Blob(chunks, {
|
|
|
+ type: this.codecFileType,
|
|
|
+ });
|
|
|
+ const url = URL.createObjectURL(downloadBlob);
|
|
|
+ const a = document.createElement('a');
|
|
|
+ document.body.appendChild(a);
|
|
|
+ a.style.display = 'none';
|
|
|
+ a.href = url;
|
|
|
+ const name = new Date().getTime();
|
|
|
+ a.download = `${name}.webm`;
|
|
|
+ a.click();
|
|
|
+ window.URL.revokeObjectURL(url);
|
|
|
+ }
|
|
|
+ updateCanvas(canvas) {
|
|
|
+ if (this.isCanvas) {
|
|
|
+ this.canvasElement = canvas;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+//# sourceMappingURL=basicSimaqRecorder.js.map
|