前言相关 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" , }; _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" , });
代码分析先说工具类EnsuredInitialed
,这是一个确保,loaded 执行之后,then 中的才开始能够执行的工具类,这是对异步操作的一个解构方法,从而确保 stream 都能够正常执行,基本原理是判断是否加载完,加载完之后把信息存下来,在 then 的时候判断是否已经加载完了,如果没有加载完,就放入 cached 中,反之,直接执行,并获得这个消息。
再说核心截取部分,确保 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,否则录取的信息是无效的。
如何理解 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.
展望与拓展