basicSimaqRecorder.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  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 interface InitConfigType extends DisplayMediaStreamConstraints {
  8. uploadUrl: '';
  9. resolution: ResolutionType;
  10. autoDownload?: boolean;
  11. isElectron?: boolean;
  12. chromeMediaSourceId?: null;
  13. debug?: boolean;
  14. }
  15. export enum RecorderStatusType {
  16. init = 0,
  17. start = 1,
  18. hold = 2,
  19. end = 3,
  20. }
  21. export class BasicSimaqRecorder extends EventEmitter {
  22. displayMediaStreamConstraints: DisplayMediaStreamConstraints = {
  23. video: videoConstraints.getValue(),
  24. audio: audioConstraints.getValue(),
  25. };
  26. private isStartRecoding = false;
  27. private stream: MediaStream;
  28. private audioInput: MediaStream;
  29. private mediaRecorder: MediaRecorder;
  30. public status: RecorderStatusType = 0;
  31. // public record = new BehaviorSubject<Blob>(null);
  32. private recordChunks: Blob[] = [];
  33. private autoDownload = false;
  34. private passiveEnd = false;
  35. private isElectron: boolean;
  36. private chromeMediaSourceId: boolean;
  37. constructor(arg: InitConfigType) {
  38. super();
  39. console.log('arg', arg);
  40. this.autoDownload = arg.autoDownload;
  41. this.isElectron = arg.isElectron;
  42. this.chromeMediaSourceId = arg.chromeMediaSourceId;
  43. videoConstraints.subscribe((value) => {
  44. console.log('subscribe', value);
  45. });
  46. }
  47. private sleep = (ms) => new Promise((r) => setTimeout(r, ms));
  48. public async startRecord(): Promise<void> {
  49. if (!this.isStartRecoding) {
  50. console.log('开始录屏!', isSupport());
  51. if (!isSupport()) {
  52. console.error('当前浏览器不支持录屏或不存在https环境');
  53. return;
  54. }
  55. const media = this.isElectron
  56. ? await this.getEletronDisplayMedia()
  57. : await this.getDisplayMedia();
  58. console.log('media', media);
  59. if (media) {
  60. this.emit('startRecord');
  61. this.isStartRecoding = true;
  62. this.status = RecorderStatusType.start;
  63. // console.log('media', media);
  64. const video: HTMLVideoElement = getVideo();
  65. if (video) {
  66. // console.log('video', video);
  67. video.srcObject = media;
  68. this.stream = media;
  69. this.createMediaRecoder();
  70. this.mediaRecorder.start();
  71. this.stream.getVideoTracks()[0].onended = () => {
  72. console.log('stop-share');
  73. this.endRecord();
  74. };
  75. }
  76. } else {
  77. this.streamStop();
  78. this.isStartRecoding = false;
  79. this.status = RecorderStatusType.end;
  80. this.emit('cancelRecord');
  81. }
  82. }
  83. }
  84. private getEletronDisplayMedia(): Promise<MediaStream | null> {
  85. return new Promise(async (resolve) => {
  86. try {
  87. const audioInput = await this.getDeaultAudio();
  88. if (audioInput) {
  89. this.audioInput = audioInput;
  90. }
  91. console.log('audioInput', audioInput);
  92. if (navigator.mediaDevices.getDisplayMedia) {
  93. const res = await navigator.mediaDevices.getUserMedia({
  94. audio: false,
  95. video: {
  96. mandatory: {
  97. chromeMediaSource: 'desktop',
  98. chromeMediaSourceId: this.chromeMediaSourceId,
  99. minWidth: 1280,
  100. maxWidth: 1280,
  101. minHeight: 720,
  102. maxHeight: 720,
  103. },
  104. },
  105. } as any as MediaStreamConstraints);
  106. return resolve(res);
  107. }
  108. return resolve(null);
  109. } catch (error) {
  110. return resolve(null);
  111. }
  112. });
  113. }
  114. private getDisplayMedia(): Promise<MediaStream | null> {
  115. return new Promise(async (resolve) => {
  116. try {
  117. const audioInput = await this.getDeaultAudio();
  118. if (audioInput) {
  119. this.audioInput = audioInput;
  120. }
  121. console.log('audioInput', audioInput);
  122. if (navigator.mediaDevices.getDisplayMedia) {
  123. const res = await navigator.mediaDevices.getDisplayMedia(
  124. this.displayMediaStreamConstraints,
  125. );
  126. return resolve(res);
  127. }
  128. return resolve(null);
  129. } catch (error) {
  130. return resolve(null);
  131. }
  132. });
  133. }
  134. private async getDeaultAudio(): Promise<MediaStream> {
  135. return new Promise(async (resolve) => {
  136. try {
  137. if (navigator.mediaDevices.getUserMedia) {
  138. const res = await navigator.mediaDevices.getUserMedia({
  139. audio: true,
  140. video: false,
  141. });
  142. return resolve(res);
  143. }
  144. return resolve(null);
  145. } catch (error) {
  146. return resolve(null);
  147. }
  148. });
  149. }
  150. public holdRecord(): void {
  151. this.isStartRecoding = false;
  152. this.status = RecorderStatusType.hold;
  153. this.streamStop();
  154. }
  155. public async endRecord(): Promise<Blob[]> {
  156. return new Promise<Blob[]>(async (resolve) => {
  157. try {
  158. this.streamStop();
  159. await this.sleep(1000);
  160. this.isStartRecoding = false;
  161. this.status = RecorderStatusType.end;
  162. const blobs = this.recordChunks.slice();
  163. console.log('last-dump', blobs);
  164. if (this.autoDownload) {
  165. blobs?.length && this.handleAutoDownload(blobs);
  166. }
  167. this.emit('endRecord', blobs);
  168. this.passiveEnd = false;
  169. this.recordChunks = [];
  170. resolve(blobs);
  171. } catch (error) {
  172. resolve([]);
  173. }
  174. });
  175. }
  176. private streamStop(): void {
  177. if (this.stream) {
  178. this.stream.getTracks().forEach((track) => track.stop());
  179. }
  180. if (this.audioInput) {
  181. this.audioInput.getTracks().forEach((track) => track.stop());
  182. }
  183. if (this.mediaRecorder) {
  184. this.mediaRecorder.stop();
  185. }
  186. }
  187. private createMediaRecoder(): void {
  188. console.log('video-flag', videoConstraints.value);
  189. // let mergeSteam: MediaStream;
  190. let audioTrack: MediaStreamTrack, videoTrack: MediaStreamTrack;
  191. if (this.audioInput) {
  192. [videoTrack] = this.stream.getVideoTracks();
  193. [audioTrack] = this.audioInput.getAudioTracks();
  194. this.stream = new MediaStream([videoTrack, audioTrack]);
  195. }
  196. const mediaRecorder = new MediaRecorder(this.stream, {
  197. mimeType: 'video/webm;codecs=H264,opus',
  198. audioBitsPerSecond: videoConstraints.value.audioBitsPerSecond,
  199. videoBitsPerSecond: videoConstraints.value.videoBitsPerSecond,
  200. });
  201. this.mediaRecorder = mediaRecorder;
  202. this.mediaRecorder.ondataavailable = (event) => {
  203. this.recordChunks.push(event.data);
  204. this.emit(
  205. 'record',
  206. new Blob([event.data], {
  207. type: 'video/mp4; codecs=h264',
  208. }),
  209. );
  210. };
  211. this.mediaRecorder.stop = () => {
  212. // setTimeout(() => {
  213. // this.handleAutoDownload();
  214. // }, 1000);
  215. };
  216. }
  217. private handleAutoDownload(chunks: Blob[]): void {
  218. const downloadBlob = new Blob(chunks, {
  219. type: 'video/mp4; codecs=h264',
  220. });
  221. const url = URL.createObjectURL(downloadBlob);
  222. const a: HTMLAnchorElement = document.createElement('a');
  223. document.body.appendChild(a);
  224. a.style.display = 'none';
  225. a.href = url;
  226. a.download = 'test.mp4';
  227. a.click();
  228. window.URL.revokeObjectURL(url);
  229. }
  230. private uploadToServer(): void {}
  231. }