@webav/av-recorder
Version:
Record MediaStream to MP4, Use Webcodecs API encode VideoFrame and AudioData, [mp4box.js](https://github.com/gpac/mp4box.js) as muxer. 录制 MediaStream 到 MP4,使用 Webcodecs API 编码 VideoFrame、AudioData,mp4box.js 封装。
1 lines • 13.9 kB
Source Map (JSON)
{"version":3,"file":"av-recorder.umd.cjs","sources":["../src/av-recorder.ts"],"sourcesContent":["import {\n Log,\n recodemux,\n autoReadStream,\n EventTool,\n file2stream,\n} from '@webav/internal-utils';\nimport {\n AVRecorderConf,\n IStream,\n IRecordeOpts as IRecordOpts,\n TClearFn,\n} from './types';\n\ntype TState = 'inactive' | 'recording' | 'paused' | 'stopped';\n\n/**\n * 录制媒体流 MediaStream,生成 MP4 文件流\n *\n * 如果你期望录制为 WebM 格式,请使用 [MediaRecorder](https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder)\n *\n * @example\n * const recorder = new AVRecorder(\n * await navigator.mediaDevices.getUserMedia({\n * video: true,\n * audio: true,\n * })\n);\n\nrecorder.start() // => ReadableStream\n * @see [录制摄像头](https://webav-tech.github.io/WebAV/demo/4_1-recorder-usermedia)\n */\nexport class AVRecorder {\n #state: TState = 'inactive';\n get state(): TState {\n return this.#state;\n }\n set state(_: TState) {\n throw new Error('state is readonly');\n }\n\n #evtTool = new EventTool<{\n stateChange: (state: TState) => void;\n }>();\n on = this.#evtTool.on;\n\n #conf: Omit<IRecordOpts, 'timeSlice'>;\n\n #recoderPauseCtrl: RecoderPauseCtrl;\n\n constructor(inputMediaStream: MediaStream, conf: AVRecorderConf = {}) {\n this.#conf = createRecoderConf(inputMediaStream, conf);\n this.#recoderPauseCtrl = new RecoderPauseCtrl(this.#conf.video.expectFPS);\n }\n\n #stopStream = () => {};\n /**\n * 开始录制,返回 MP4 文件流\n * @param timeSlice 控制流输出数据的时间间隔,单位毫秒\n *\n */\n start(timeSlice: number = 500): ReadableStream<Uint8Array> {\n if (this.#state === 'stopped') throw Error('AVRecorder is stopped');\n Log.info('AVRecorder.start recoding');\n\n const { streams } = this.#conf;\n\n if (streams.audio == null && streams.video == null) {\n throw new Error('No available tracks in MediaStream');\n }\n\n const { stream, exit } = startRecord(\n { timeSlice, ...this.#conf },\n this.#recoderPauseCtrl,\n () => {\n this.stop();\n },\n );\n this.#stopStream();\n this.#stopStream = exit;\n return stream;\n }\n\n /**\n * 暂停录制\n */\n pause(): void {\n this.#state = 'paused';\n this.#recoderPauseCtrl.pause();\n this.#evtTool.emit('stateChange', this.#state);\n }\n /**\n * 恢复录制\n */\n resume(): void {\n if (this.#state === 'stopped') throw Error('AVRecorder is stopped');\n this.#state = 'recording';\n this.#recoderPauseCtrl.play();\n this.#evtTool.emit('stateChange', this.#state);\n }\n\n /**\n * 停止\n */\n async stop(): Promise<void> {\n if (this.#state === 'stopped') return;\n this.#state = 'stopped';\n\n this.#stopStream();\n }\n}\n\nfunction createRecoderConf(inputMS: MediaStream, userConf: AVRecorderConf) {\n const conf = {\n bitrate: 3e6,\n expectFPS: 30,\n videoCodec: 'avc1.42E032',\n ...userConf,\n };\n const { streams, width, height, sampleRate, channelCount } =\n extractMSSettings(inputMS);\n\n const opts: Omit<IRecordOpts, 'timeSlice'> = {\n video: {\n width: width ?? 1280,\n height: height ?? 720,\n expectFPS: conf.expectFPS,\n codec: conf.videoCodec,\n },\n audio: {\n codec: 'aac',\n sampleRate: sampleRate ?? 44100,\n channelCount: channelCount ?? 2,\n },\n bitrate: conf.bitrate,\n streams,\n };\n return opts;\n}\n\nfunction extractMSSettings(inputMS: MediaStream) {\n const videoTrack = inputMS.getVideoTracks()[0];\n const settings: MediaTrackSettings & { streams: IStream } = { streams: {} };\n if (videoTrack != null) {\n Object.assign(settings, videoTrack.getSettings());\n settings.streams.video = new MediaStreamTrackProcessor({\n track: videoTrack,\n }).readable;\n }\n\n const audioTrack = inputMS.getAudioTracks()[0];\n if (audioTrack != null) {\n Object.assign(settings, audioTrack.getSettings());\n Log.info('AVRecorder recording audioConf:', settings);\n settings.streams.audio = new MediaStreamTrackProcessor({\n track: audioTrack,\n }).readable;\n }\n\n return settings;\n}\n\nclass RecoderPauseCtrl {\n // 当前帧的偏移时间,用于计算帧的 timestamp\n #offsetTime = performance.now();\n\n // 编码上一帧的时间,用于计算出当前帧的持续时长\n #lastTime = this.#offsetTime;\n\n // 用于限制 帧率\n #frameCnt = 0;\n\n // 如果为true,则暂停编码数据\n // 取消暂停时,需要减去\n #paused = false;\n\n // 触发暂停的时间,用于计算暂停持续了多久\n #pauseTime = 0;\n\n // 间隔多少帧生成一个关键帧\n #gopSize = 30;\n\n constructor(readonly expectFPS: number) {\n this.#gopSize = Math.floor(expectFPS * 3);\n }\n\n start() {\n this.#offsetTime = performance.now();\n this.#lastTime = this.#offsetTime;\n }\n\n play() {\n if (!this.#paused) return;\n this.#paused = false;\n\n this.#offsetTime += performance.now() - this.#pauseTime;\n this.#lastTime += performance.now() - this.#pauseTime;\n }\n\n pause() {\n if (this.#paused) return;\n this.#paused = true;\n this.#pauseTime = performance.now();\n }\n\n transfromVideo(frame: VideoFrame) {\n const now = performance.now();\n const offsetTime = now - this.#offsetTime;\n if (\n this.#paused ||\n // 避免帧率超出期望太高\n (this.#frameCnt / offsetTime) * 1000 > this.expectFPS\n ) {\n frame.close();\n return;\n }\n\n const vf = new VideoFrame(frame, {\n // timestamp 单位 微秒\n timestamp: offsetTime * 1000,\n duration: (now - this.#lastTime) * 1000,\n });\n this.#lastTime = now;\n\n this.#frameCnt += 1;\n frame.close();\n return {\n vf,\n opts: { keyFrame: this.#frameCnt % this.#gopSize === 0 },\n };\n }\n\n transformAudio(ad: AudioData) {\n if (this.#paused) {\n ad.close();\n return;\n }\n return ad;\n }\n}\n\nfunction startRecord(\n opts: IRecordOpts,\n ctrl: RecoderPauseCtrl,\n onEnded: TClearFn,\n) {\n let stopEncodeVideo: TClearFn | null = null;\n let stopEncodeAudio: TClearFn | null = null;\n\n const [hasVideoTrack, hasAudioTrack] = [\n opts.streams.video != null,\n opts.streams.audio != null && opts.audio != null,\n ];\n\n const recoder = recodemux({\n video: hasVideoTrack\n ? { ...opts.video, bitrate: opts.bitrate ?? 3_000_000 }\n : null,\n audio: hasAudioTrack ? opts.audio : null,\n });\n\n let stoped = false;\n if (hasVideoTrack) {\n let lastVf: VideoFrame | null = null;\n let autoInsertVFTimer = 0;\n const emitVf = (vf: VideoFrame) => {\n clearTimeout(autoInsertVFTimer);\n\n lastVf?.close();\n lastVf = vf;\n const vfWrap = ctrl.transfromVideo(vf.clone());\n if (vfWrap == null) return;\n recoder.encodeVideo(vfWrap.vf, vfWrap.opts);\n\n // 录制静态画面,MediaStream 不出帧时,每秒插入一帧\n autoInsertVFTimer = self.setTimeout(() => {\n if (lastVf == null) return;\n const newVf = new VideoFrame(lastVf, {\n timestamp: lastVf.timestamp + 1e6,\n duration: 1e6,\n });\n emitVf(newVf);\n }, 1000);\n };\n\n ctrl.start();\n const stopReadStream = autoReadStream(opts.streams.video!, {\n onChunk: async (chunk: VideoFrame) => {\n if (stoped) {\n chunk.close();\n return;\n }\n emitVf(chunk);\n },\n onDone: () => {},\n });\n\n stopEncodeVideo = () => {\n stopReadStream();\n clearTimeout(autoInsertVFTimer);\n lastVf?.close();\n };\n }\n\n if (hasAudioTrack) {\n stopEncodeAudio = autoReadStream(opts.streams.audio!, {\n onChunk: async (ad: AudioData) => {\n if (stoped) {\n ad.close();\n return;\n }\n const newAD = ctrl.transformAudio(ad);\n if (newAD != null) recoder.encodeAudio(ad);\n },\n onDone: () => {},\n });\n }\n\n const { stream, stop: stopStream } = file2stream(\n recoder.mp4file,\n opts.timeSlice,\n () => {\n exit();\n onEnded();\n },\n );\n\n function exit() {\n stoped = true;\n\n stopEncodeVideo?.();\n stopEncodeAudio?.();\n recoder.close();\n stopStream();\n }\n\n return { exit, stream };\n}\n"],"names":["AVRecorder","inputMediaStream","conf","__privateAdd","_state","_evtTool","EventTool","__publicField","__privateGet","_conf","_recoderPauseCtrl","_stopStream","__privateSet","createRecoderConf","RecoderPauseCtrl","_","timeSlice","Log","streams","stream","exit","startRecord","inputMS","userConf","width","height","sampleRate","channelCount","extractMSSettings","videoTrack","settings","audioTrack","expectFPS","_offsetTime","_lastTime","_frameCnt","_paused","_pauseTime","_gopSize","frame","now","offsetTime","vf","ad","opts","ctrl","onEnded","stopEncodeVideo","stopEncodeAudio","hasVideoTrack","hasAudioTrack","recoder","recodemux","stoped","lastVf","autoInsertVFTimer","emitVf","vfWrap","newVf","stopReadStream","autoReadStream","chunk","stopStream","file2stream"],"mappings":"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"}