videomail-client 16.2.0 → 16.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/index.js CHANGED
@@ -3620,7 +3620,7 @@ const constants = {
3620
3620
  }
3621
3621
  };
3622
3622
  var package_namespaceObject = {
3623
- rE: "16.2.0"
3623
+ rE: "16.4.0"
3624
3624
  };
3625
3625
  function isAudioEnabled(options) {
3626
3626
  return Boolean(options.audio.enabled);
@@ -7740,6 +7740,49 @@ const MEDIA_EVENTS = [
7740
7740
  "volumechange"
7741
7741
  ];
7742
7742
  const mediaEvents = MEDIA_EVENTS;
7743
+ const AUDIO_RECORDER_PROCESSOR_NAME = "videomail-pcm-recorder";
7744
+ function registerAudioRecorderProcessor(name, Processor, register) {
7745
+ class PcmRecorderProcessor extends Processor {
7746
+ process(inputs) {
7747
+ if (this.recordingId < 0) return false;
7748
+ const channel = inputs[0]?.[0];
7749
+ if (!channel || 0 === this.recordingId) return true;
7750
+ let inputOffset = 0;
7751
+ while(inputOffset < channel.length){
7752
+ const count = Math.min(this.samples.length - this.offset, channel.length - inputOffset);
7753
+ this.samples.set(channel.subarray(inputOffset, inputOffset + count), this.offset);
7754
+ this.offset += count;
7755
+ inputOffset += count;
7756
+ if (this.offset === this.samples.length) {
7757
+ const samples = this.samples;
7758
+ this.port.postMessage({
7759
+ recordingId: this.recordingId,
7760
+ samples
7761
+ }, [
7762
+ samples.buffer
7763
+ ]);
7764
+ this.samples = new Float32Array(this.offset);
7765
+ this.offset = 0;
7766
+ }
7767
+ }
7768
+ return true;
7769
+ }
7770
+ constructor(options){
7771
+ super();
7772
+ this.samples = new Float32Array(options.processorOptions.bufferSize);
7773
+ this.offset = 0;
7774
+ this.recordingId = 0;
7775
+ this.port.onmessage = (event)=>{
7776
+ const recordingId = event.data;
7777
+ if ("number" != typeof recordingId) return;
7778
+ if (!Number.isSafeInteger(recordingId) || recordingId < -1) return;
7779
+ this.recordingId = recordingId;
7780
+ this.offset = 0;
7781
+ };
7782
+ }
7783
+ }
7784
+ register(name, PcmRecorderProcessor);
7785
+ }
7743
7786
  function AudioRecorder_define_property(obj, key, value) {
7744
7787
  if (key in obj) Object.defineProperty(obj, key, {
7745
7788
  value: value,
@@ -7751,40 +7794,113 @@ function AudioRecorder_define_property(obj, key, value) {
7751
7794
  return obj;
7752
7795
  }
7753
7796
  const CHANNELS = 1;
7754
- function getAudioContextClass() {
7755
- return window.AudioContext;
7756
- }
7757
- class AudioRecorder {
7758
- hasAudioContext() {
7759
- return Boolean(getAudioContextClass()) && Boolean(this.getAudioContext());
7760
- }
7797
+ const DEFAULT_BUFFER_SIZE = 2048;
7798
+ const MIN_BUFFER_SIZE = 256;
7799
+ const MAX_BUFFER_SIZE = 16384;
7800
+ class AudioRecorder extends util_Despot {
7761
7801
  getAudioContext() {
7762
- if (!this.vcAudioContext) {
7763
- const AudioContext = getAudioContextClass();
7764
- this.vcAudioContext = new AudioContext();
7765
- }
7802
+ if (!this.vcAudioContext) this.vcAudioContext = new window.AudioContext();
7766
7803
  return this.vcAudioContext;
7767
7804
  }
7768
- onAudioProcess(e, cb) {
7805
+ getBufferSize() {
7806
+ const configuredSize = this.options.audio.bufferSize;
7807
+ const bufferSize = "auto" === configuredSize ? DEFAULT_BUFFER_SIZE : configuredSize;
7808
+ if (void 0 === bufferSize) throw error_createError({
7809
+ message: "Audio buffer size is required.",
7810
+ options: this.options
7811
+ });
7812
+ const validBufferSize = Number.isInteger(bufferSize) && bufferSize >= MIN_BUFFER_SIZE && bufferSize <= MAX_BUFFER_SIZE && is_power_of_two(bufferSize);
7813
+ if (!validBufferSize) throw error_createError({
7814
+ message: `Audio buffer size must be a power of two between ${MIN_BUFFER_SIZE} and ${MAX_BUFFER_SIZE}. The current buffer size is ${bufferSize}.`,
7815
+ options: this.options
7816
+ });
7817
+ return bufferSize;
7818
+ }
7819
+ validateVolume() {
7820
+ const { volume } = this.options.audio;
7821
+ const validVolume = Number.isFinite(volume) && volume >= 0 && volume <= 1;
7822
+ if (!validVolume) throw error_createError({
7823
+ message: `Audio volume must be between zero and one. The current volume is ${volume}.`,
7824
+ options: this.options
7825
+ });
7826
+ }
7827
+ revokeModuleUrl() {
7828
+ if (this.moduleUrl) {
7829
+ URL.revokeObjectURL(this.moduleUrl);
7830
+ this.moduleUrl = void 0;
7831
+ }
7832
+ }
7833
+ async loadWorklet(context) {
7834
+ const source = `(${registerAudioRecorderProcessor.toString()})(${JSON.stringify(AUDIO_RECORDER_PROCESSOR_NAME)}, AudioWorkletProcessor, registerProcessor);`;
7835
+ const moduleUrl = URL.createObjectURL(new Blob([
7836
+ source
7837
+ ], {
7838
+ type: "text/javascript"
7839
+ }));
7840
+ this.moduleUrl = moduleUrl;
7841
+ try {
7842
+ await context.audioWorklet.addModule(moduleUrl);
7843
+ } finally{
7844
+ if (this.moduleUrl === moduleUrl) this.revokeModuleUrl();
7845
+ }
7846
+ }
7847
+ onAudioProcess(event) {
7848
+ const callback = this.audioCallback;
7849
+ if (!callback) return;
7769
7850
  if (!this.userMedia.isRecording() || this.userMedia.isPaused()) return;
7770
- const float32Array = e.inputBuffer.getChannelData(0);
7771
- cb(new audio_sample(float32Array));
7851
+ const message = event.data;
7852
+ if ("object" != typeof message || null === message) return;
7853
+ if (!("recordingId" in message) || message.recordingId !== this.recordingId) return;
7854
+ if (!("samples" in message) || !(message.samples instanceof Float32Array)) return;
7855
+ if (0 === message.samples.length) return;
7856
+ callback(new audio_sample(message.samples));
7772
7857
  }
7773
7858
  disconnect() {
7774
- if (this.scriptProcessor) {
7775
- this.scriptProcessor.onaudioprocess = null;
7776
- this.scriptProcessor.disconnect();
7859
+ if (this.worklet) {
7860
+ this.worklet.port.onmessage = null;
7861
+ this.worklet.onprocessorerror = null;
7862
+ this.worklet.port.postMessage(-1);
7863
+ this.worklet.port.close();
7864
+ this.worklet.disconnect();
7865
+ this.worklet = void 0;
7777
7866
  }
7778
7867
  this.audioInput?.disconnect();
7779
7868
  this.volume?.disconnect();
7869
+ this.audioInput = void 0;
7870
+ this.volume = void 0;
7871
+ this.audioCallback = void 0;
7872
+ this.recordingId = 0;
7780
7873
  }
7781
- init(localMediaStream) {
7874
+ async init(localMediaStream) {
7782
7875
  this.options.logger.debug("AudioRecorder: init()");
7876
+ const initializationId = ++this.initializationId;
7783
7877
  this.disconnect();
7784
- const volume = this.getAudioContext().createGain();
7785
- this.volume = volume;
7878
+ const bufferSize = this.getBufferSize();
7879
+ this.validateVolume();
7880
+ if ("function" != typeof window.AudioContext) throw error_createError({
7881
+ message: "Audio recording requires Web Audio support.",
7882
+ options: this.options
7883
+ });
7884
+ const context = this.getAudioContext();
7885
+ if (void 0 === context.audioWorklet || "function" != typeof window.AudioWorkletNode) throw error_createError({
7886
+ message: "Audio recording requires AudioWorklet support in a secure context.",
7887
+ options: this.options
7888
+ });
7889
+ const workletReady = this.workletReady ??= this.loadWorklet(context);
7890
+ try {
7891
+ await workletReady;
7892
+ } catch (exc) {
7893
+ if (this.workletReady === workletReady) this.workletReady = void 0;
7894
+ if (initializationId !== this.initializationId) return;
7895
+ throw error_createError({
7896
+ message: "Unable to load the audio recorder worklet. Check AudioWorklet and Content Security Policy support for blob: scripts.",
7897
+ cause: exc,
7898
+ options: this.options
7899
+ });
7900
+ }
7901
+ if (initializationId !== this.initializationId) return;
7786
7902
  try {
7787
- this.audioInput = this.getAudioContext().createMediaStreamSource(localMediaStream);
7903
+ this.audioInput = context.createMediaStreamSource(localMediaStream);
7788
7904
  } catch (exc) {
7789
7905
  throw error_createError({
7790
7906
  message: "Webcam has no audio",
@@ -7792,38 +7908,69 @@ class AudioRecorder {
7792
7908
  options: this.options
7793
7909
  });
7794
7910
  }
7795
- let { bufferSize } = this.options.audio;
7796
- if ("auto" === bufferSize) bufferSize = util_getBrowser(this.options).isFirefox() ? 512 : 2048;
7797
- if (void 0 === bufferSize || !is_power_of_two(bufferSize)) throw error_createError({
7798
- message: `Audio buffer size must be a power of two. The current buffer size is ${bufferSize}.`,
7799
- options: this.options
7800
- });
7801
- if (!Number.isFinite(this.options.audio.volume) || this.options.audio.volume < 0 || this.options.audio.volume > 1) throw error_createError({
7802
- message: `Audio volume must be between zero and one. The current volume is ${this.options.audio.volume}.`,
7803
- options: this.options
7804
- });
7805
- volume.gain.value = this.options.audio.volume;
7806
- this.scriptProcessor = this.getAudioContext().createScriptProcessor(bufferSize, CHANNELS, CHANNELS);
7807
- this.audioInput.connect(volume);
7808
- volume.connect(this.scriptProcessor);
7809
- this.scriptProcessor.connect(this.getAudioContext().destination);
7911
+ try {
7912
+ this.volume = context.createGain();
7913
+ this.volume.gain.value = this.options.audio.volume;
7914
+ this.worklet = new window.AudioWorkletNode(context, AUDIO_RECORDER_PROCESSOR_NAME, {
7915
+ channelCount: CHANNELS,
7916
+ channelCountMode: "explicit",
7917
+ outputChannelCount: [
7918
+ CHANNELS
7919
+ ],
7920
+ processorOptions: {
7921
+ bufferSize
7922
+ }
7923
+ });
7924
+ this.worklet.port.onmessage = (event)=>{
7925
+ this.onAudioProcess(event);
7926
+ };
7927
+ this.worklet.onprocessorerror = ()=>{
7928
+ this.stop();
7929
+ this.emit("ERROR", {
7930
+ err: error_createError({
7931
+ message: "Audio recording processor failed.",
7932
+ options: this.options
7933
+ })
7934
+ });
7935
+ };
7936
+ this.audioInput.connect(this.volume);
7937
+ this.volume.connect(this.worklet);
7938
+ this.worklet.connect(context.destination);
7939
+ } catch (exc) {
7940
+ this.disconnect();
7941
+ throw error_createError({
7942
+ message: "Failed to connect the audio recorder.",
7943
+ exc,
7944
+ options: this.options
7945
+ });
7946
+ }
7810
7947
  }
7811
7948
  record(cb) {
7812
7949
  this.options.logger.debug("AudioRecorder: record()");
7813
- if (this.scriptProcessor) this.scriptProcessor.onaudioprocess = (e)=>{
7814
- this.onAudioProcess(e, cb);
7815
- };
7950
+ this.audioCallback = cb;
7951
+ this.updateRecordingState();
7952
+ }
7953
+ updateRecordingState() {
7954
+ if (!this.worklet) return;
7955
+ const shouldRecord = Boolean(this.audioCallback) && this.userMedia.isRecording() && !this.userMedia.isPaused();
7956
+ this.recordingId = shouldRecord ? ++this.recordingSequence : 0;
7957
+ this.worklet.port.postMessage(this.recordingId);
7816
7958
  }
7817
7959
  async resume() {
7818
- const context = this.getAudioContext();
7960
+ const context = this.vcAudioContext;
7961
+ if (!context) return;
7819
7962
  if ("running" !== context.state) await context.resume();
7820
7963
  }
7821
7964
  stop() {
7822
7965
  this.options.logger.debug("AudioRecorder: stop()");
7966
+ this.initializationId++;
7823
7967
  this.disconnect();
7824
- if (this.hasAudioContext()) this.getAudioContext().close().then(()=>{
7968
+ this.revokeModuleUrl();
7969
+ const context = this.vcAudioContext;
7970
+ this.vcAudioContext = void 0;
7971
+ this.workletReady = void 0;
7972
+ if (context && "closed" !== context.state) context.close().then(()=>{
7825
7973
  this.options.logger.debug("AudioRecorder: audio context is closed");
7826
- this.vcAudioContext = void 0;
7827
7974
  }).catch((err)=>{
7828
7975
  if (err instanceof Error) return void this.options.logger.error(error_createError({
7829
7976
  err,
@@ -7833,17 +7980,11 @@ class AudioRecorder {
7833
7980
  });
7834
7981
  }
7835
7982
  getSampleRate() {
7836
- if (this.hasAudioContext()) return this.getAudioContext().sampleRate;
7983
+ if ("function" == typeof window.AudioContext) return this.getAudioContext().sampleRate;
7837
7984
  return -1;
7838
7985
  }
7839
7986
  constructor(userMedia, options){
7840
- AudioRecorder_define_property(this, "scriptProcessor", void 0);
7841
- AudioRecorder_define_property(this, "audioInput", void 0);
7842
- AudioRecorder_define_property(this, "volume", void 0);
7843
- AudioRecorder_define_property(this, "vcAudioContext", void 0);
7844
- AudioRecorder_define_property(this, "userMedia", void 0);
7845
- AudioRecorder_define_property(this, "options", void 0);
7846
- this.options = options;
7987
+ super("AudioRecorder", options), AudioRecorder_define_property(this, "userMedia", void 0), AudioRecorder_define_property(this, "worklet", void 0), AudioRecorder_define_property(this, "audioInput", void 0), AudioRecorder_define_property(this, "volume", void 0), AudioRecorder_define_property(this, "vcAudioContext", void 0), AudioRecorder_define_property(this, "workletReady", void 0), AudioRecorder_define_property(this, "moduleUrl", void 0), AudioRecorder_define_property(this, "audioCallback", void 0), AudioRecorder_define_property(this, "initializationId", 0), AudioRecorder_define_property(this, "recordingSequence", 0), AudioRecorder_define_property(this, "recordingId", 0);
7847
7988
  this.userMedia = userMedia;
7848
7989
  }
7849
7990
  }
@@ -7989,27 +8130,33 @@ class UserMedia extends util_Despot {
7989
8130
  const fireCallbacks = ()=>{
7990
8131
  const readyState = this.rawVisualUserMedia?.readyState;
7991
8132
  this.options.logger.debug(`UserMedia: fireCallbacks(readyState=${readyState}, onPlayReached=${this.onPlayReached}, onLoadedMetaDataReached=${this.onLoadedMetaDataReached})`);
7992
- if (this.onPlayReached && this.onLoadedMetaDataReached) if (this.audioRecorder) try {
7993
- this.audioRecorder.init(localMediaStream);
7994
- const onAudioReady = ()=>{
7995
- if (initializationId !== this.initializationId || this.currentVisualStream !== localMediaStream) return;
7996
- if (this.recording) this.audioRecord(audioCallback);
7997
- else this.on("SENDING_FIRST_FRAME", ()=>{
7998
- this.audioRecord(audioCallback);
7999
- });
8000
- this.clearInitializationListeners?.();
8001
- videoCallback();
8133
+ if (this.onPlayReached && this.onLoadedMetaDataReached) if (this.audioRecorder) {
8134
+ const initializeAudio = async (audioRecorder)=>{
8135
+ try {
8136
+ await audioRecorder.init(localMediaStream);
8137
+ if (initializationId !== this.initializationId || this.currentVisualStream !== localMediaStream) return;
8138
+ const onAudioReady = ()=>{
8139
+ if (initializationId !== this.initializationId || this.currentVisualStream !== localMediaStream) return;
8140
+ if (this.recording) this.audioRecord(audioCallback);
8141
+ else this.on("SENDING_FIRST_FRAME", ()=>{
8142
+ this.audioRecord(audioCallback);
8143
+ });
8144
+ this.clearInitializationListeners?.();
8145
+ videoCallback();
8146
+ };
8147
+ if (params?.recovering) await audioRecorder.resume();
8148
+ onAudioReady();
8149
+ } catch (exc) {
8150
+ if (initializationId !== this.initializationId) return;
8151
+ unloadAllEventListeners();
8152
+ this.stop(void 0, {
8153
+ preserveRecordingState: Boolean(params?.recovering)
8154
+ });
8155
+ endedEarlyCallback(exc);
8156
+ }
8002
8157
  };
8003
- if (params?.recovering) this.audioRecorder.resume().then(onAudioReady).catch(endedEarlyCallback);
8004
- else onAudioReady();
8005
- } catch (exc) {
8006
- unloadAllEventListeners();
8007
- this.stop(void 0, {
8008
- preserveRecordingState: Boolean(params?.recovering)
8009
- });
8010
- endedEarlyCallback(exc);
8011
- }
8012
- else {
8158
+ initializeAudio(this.audioRecorder);
8159
+ } else {
8013
8160
  this.clearInitializationListeners?.();
8014
8161
  videoCallback();
8015
8162
  }
@@ -8166,15 +8313,18 @@ class UserMedia extends util_Despot {
8166
8313
  }
8167
8314
  pause() {
8168
8315
  this.paused = true;
8316
+ this.audioRecorder?.updateRecordingState();
8169
8317
  }
8170
8318
  isPaused() {
8171
8319
  return this.paused;
8172
8320
  }
8173
8321
  resume() {
8174
8322
  this.paused = false;
8323
+ this.audioRecorder?.updateRecordingState();
8175
8324
  }
8176
8325
  record() {
8177
8326
  this.recording = true;
8327
+ this.audioRecorder?.updateRecordingState();
8178
8328
  }
8179
8329
  isRecording() {
8180
8330
  return this.recording;
@@ -1,21 +1,31 @@
1
1
  import AudioSample from "audio-sample";
2
- import { VideomailClientOptions } from "../../../types/options.js";
3
- import UserMedia from "../../../wrappers/visuals/userMedia.js";
2
+ import type { VideomailClientOptions } from "../../../types/options.js";
3
+ import type UserMedia from "../../../wrappers/visuals/userMedia.js";
4
+ import Despot from "../../Despot.js";
4
5
  export type AudioProcessCB = (audioSample: AudioSample) => void;
5
- declare class AudioRecorder {
6
- private scriptProcessor?;
6
+ declare class AudioRecorder extends Despot {
7
+ private readonly userMedia;
8
+ private worklet?;
7
9
  private audioInput?;
8
10
  private volume?;
9
11
  private vcAudioContext?;
10
- private readonly userMedia;
11
- private readonly options;
12
- constructor(userMedia: UserMedia, options: VideomailClientOptions);
13
- private hasAudioContext;
12
+ private workletReady?;
13
+ private moduleUrl?;
14
+ private audioCallback?;
15
+ private initializationId;
16
+ private recordingSequence;
17
+ private recordingId;
18
+ constructor(userMedia: Pick<UserMedia, "isRecording" | "isPaused">, options: VideomailClientOptions);
14
19
  private getAudioContext;
20
+ private getBufferSize;
21
+ private validateVolume;
22
+ private revokeModuleUrl;
23
+ private loadWorklet;
15
24
  private onAudioProcess;
16
25
  private disconnect;
17
- init(localMediaStream: MediaStream): void;
26
+ init(localMediaStream: MediaStream): Promise<void>;
18
27
  record(cb: AudioProcessCB): void;
28
+ updateRecordingState(): void;
19
29
  resume(): Promise<void>;
20
30
  stop(): void;
21
31
  getSampleRate(): number;
@@ -0,0 +1,17 @@
1
+ export declare const AUDIO_RECORDER_PROCESSOR_NAME = "videomail-pcm-recorder";
2
+ export interface AudioRecorderProcessorOptions {
3
+ processorOptions: {
4
+ bufferSize: number;
5
+ };
6
+ }
7
+ export interface AudioRecorderProcessor {
8
+ process(inputs: readonly (readonly Float32Array[])[], outputs: readonly (readonly Float32Array[])[]): boolean;
9
+ }
10
+ interface AudioRecorderProcessorBase {
11
+ readonly port: {
12
+ onmessage: ((event: MessageEvent<unknown>) => void) | null;
13
+ postMessage: (message: unknown, transfer: Transferable[]) => void;
14
+ };
15
+ }
16
+ export default function registerAudioRecorderProcessor(name: string, Processor: new () => AudioRecorderProcessorBase, register: (processorName: string, processor: new (options: AudioRecorderProcessorOptions) => AudioRecorderProcessor) => void): void;
17
+ export {};