UNPKG

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

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