basicSimaqRecorder.ts 9.5 KB

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