UNPKG

@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 封装。

210 lines (209 loc) 5.11 kB
import { EventTool as V, Log as w, recodemux as k, autoReadStream as p, file2stream as A } from "@webav/internal-utils"; class P { #e = "inactive"; get state() { return this.#e; } set state(e) { throw new Error("state is readonly"); } #s = new V(); on = this.#s.on; #o; #t; constructor(e, t = {}) { this.#o = C(e, t), this.#t = new b(this.#o.video.expectFPS); } #r = () => { }; /** * 开始录制,返回 MP4 文件流 * @param timeSlice 控制流输出数据的时间间隔,单位毫秒 * */ start(e = 500) { if (this.#e === "stopped") throw Error("AVRecorder is stopped"); w.info("AVRecorder.start recoding"); const { streams: t } = this.#o; if (t.audio == null && t.video == null) throw new Error("No available tracks in MediaStream"); const { stream: o, exit: i } = x( { timeSlice: e, ...this.#o }, this.#t, () => { this.stop(); } ); return this.#r(), this.#r = i, o; } /** * 暂停录制 */ pause() { this.#e = "paused", this.#t.pause(), this.#s.emit("stateChange", this.#e); } /** * 恢复录制 */ resume() { if (this.#e === "stopped") throw Error("AVRecorder is stopped"); this.#e = "recording", this.#t.play(), this.#s.emit("stateChange", this.#e); } /** * 停止 */ async stop() { this.#e !== "stopped" && (this.#e = "stopped", this.#r()); } } function C(s, e) { const t = { bitrate: 3e6, expectFPS: 30, videoCodec: "avc1.42E032", ...e }, { streams: o, width: i, height: c, sampleRate: d, channelCount: n } = R(s); return { video: { width: i ?? 1280, height: c ?? 720, expectFPS: t.expectFPS, codec: t.videoCodec }, audio: { codec: "aac", sampleRate: d ?? 44100, channelCount: n ?? 2 }, bitrate: t.bitrate, streams: o }; } function R(s) { const e = s.getVideoTracks()[0], t = { streams: {} }; e != null && (Object.assign(t, e.getSettings()), t.streams.video = new MediaStreamTrackProcessor({ track: e }).readable); const o = s.getAudioTracks()[0]; return o != null && (Object.assign(t, o.getSettings()), w.info("AVRecorder recording audioConf:", t), t.streams.audio = new MediaStreamTrackProcessor({ track: o }).readable), t; } class b { constructor(e) { this.expectFPS = e, this.#i = Math.floor(e * 3); } // 当前帧的偏移时间,用于计算帧的 timestamp #e = performance.now(); // 编码上一帧的时间,用于计算出当前帧的持续时长 #s = this.#e; // 用于限制 帧率 #o = 0; // 如果为true,则暂停编码数据 // 取消暂停时,需要减去 #t = !1; // 触发暂停的时间,用于计算暂停持续了多久 #r = 0; // 间隔多少帧生成一个关键帧 #i = 30; start() { this.#e = performance.now(), this.#s = this.#e; } play() { this.#t && (this.#t = !1, this.#e += performance.now() - this.#r, this.#s += performance.now() - this.#r); } pause() { this.#t || (this.#t = !0, this.#r = performance.now()); } transfromVideo(e) { const t = performance.now(), o = t - this.#e; if (this.#t || // 避免帧率超出期望太高 this.#o / o * 1e3 > this.expectFPS) { e.close(); return; } const i = new VideoFrame(e, { // timestamp 单位 微秒 timestamp: o * 1e3, duration: (t - this.#s) * 1e3 }); return this.#s = t, this.#o += 1, e.close(), { vf: i, opts: { keyFrame: this.#o % this.#i === 0 } }; } transformAudio(e) { if (this.#t) { e.close(); return; } return e; } } function x(s, e, t) { let o = null, i = null; const [c, d] = [ s.streams.video != null, s.streams.audio != null && s.audio != null ], n = k({ video: c ? { ...s.video, bitrate: s.bitrate ?? 3e6 } : null, audio: d ? s.audio : null }); let u = !1; if (c) { let r = null, l = 0; const f = (a) => { clearTimeout(l), r?.close(), r = a; const h = e.transfromVideo(a.clone()); h != null && (n.encodeVideo(h.vf, h.opts), l = self.setTimeout(() => { if (r == null) return; const S = new VideoFrame(r, { timestamp: r.timestamp + 1e6, duration: 1e6 }); f(S); }, 1e3)); }; e.start(); const T = p(s.streams.video, { onChunk: async (a) => { if (u) { a.close(); return; } f(a); }, onDone: () => { } }); o = () => { T(), clearTimeout(l), r?.close(); }; } d && (i = p(s.streams.audio, { onChunk: async (r) => { if (u) { r.close(); return; } e.transformAudio(r) != null && n.encodeAudio(r); }, onDone: () => { } })); const { stream: g, stop: v } = A( n.mp4file, s.timeSlice, () => { m(), t(); } ); function m() { u = !0, o?.(), i?.(), n.close(), v(); } return { exit: m, stream: g }; } export { P as AVRecorder }; //# sourceMappingURL=av-recorder.js.map