@@ -16,20 +16,16 @@ limitations under the License.
1616
1717import * as Recorder from 'opus-recorder' ;
1818import encoderPath from 'opus-recorder/dist/encoderWorker.min.js' ;
19- import { MatrixClient } from "matrix-js-sdk/src/client" ;
2019import { SimpleObservable } from "matrix-widget-api" ;
2120import EventEmitter from "events" ;
22- import { IEncryptedFile } from "matrix-js-sdk/src/@types/event" ;
2321import { logger } from "matrix-js-sdk/src/logger" ;
2422
2523import MediaDeviceHandler from "../MediaDeviceHandler" ;
2624import { IDestroyable } from "../utils/IDestroyable" ;
2725import { Singleflight } from "../utils/Singleflight" ;
2826import { PayloadEvent , WORKLET_NAME } from "./consts" ;
2927import { UPDATE_EVENT } from "../stores/AsyncStore" ;
30- import { Playback } from "./Playback" ;
3128import { createAudioContext } from "./compat" ;
32- import { uploadFile } from "../ContentMessages" ;
3329import { FixedRollingArray } from "../utils/FixedRollingArray" ;
3430import { clamp } from "../utils/numbers" ;
3531import mxRecorderWorkletPath from "./RecorderWorklet" ;
@@ -55,38 +51,23 @@ export enum RecordingState {
5551 Uploaded = "uploaded" ,
5652}
5753
58- export interface IUpload {
59- mxc ?: string ; // for unencrypted uploads
60- encrypted ?: IEncryptedFile ;
61- }
62-
6354export class VoiceRecording extends EventEmitter implements IDestroyable {
6455 private recorder : Recorder ;
6556 private recorderContext : AudioContext ;
6657 private recorderSource : MediaStreamAudioSourceNode ;
6758 private recorderStream : MediaStream ;
6859 private recorderWorklet : AudioWorkletNode ;
6960 private recorderProcessor : ScriptProcessorNode ;
70- private buffer = new Uint8Array ( 0 ) ; // use this.audioBuffer to access
71- private lastUpload : IUpload ;
7261 private recording = false ;
7362 private observable : SimpleObservable < IRecordingUpdate > ;
74- private amplitudes : number [ ] = [ ] ; // at each second mark, generated
75- private playback : Playback ;
63+ public amplitudes : number [ ] = [ ] ; // at each second mark, generated
7664 private liveWaveform = new FixedRollingArray ( RECORDING_PLAYBACK_SAMPLES , 0 ) ;
77-
78- public constructor ( private client : MatrixClient ) {
79- super ( ) ;
80- }
65+ public onDataAvailable : ( data : ArrayBuffer ) => void ;
8166
8267 public get contentType ( ) : string {
8368 return "audio/ogg" ;
8469 }
8570
86- public get contentLength ( ) : number {
87- return this . buffer . length ;
88- }
89-
9071 public get durationSeconds ( ) : number {
9172 if ( ! this . recorder ) throw new Error ( "Duration not available without a recording" ) ;
9273 return this . recorderContext . currentTime ;
@@ -165,13 +146,9 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
165146 encoderComplexity : 3 , // 0-10, 10 is slow and high quality.
166147 resampleQuality : 3 , // 0-10, 10 is slow and high quality
167148 } ) ;
168- this . recorder . ondataavailable = ( a : ArrayBuffer ) => {
169- const buf = new Uint8Array ( a ) ;
170- const newBuf = new Uint8Array ( this . buffer . length + buf . length ) ;
171- newBuf . set ( this . buffer , 0 ) ;
172- newBuf . set ( buf , this . buffer . length ) ;
173- this . buffer = newBuf ;
174- } ;
149+
150+ // not using EventEmitter here because it leads to detached bufferes
151+ this . recorder . ondataavailable = ( data : ArrayBuffer ) => this ?. onDataAvailable ( data ) ;
175152 } catch ( e ) {
176153 logger . error ( "Error starting recording: " , e ) ;
177154 if ( e instanceof DOMException ) { // Unhelpful DOMExceptions are common - parse them sanely
@@ -191,12 +168,6 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
191168 }
192169 }
193170
194- private get audioBuffer ( ) : Uint8Array {
195- // We need a clone of the buffer to avoid accidentally changing the position
196- // on the real thing.
197- return this . buffer . slice ( 0 ) ;
198- }
199-
200171 public get liveData ( ) : SimpleObservable < IRecordingUpdate > {
201172 if ( ! this . recording ) throw new Error ( "No observable when not recording" ) ;
202173 return this . observable ;
@@ -206,10 +177,6 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
206177 return ! ! Recorder . isRecordingSupported ( ) ;
207178 }
208179
209- public get hasRecording ( ) : boolean {
210- return this . buffer . length > 0 ;
211- }
212-
213180 private onAudioProcess = ( ev : AudioProcessingEvent ) => {
214181 this . processAudioUpdate ( ev . playbackTime ) ;
215182
@@ -251,9 +218,6 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
251218 } ;
252219
253220 public async start ( ) : Promise < void > {
254- if ( this . lastUpload || this . hasRecording ) {
255- throw new Error ( "Recording already prepared" ) ;
256- }
257221 if ( this . recording ) {
258222 throw new Error ( "Recording already in progress" ) ;
259223 }
@@ -267,7 +231,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
267231 this . emit ( RecordingState . Started ) ;
268232 }
269233
270- public async stop ( ) : Promise < Uint8Array > {
234+ public async stop ( ) : Promise < void > {
271235 return Singleflight . for ( this , "stop" ) . do ( async ( ) => {
272236 if ( ! this . recording ) {
273237 throw new Error ( "No recording to stop" ) ;
@@ -293,54 +257,16 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
293257 this . recording = false ;
294258 await this . recorder . close ( ) ;
295259 this . emit ( RecordingState . Ended ) ;
296-
297- return this . audioBuffer ;
298260 } ) ;
299261 }
300262
301- /**
302- * Gets a playback instance for this voice recording. Note that the playback will not
303- * have been prepared fully, meaning the `prepare()` function needs to be called on it.
304- *
305- * The same playback instance is returned each time.
306- *
307- * @returns {Playback } The playback instance.
308- */
309- public getPlayback ( ) : Playback {
310- this . playback = Singleflight . for ( this , "playback" ) . do ( ( ) => {
311- return new Playback ( this . audioBuffer . buffer , this . amplitudes ) ; // cast to ArrayBuffer proper;
312- } ) ;
313- return this . playback ;
314- }
315-
316263 public destroy ( ) {
317264 // noinspection JSIgnoredPromiseFromCall - not concerned about stop() being called async here
318265 this . stop ( ) ;
319266 this . removeAllListeners ( ) ;
267+ this . onDataAvailable = undefined ;
320268 Singleflight . forgetAllFor ( this ) ;
321269 // noinspection JSIgnoredPromiseFromCall - not concerned about being called async here
322- this . playback ?. destroy ( ) ;
323270 this . observable . close ( ) ;
324271 }
325-
326- public async upload ( inRoomId : string ) : Promise < IUpload > {
327- if ( ! this . hasRecording ) {
328- throw new Error ( "No recording available to upload" ) ;
329- }
330-
331- if ( this . lastUpload ) return this . lastUpload ;
332-
333- try {
334- this . emit ( RecordingState . Uploading ) ;
335- const { url : mxc , file : encrypted } = await uploadFile ( this . client , inRoomId , new Blob ( [ this . audioBuffer ] , {
336- type : this . contentType ,
337- } ) ) ;
338- this . lastUpload = { mxc, encrypted } ;
339- this . emit ( RecordingState . Uploaded ) ;
340- } catch ( e ) {
341- this . emit ( RecordingState . Ended ) ;
342- throw e ;
343- }
344- return this . lastUpload ;
345- }
346272}
0 commit comments