音视频保存方案
CouriourC Lv4

前言

相关 API: MediaRecorder、Blob
原理:追踪视频流的 track,逐段保留 blob,然后组合为 File 对象,提交到后端。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export class EnsuredInitialed {
_cached: Function[] = [];
is_loaded = false;
_messages: any = null;

then(fn: Function) {
if (!this.is_loaded) {
this._cached.push(fn);
return this;
}
fn(this._messages);
return this;
};
loaded(messages: any) {
this.is_loaded = true;
setTimeout(() => {
this._messages = messages;
this._cached.forEach((fn) => {
fn(messages);
})
})
};
destroy() {
this._cached.length = 0;
this.is_loaded = false;
};}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import {EnsuredInitialed} from "./utils";

export class StreamingRecorderLoader extends EnsuredInitialed {
_recorder: MediaRecorder | null = null;
_options: MediaRecorderOptions = {
mimeType: "video/webm;codecs=vp9",
};
/*@ts-ignore*/
_stream: MediaStream = null;
_blobs: Blob[] = [];

constructor(options: Partial<MediaRecorderOptions>) {
super();
this.#init();
Object.assign(this._options, options)
}
loaded(stream: MediaStream) {
this._recorder = new MediaRecorder(stream, this._options);
this._stream = stream;
super.loaded(this._recorder as MediaRecorder);
}
start() {
this.then(() => {
this._blobs = [];
this._recorder?.start();
})
}
stop() {
return new Promise((resolve, reject) => {
this.then(() => {
this._recorder!.onstop = resolve;
this._recorder?.stop();
})
})
}
getBlob() {
return new Blob(this._blobs, {type: this._options.mimeType});
}
geFileObj(filename: string) {
return new File([this.getBlob()], filename, {
type: this._options.mimeType,
});
}
save(filename: string) {
if (!this.is_loaded) return;
this._recorder!.onstop = (e) => {
const blob = this.getBlob();
// 创建下载链接
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename || `${Date.now()}`;
a.click();
URL.revokeObjectURL(a.href);
}
this.stop();
}
ondataavailable(e: MediaRecorderEventMap['dataavailable']) {
this._blobs.push(e.data);
}
#init() {
this.then(() => {
this._recorder?.addEventListener("dataavailable", this.ondataavailable.bind(this))
});
}}

export const streamingRecorder = new StreamingRecorderLoader({
mimeType: "video/webm;codecs=vp9",
});

代码分析

  1. 先说工具类EnsuredInitialed,这是一个确保,loaded 执行之后,then 中的才开始能够执行的工具类,这是对异步操作的一个解构方法,从而确保 stream 都能够正常执行,基本原理是判断是否加载完,加载完之后把信息存下来,在 then 的时候判断是否已经加载完了,如果没有加载完,就放入 cached 中,反之,直接执行,并获得这个消息。

  2. 再说核心截取部分,确保 loaded 中有需要 track 的 MediaStream,这个 stream 可以用于 video.srcObject 获取,MediaRecorder 提供了 start,stop, 来录制的控制开始与结束,以及 pause,resume 来暂停与恢复,有几个比较关键的事件:

  • ondataavailable
    调用它用来处理 dataavailable 事件,该事件可用于获取录制的媒体资源 (在事件的 data 属性中会提供一个可用的 Blob 对象.)

  • onstart
    用来处理 start 事件,该事件在媒体开始录制时触发(MediaRecorder.start() (en-US)).

  • onstop
    用来处理 stop 事件,该事件会在媒体录制结束时、媒体流(MediaStream)结束时、或者调用 MediaRecorder.stop() (en-US) 方法后触发。stop 的时候停止录制。同时触发 dataavailable 事件,返回一个存储 Blob 内容的录制数据。因此需要手动触发一次 stop,否则录取的信息是无效的。

MediaStreamTrack

如何理解 track ,首先要弄清楚一个概念——音轨,准确来说是轨道,音视频是以二进制存在的,输出需要在一个轨道上,有点留声机那种感觉,针放在哪个轨道放哪一个,合在一起那就混音了,因此每一个轨道也可能有多个通道,每一个通道代表了一个媒体流的最小单元,比如声音放大啊(,在这某一线段)。

在 Chromium 源码中(media_track.h,media\base\media_tracks.h),定义了 MediaTrack,方法中通过 TrackTypeToStr 反映当前是视频还是音频,而后者创建了一个 MediaTracksCollection。对其进行了拼接粘合操作。

// A few notes on sample rates of common formats:
// - AAC files are limited to 96 kHz.
// - MP3 files are limited to 48 kHz.
// - Vorbis used to be limited to 96 kHz, but no longer has that
// restriction.
// - Most PC audio hardware is limited to 192 kHz, some specialized DAC
// devices will use 768 kHz though.

展望与拓展

  • 能录制,也能做到边录边播(参考 MediaSource),也就是能截取到每一帧的信息,进行加工处理,那么就可以做很多有趣的东西了。

  • 一下是对于 ArrayBuffer Blob File 之间的关系操作总结

 评论