basicSimaqRecorder.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. import { audioConstraints } from './audioConstraints';
  2. import { videoConstraints } from './videoConstraints';
  3. import { isSupport } from './isSupport';
  4. import { getVideo } from './videoElement';
  5. import { EventEmitter } from 'eventemitter3';
  6. export type ResolutionType = '1080p' | '2k' | '4k';
  7. export type PlatformType = 'web' | 'electron' | 'canvas';
  8. export interface PlatformConfigType {
  9. chromeMediaSourceId?: string | null;
  10. canvasId?: string;
  11. minWidth?: number;
  12. maxWidth?: number;
  13. minHeight?: number;
  14. maxHeight?: number;
  15. frameRate?: number;
  16. }
  17. export interface InitConfigType extends DisplayMediaStreamConstraints {
  18. uploadUrl?: string;
  19. resolution: ResolutionType;
  20. autoDownload?: boolean;
  21. isElectron?: boolean;
  22. platform?: PlatformType;
  23. config?: PlatformConfigType;
  24. debug?: boolean;
  25. }
  26. export enum RecorderStatusType {
  27. init = 0,
  28. start = 1,
  29. hold = 2,
  30. end = 3,
  31. }
  32. export class BasicSimaqRecorder extends EventEmitter {
  33. displayMediaStreamConstraints: DisplayMediaStreamConstraints = {
  34. video: videoConstraints.getValue(),
  35. audio: audioConstraints.getValue(),
  36. };
  37. private isStartRecoding = false;
  38. private stream: MediaStream;
  39. private audioInput: MediaStream;
  40. private mediaRecorder: MediaRecorder;
  41. public status: RecorderStatusType = 0;
  42. // public record = new BehaviorSubject<Blob>(null);
  43. private recordChunks: Blob[] = [];
  44. private autoDownload = false;
  45. private passiveEnd = false;
  46. private platform: string;
  47. private uploadUrl: string;
  48. private canvasId: string;
  49. private platformConfig: PlatformConfigType;
  50. private chromeMediaSourceId: string | null;
  51. constructor(arg: InitConfigType) {
  52. super();
  53. console.log('arg', arg);
  54. this.autoDownload = arg.autoDownload;
  55. this.platform = arg.platform;
  56. this.platformConfig = arg.config;
  57. this.uploadUrl = arg.uploadUrl;
  58. this.initParams(arg);
  59. videoConstraints.subscribe((value) => {
  60. console.log('subscribe', value);
  61. });
  62. }
  63. private sleep = (ms) => new Promise((r) => setTimeout(r, ms));
  64. private get isElectron(): boolean {
  65. return this.platform === 'electron';
  66. }
  67. private get isWeb(): boolean {
  68. return this.platform === 'web';
  69. }
  70. private get isCanvas(): boolean {
  71. return this.platform === 'canvas';
  72. }
  73. private get canvasElement(): HTMLCanvasElement {
  74. // return document.getElementById(this.canvasId);
  75. return document.querySelector(this.canvasId);
  76. }
  77. private set canvasElement(canvas) {
  78. this.canvasElement = canvas;
  79. }
  80. private initParams(arg: InitConfigType): void {
  81. switch (arg.platform) {
  82. case 'web':
  83. break;
  84. case 'electron':
  85. this.chromeMediaSourceId = arg.config.chromeMediaSourceId;
  86. break;
  87. case 'canvas':
  88. this.canvasId = arg.config.canvasId;
  89. break;
  90. default:
  91. break;
  92. }
  93. }
  94. public async startRecord(): Promise<void> {
  95. try {
  96. if (!this.isStartRecoding) {
  97. console.log('开始录屏!', isSupport());
  98. if (!isSupport()) {
  99. console.error('当前浏览器不支持录屏或不存在https环境');
  100. return;
  101. }
  102. // const media = this.isElectron
  103. // ? await this.getEletronDisplayMedia()
  104. // : await this.getDisplayMedia();
  105. const media = await this.getDefaultMedia();
  106. console.log('media', media);
  107. if (media) {
  108. this.emit('startRecord');
  109. this.isStartRecoding = true;
  110. this.status = RecorderStatusType.start;
  111. // console.log('media', media);
  112. const video: HTMLVideoElement = getVideo();
  113. if (video) {
  114. // console.log('video', video);
  115. video.srcObject = media;
  116. this.stream = media;
  117. this.createMediaRecoder();
  118. this.mediaRecorder.start();
  119. this.stream.getVideoTracks()[0].onended = () => {
  120. console.log('stop-share');
  121. this.endRecord();
  122. };
  123. }
  124. } else {
  125. this.streamStop();
  126. this.isStartRecoding = false;
  127. this.status = RecorderStatusType.end;
  128. this.emit('cancelRecord');
  129. }
  130. }
  131. } catch (error) {
  132. console.error('startRecord::', error);
  133. }
  134. }
  135. private getDefaultMedia(): Promise<MediaStream | null> {
  136. return new Promise(async (resolve) => {
  137. switch (this.platform) {
  138. case 'web':
  139. return resolve(await this.getDisplayMedia());
  140. case 'canvas':
  141. return resolve(await this.getCanvasSteam());
  142. case 'electron':
  143. return resolve(await this.getEletronDisplayMedia());
  144. default:
  145. return resolve(await this.getDisplayMedia());
  146. }
  147. });
  148. }
  149. private getCanvasSteam(): Promise<MediaStream | null> {
  150. return new Promise(async (resolve) => {
  151. try {
  152. const audioInput = await this.getDeaultAudio();
  153. if (audioInput) {
  154. this.audioInput = audioInput;
  155. }
  156. console.log('audioInput', audioInput);
  157. console.log('this.canvasElement', this.canvasElement);
  158. const stream = this.canvasElement.captureStream(30);
  159. if (stream) {
  160. return resolve(stream);
  161. }
  162. return resolve(null);
  163. } catch (error) {
  164. return resolve(null);
  165. }
  166. });
  167. }
  168. private getEletronDisplayMedia(): Promise<MediaStream | null> {
  169. return new Promise(async (resolve) => {
  170. try {
  171. const audioInput = await this.getEletronDeaultAudio();
  172. if (audioInput) {
  173. this.audioInput = audioInput;
  174. }
  175. console.log('eletron-audioInput', audioInput);
  176. if (navigator.mediaDevices.getDisplayMedia) {
  177. const videoConfig = {
  178. mandatory: {
  179. chromeMediaSource: 'desktop',
  180. chromeMediaSourceId: this.chromeMediaSourceId,
  181. minWidth: this.platformConfig.minWidth || 1280,
  182. maxWidth: this.platformConfig.maxWidth || 3840,
  183. minHeight: this.platformConfig.minHeight || 720,
  184. maxHeight: this.platformConfig.maxHeight || 2160,
  185. },
  186. };
  187. console.log('videoConfig', videoConfig);
  188. const res = await navigator.mediaDevices.getUserMedia({
  189. audio: false,
  190. video: videoConfig,
  191. } as any as MediaStreamConstraints);
  192. return resolve(res);
  193. }
  194. return resolve(null);
  195. } catch (error) {
  196. return resolve(null);
  197. }
  198. });
  199. }
  200. private getDisplayMedia(): Promise<MediaStream | null> {
  201. return new Promise(async (resolve) => {
  202. try {
  203. const audioInput = await this.getDeaultAudio();
  204. if (audioInput) {
  205. this.audioInput = audioInput;
  206. }
  207. console.log('audioInput', audioInput);
  208. if (navigator.mediaDevices.getDisplayMedia) {
  209. const res = await navigator.mediaDevices.getDisplayMedia(
  210. this.displayMediaStreamConstraints,
  211. );
  212. return resolve(res);
  213. }
  214. return resolve(null);
  215. } catch (error) {
  216. return resolve(null);
  217. }
  218. });
  219. }
  220. private async getDeaultAudio(): Promise<MediaStream> {
  221. return new Promise(async (resolve) => {
  222. try {
  223. if (navigator.mediaDevices.getUserMedia) {
  224. const res = await navigator.mediaDevices.getUserMedia({
  225. audio: true,
  226. video: false,
  227. });
  228. return resolve(res);
  229. }
  230. return resolve(null);
  231. } catch (error) {
  232. return resolve(null);
  233. }
  234. });
  235. }
  236. private async getEletronDeaultAudio(): Promise<MediaStream> {
  237. return new Promise(async (resolve) => {
  238. try {
  239. if (navigator.mediaDevices.getUserMedia) {
  240. const res = await navigator.mediaDevices.getUserMedia({
  241. video: false,
  242. audio: { deviceId: 'default' },
  243. } as any as MediaStreamConstraints);
  244. return resolve(res);
  245. }
  246. return resolve(null);
  247. } catch (error) {
  248. return resolve(null);
  249. }
  250. });
  251. }
  252. public holdRecord(): void {
  253. this.isStartRecoding = false;
  254. this.status = RecorderStatusType.hold;
  255. this.streamStop();
  256. }
  257. public async endRecord(): Promise<Blob[]> {
  258. return new Promise<Blob[]>(async (resolve) => {
  259. try {
  260. this.streamStop();
  261. await this.sleep(1000);
  262. this.isStartRecoding = false;
  263. this.status = RecorderStatusType.end;
  264. const blobs = this.recordChunks.slice();
  265. console.log('last-dump', blobs);
  266. if (this.autoDownload) {
  267. blobs?.length && this.handleAutoDownload(blobs);
  268. }
  269. this.emit('endRecord', blobs);
  270. this.passiveEnd = false;
  271. this.recordChunks = [];
  272. resolve(blobs);
  273. } catch (error) {
  274. resolve([]);
  275. }
  276. });
  277. }
  278. private streamStop(): void {
  279. if (this.stream) {
  280. this.stream.getTracks().forEach((track) => track.stop());
  281. }
  282. if (this.audioInput) {
  283. this.audioInput.getTracks().forEach((track) => track.stop());
  284. }
  285. if (this.mediaRecorder) {
  286. this.mediaRecorder.stop();
  287. }
  288. }
  289. private createMediaRecoder(): void {
  290. console.log('video-flag', videoConstraints.value);
  291. // let mergeSteam: MediaStream;
  292. let audioTrack: MediaStreamTrack, videoTrack: MediaStreamTrack;
  293. if (this.audioInput) {
  294. [videoTrack] = this.stream.getVideoTracks();
  295. [audioTrack] = this.audioInput.getAudioTracks();
  296. this.stream = new MediaStream([videoTrack, audioTrack]);
  297. }
  298. const mediaRecorder = new MediaRecorder(this.stream, {
  299. mimeType: 'video/webm;codecs=H264,opus',
  300. audioBitsPerSecond: videoConstraints.value.audioBitsPerSecond,
  301. videoBitsPerSecond: videoConstraints.value.videoBitsPerSecond,
  302. });
  303. this.mediaRecorder = mediaRecorder;
  304. this.mediaRecorder.ondataavailable = (event) => {
  305. this.recordChunks.push(event.data);
  306. this.emit(
  307. 'record',
  308. new Blob([event.data], {
  309. type: 'video/mp4; codecs=h264',
  310. }),
  311. );
  312. };
  313. this.mediaRecorder.stop = () => {
  314. // setTimeout(() => {
  315. // this.handleAutoDownload();
  316. // }, 1000);
  317. };
  318. }
  319. private handleAutoDownload(chunks: Blob[]): void {
  320. const downloadBlob = new Blob(chunks, {
  321. type: 'video/mp4; codecs=h264',
  322. });
  323. const url = URL.createObjectURL(downloadBlob);
  324. const a: HTMLAnchorElement = document.createElement('a');
  325. document.body.appendChild(a);
  326. a.style.display = 'none';
  327. a.href = url;
  328. a.download = 'test.mp4';
  329. a.click();
  330. window.URL.revokeObjectURL(url);
  331. }
  332. public updateCanvas(canvas: HTMLCanvasElement) {
  333. if (this.isCanvas) {
  334. this.canvasElement = canvas;
  335. }
  336. }
  337. private uploadToServer(): void { }
  338. }