66 */
77
88import { ArrayBufferTarget , Muxer } from 'mp4-muxer' ;
9+ import { bufferToI16Interleaved , getVideoEncoderConfigs , resampleBuffer } from '../utils' ;
10+ import { OpusEncoder } from './opus' ;
11+ import { toOpusSampleRate } from './utils' ;
912import { ExportError } from '../errors' ;
10- import * as utils from '../utils' ;
1113
1214import type { frame } from '../types' ;
13- import type { EncoderOptions } from './config.types' ;
14-
15- type CanvasEncoderOptions = Omit < EncoderOptions , 'resolution' | 'debug' > ;
15+ import type { EncoderInit } from './interfaces' ;
1616
1717/**
1818 * Generic encoder that allows you to encode
1919 * a canvas frame by frame
2020 */
21- export class CanvasEncoder implements Required < CanvasEncoderOptions > {
21+ export class CanvasEncoder implements Required < EncoderInit > {
2222 private canvas : HTMLCanvasElement | OffscreenCanvas ;
2323 private muxer ?: Muxer < ArrayBufferTarget > ;
2424 private videoEncoder ?: VideoEncoder ;
25- private audioEncoder ?: AudioEncoder ;
26- private onready ?: ( ) => void ;
2725
2826 public frame : frame = 0 ;
29- public sampleRate : number ; // 44100
30- public numberOfChannels : number ; // 2
31- public videoBitrate : number // 10e6
32- public gpuBatchSize : number ; // 5
33- public fps : number ; // 30
27+ public sampleRate : number ;
28+ public numberOfChannels : number ;
29+ public videoBitrate : number ;
30+ public gpuBatchSize : number ;
31+ public fps : number ;
3432
3533 public height : number ;
3634 public width : number ;
37-
38- private encodedAudio = false ;
35+ public audio : boolean ;
3936
4037 /**
4138 * Create a new Webcodecs encoder
4239 * @param canvas - The canvas to encode
43- * @param options - Configure the output
40+ * @param init - Configure the output
4441 * @example
45- * ```typescript
42+ * ```
4643 * const encoder = new CanvasEncoder(canvas, { fps: 60 });
4744 * ```
4845 */
49- public constructor ( canvas : HTMLCanvasElement | OffscreenCanvas , options ?: CanvasEncoderOptions ) {
46+ public constructor ( canvas : HTMLCanvasElement | OffscreenCanvas , init ?: EncoderInit ) {
5047 this . canvas = canvas ;
5148 this . width = canvas . width ;
5249 this . height = canvas . height ;
53- this . fps = options ?. fps ?? 30 ;
54- this . sampleRate = options ?. sampleRate ?? 44100 ;
55- this . numberOfChannels = options ?. numberOfChannels ?? 2 ;
56- this . videoBitrate = options ?. videoBitrate ?? 10e6 ;
57- this . gpuBatchSize = options ?. gpuBatchSize ?? 5 ;
58-
59- this . init ( ) ;
50+ this . fps = init ?. fps ?? 30 ;
51+ this . sampleRate = toOpusSampleRate ( init ?. sampleRate ?? 48000 ) ;
52+ this . numberOfChannels = init ?. numberOfChannels ?? 2 ;
53+ this . videoBitrate = init ?. videoBitrate ?? 10e6 ;
54+ this . gpuBatchSize = init ?. gpuBatchSize ?? 5 ;
55+ this . audio = init ?. audio ?? false ;
6056 }
6157
6258 /**
@@ -65,29 +61,23 @@ export class CanvasEncoder implements Required<CanvasEncoderOptions> {
6561 */
6662 private async init ( ) {
6763 // First check whether web codecs are supported
68- const [ video , audio ] = await utils . getSupportedEncoderConfigs ( {
69- video : {
70- height : Math . round ( this . height ) ,
71- width : Math . round ( this . width ) ,
72- bitrate : this . videoBitrate ,
73- fps : this . fps ,
74- } ,
75- audio : {
76- sampleRate : this . sampleRate ,
77- numberOfChannels : this . numberOfChannels ,
78- bitrate : 128_000 , // 128 kbps
79- }
64+ const configs = await getVideoEncoderConfigs ( {
65+ height : Math . round ( this . height ) ,
66+ width : Math . round ( this . width ) ,
67+ bitrate : this . videoBitrate ,
68+ fps : this . fps ,
8069 } ) ;
8170
8271 this . muxer = new Muxer ( {
8372 target : new ArrayBufferTarget ( ) ,
84- video : { ...video , codec : 'avc' } ,
73+ video : { ...configs [ 0 ] , codec : 'avc' } ,
8574 firstTimestampBehavior : 'offset' ,
8675 fastStart : 'in-memory' ,
87- audio : {
88- ...audio ,
89- codec : audio . codec == 'opus' ? 'opus' : 'aac' ,
90- } ,
76+ audio : this . audio ? {
77+ numberOfChannels : this . numberOfChannels ,
78+ sampleRate : this . sampleRate ,
79+ codec : 'opus' ,
80+ } : undefined ,
9181 } ) ;
9282
9383 const init : VideoEncoderInit = {
@@ -98,18 +88,7 @@ export class CanvasEncoder implements Required<CanvasEncoderOptions> {
9888 } ;
9989
10090 this . videoEncoder = new VideoEncoder ( init ) ;
101- this . videoEncoder . configure ( video ) ;
102-
103- this . audioEncoder = new AudioEncoder ( {
104- output : ( chunk , meta ) => {
105- meta && this . muxer ?. addAudioChunk ( chunk , meta ) ;
106- } ,
107- error : console . error ,
108- } ) ;
109-
110- this . audioEncoder . configure ( audio ) ;
111-
112- this . onready ?.( )
91+ this . videoEncoder . configure ( configs [ 0 ] ) ;
11392 }
11493
11594 /**
@@ -118,7 +97,7 @@ export class CanvasEncoder implements Required<CanvasEncoderOptions> {
11897 * @returns {Promise<void> } - A promise that resolves when the frame has been encoded
11998 */
12099 public async encodeVideo ( canvas ?: HTMLCanvasElement | OffscreenCanvas ) : Promise < void > {
121- if ( ! this . videoEncoder ) await this . ready ( ) ;
100+ if ( ! this . videoEncoder ) await this . init ( ) ;
122101
123102 if ( this . videoEncoder ! . encodeQueueSize > this . gpuBatchSize ) {
124103 await new Promise ( ( resolve ) => {
@@ -142,40 +121,41 @@ export class CanvasEncoder implements Required<CanvasEncoderOptions> {
142121 * @returns {Promise<void> } - A promise that resolves when the audio has been added to the encoder queue
143122 */
144123 public async encodeAudio ( buffer : AudioBuffer ) : Promise < void > {
145- if ( ! this . audioEncoder ) await this . ready ( ) ;
124+ if ( ! this . muxer ) await this . init ( ) ;
146125
147- if ( buffer . sampleRate != this . sampleRate || buffer . numberOfChannels != this . numberOfChannels ) {
148- buffer = utils . resampleBuffer ( buffer , this . sampleRate , this . numberOfChannels ) ;
149- }
126+ const data = resampleBuffer ( buffer , this . sampleRate , this . numberOfChannels ) ;
150127
151- this . audioEncoder ?. encode (
152- new AudioData ( {
153- format : 'f32-planar' ,
154- sampleRate : buffer . sampleRate ,
155- numberOfChannels : buffer . numberOfChannels ,
156- numberOfFrames : buffer . length ,
157- timestamp : 0 ,
158- data : utils . bufferToF32Planar ( buffer ) ,
159- } )
160- ) ;
128+ const encoder = new OpusEncoder ( {
129+ output : ( chunk , meta ) => {
130+ this . muxer ?. addAudioChunkRaw (
131+ chunk . data ,
132+ chunk . type ,
133+ chunk . timestamp ,
134+ chunk . duration ,
135+ meta
136+ ) ;
137+ } ,
138+ error : console . error ,
139+ } ) ;
161140
162- this . encodedAudio = true ;
141+ await encoder . configure ( {
142+ numberOfChannels : this . numberOfChannels ,
143+ sampleRate : this . sampleRate ,
144+ } ) ;
145+
146+ encoder . encode ( {
147+ data : bufferToI16Interleaved ( data ) ,
148+ numberOfFrames : data . length ,
149+ } ) ;
163150 }
164151
165152 /**
166153 * Finalizes the rendering process and exports the result as an MP4
167154 * @returns {Promise<Blob> } - The rendered video as a Blob
168155 */
169- public async export ( ) : Promise < Blob > {
156+ public async finalize ( ) : Promise < Blob > {
170157 // encode empty buffer
171- if ( ! this . encodedAudio ) {
172- const args = [ this . numberOfChannels , 1 , this . sampleRate ] as const ;
173- const context = new OfflineAudioContext ( ...args ) ;
174- await this . encodeAudio ( context . createBuffer ( ...args ) ) ;
175- }
176-
177158 await this . videoEncoder ?. flush ( ) ;
178- await this . audioEncoder ?. flush ( ) ;
179159
180160 this . muxer ?. finalize ( ) ;
181161
@@ -192,12 +172,9 @@ export class CanvasEncoder implements Required<CanvasEncoderOptions> {
192172 }
193173
194174 /**
195- * Wait until the encoder is ready
196- * @returns {Promise<void> } - A promise that resolves when the encoder is ready
175+ * @deprecated use `finalize` instead
197176 */
198- private async ready ( ) {
199- await new Promise < void > ( ( resolve ) => {
200- this . onready = ( ) => resolve ( ) ;
201- } )
177+ public async export ( ) : Promise < Blob > {
178+ return this . finalize ( ) ;
202179 }
203180}
0 commit comments