@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
JavaScript
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