@pexip/media 18.4.0 → 19.0.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.
@@ -1,9 +1,10 @@
1
- import { stopMediaStream, extractConstraintsWithKeys, muteStreamTrack, } from '@pexip/media-control';
2
- import { createQueue, isEmpty } from '@pexip/utils';
1
+ import { extractConstraintsWithKeys } from '@pexip/media-control';
2
+ import { createQueue, isEmpty, assert } from '@pexip/utils';
3
3
  import { createAudioGraph, createAudioGraphProxy, createStreamSourceGraphNode, createStreamDestinationGraphNode, createAnalyzerSubscribableGraphNode, createDenoiseWorkletGraphNode, createAudioSignalDetector, createVADetector, createVoiceDetectorFromTimeData, createVoiceDetectorFromProbability, avg, } from '@pexip/media-processor';
4
+ import { PROCESSOR_LABELS } from './constants';
4
5
  import { logger } from './logger';
5
6
  import { isAudioContentHint } from './typeGuard';
6
- import { shallowCopy, wrapToJSON, applyExtendedConstraints } from './utils';
7
+ import { createMediaTrack } from './utils';
7
8
  /**
8
9
  * Fetch the wasm when it doesn't exist from the provided, otherwise do nothing
9
10
  *
@@ -58,7 +59,7 @@ audioGraphOptions, audioSignalDetectionDuration = 4.0, // 4 seconds
58
59
  clock, createNodes, denoiseParams, fftSize = 2048, // FFT size
59
60
  onAudioSignalDetected, onVoiceActivityDetected, shouldEnable, silentThreshold = 10.0 / 32767, // At least one LSB 16-bit data (compare is on absolute value).
60
61
  throttleMs = 3000, // 3 seconds
61
- scope = 'media', }) => {
62
+ label = PROCESSOR_LABELS.AudioProcessor, }) => {
62
63
  const props = {
63
64
  audioGraphOptions,
64
65
  vad: false,
@@ -86,7 +87,7 @@ scope = 'media', }) => {
86
87
  }
87
88
  catch (error) {
88
89
  logger.error({
89
- scope,
90
+ label,
90
91
  error,
91
92
  moduleURL: denoiseParams?.workletModule,
92
93
  options: denoiseParams?.workletOptions,
@@ -99,7 +100,7 @@ scope = 'media', }) => {
99
100
  }
100
101
  catch (error) {
101
102
  logger.error({
102
- scope,
103
+ label,
103
104
  error,
104
105
  url: denoiseParams?.wasmURL,
105
106
  prevWasm: props.denoiseWasm,
@@ -144,167 +145,131 @@ scope = 'media', }) => {
144
145
  props.analyzer = analyzer;
145
146
  return analyzer;
146
147
  };
147
- return async (mediaP) => {
148
- const media = await mediaP;
149
- updateFeatureProps(media.constraints?.audio, props);
148
+ return async (prevMediaTrack) => {
149
+ updateFeatureProps(prevMediaTrack.getConstraints(), props);
150
150
  const shouldProcessAudio = shouldEnable() &&
151
- !!media.stream?.getAudioTracks().length &&
152
151
  (!!onVoiceActivityDetected ||
153
152
  !!onAudioSignalDetected ||
154
- props.asd ||
155
- props.vad ||
156
- props.denoise ||
153
+ !!props.asd ||
154
+ !!props.vad ||
155
+ !!props.denoise ||
157
156
  !!createNodes);
158
- if (!media.stream?.getAudioTracks().length || !shouldProcessAudio) {
159
- return media;
157
+ if (!shouldProcessAudio || !prevMediaTrack.track) {
158
+ return prevMediaTrack;
160
159
  }
161
- try {
162
- const source = createStreamSourceGraphNode(media.stream);
163
- const destination = createStreamDestinationGraphNode();
164
- const otherNodes = createNodes?.(media) ?? [];
165
- const analyzer = createAnalyzer();
166
- const initialAudioNodeConnection = [
167
- [source, ...otherNodes, destination],
168
- [source, analyzer],
169
- ];
170
- logger.debug({ initialAudioNodeConnection, scope }, 'Initial AudioNodeConnection');
171
- const audioGraph = createAudioGraphProxy(createAudioGraph(initialAudioNodeConnection, props.audioGraphOptions), {
172
- connect: (target, args) => {
173
- logger.debug({ scope, target, args }, 'connect nodes');
174
- },
175
- disconnect: (target, args) => {
176
- logger.debug({ scope, target, args }, 'disconnect nodes');
177
- },
178
- });
179
- props.audioGraph = audioGraph;
180
- const denoiseNode = await createDenoiseNode(props.denoise);
181
- const connectDenoise = (node) => {
182
- if (node) {
183
- audioGraph.disconnect([source, ...otherNodes, destination]);
184
- audioGraph.connect([
185
- source,
186
- node,
187
- ...otherNodes,
188
- destination,
189
- ]);
190
- }
191
- };
192
- connectDenoise(denoiseNode);
193
- const tracks = [
194
- ...(destination?.node?.stream.getAudioTracks() ?? []),
195
- ...(media.stream?.getVideoTracks().map(t => t.clone()) ?? []),
196
- ];
197
- const stream = new MediaStream(tracks);
198
- const applyConstraints = applyExtendedConstraints(media, async (constraints) => {
199
- if (isEmpty(constraints.audio)) {
160
+ const source = createStreamSourceGraphNode(new MediaStream([prevMediaTrack.track]));
161
+ const destination = createStreamDestinationGraphNode();
162
+ const otherNodes = createNodes?.(prevMediaTrack.track) ?? [];
163
+ const analyzer = createAnalyzer();
164
+ const initialAudioNodeConnection = [
165
+ [source, ...otherNodes, destination],
166
+ [source, analyzer],
167
+ ];
168
+ logger.debug({ initialAudioNodeConnection, label }, 'Initial AudioNodeConnection');
169
+ const audioGraph = createAudioGraphProxy(createAudioGraph(initialAudioNodeConnection, props.audioGraphOptions), {
170
+ connect: (target, args) => {
171
+ logger.debug({ label, target, args }, 'connect nodes');
172
+ },
173
+ disconnect: (target, args) => {
174
+ logger.debug({ label, target, args }, 'disconnect nodes');
175
+ },
176
+ });
177
+ props.audioGraph = audioGraph;
178
+ const denoiseNode = await createDenoiseNode(props.denoise);
179
+ const connectDenoise = (node) => {
180
+ if (node) {
181
+ audioGraph.disconnect([source, ...otherNodes, destination]);
182
+ audioGraph.connect([source, node, ...otherNodes, destination]);
183
+ }
184
+ };
185
+ connectDenoise(denoiseNode);
186
+ const track = destination?.node?.stream.getAudioTracks().at(0) ?? null;
187
+ assert(track, 'No audio track available');
188
+ // Inherit contentHint from previous track
189
+ track.contentHint = prevMediaTrack.track.contentHint;
190
+ const release = async () => {
191
+ logger.debug({ label }, 'Release Media');
192
+ track.stop();
193
+ // Release Props
194
+ await audioGraph.release();
195
+ props.denoiseNode = undefined;
196
+ props.analyzer = undefined;
197
+ props.audioGraph = undefined;
198
+ };
199
+ const mute = (mute) => {
200
+ if (track) {
201
+ track.enabled = !mute;
202
+ }
203
+ };
204
+ return createMediaTrack({
205
+ label,
206
+ kind: 'audioinput',
207
+ constraints: prevMediaTrack.getConstraints(),
208
+ previousMediaTrack: prevMediaTrack,
209
+ input: prevMediaTrack.input,
210
+ expectedInput: prevMediaTrack.expectedInput,
211
+ mute,
212
+ track,
213
+ release,
214
+ applyConstraints: async (constraints) => {
215
+ if (isEmpty(constraints)) {
200
216
  return;
201
217
  }
202
- const features = updateFeatureProps(constraints.audio, props);
203
- logger.debug({ scope, constraints: constraints.audio, features }, 'apply audio constraints');
218
+ const features = updateFeatureProps(constraints, props);
219
+ logger.debug({ label, constraints: constraints, features }, 'apply audio constraints');
204
220
  if (isEmpty(features) ||
205
221
  ['closed', 'closing'].includes(audioGraph.state)) {
206
222
  return;
207
223
  }
208
- try {
209
- const denoiseNode = await createDenoiseNode(props.denoise);
210
- if (denoiseNode) {
211
- if (!source.hasConnectedTo(denoiseNode)) {
212
- connectDenoise(denoiseNode);
213
- }
214
- }
215
- else {
216
- if (props.denoiseNode) {
217
- audioGraph.disconnect([
218
- source,
219
- props.denoiseNode,
220
- ...otherNodes,
221
- destination,
222
- ]);
223
- audioGraph.connect([
224
- source,
225
- ...otherNodes,
226
- destination,
227
- ]);
228
- audioGraph.releaseInit(props.denoiseNode);
229
- props.denoiseNode = undefined;
230
- }
231
- }
232
- const analyzer = createAnalyzer();
233
- if (analyzer) {
234
- audioGraph.connect([source, analyzer]);
224
+ const denoiseNode = await createDenoiseNode(props.denoise);
225
+ if (denoiseNode) {
226
+ if (!source.hasConnectedTo(denoiseNode)) {
227
+ connectDenoise(denoiseNode);
235
228
  }
236
- else {
237
- if (props.analyzer) {
238
- audioGraph.disconnect([source, props.analyzer]);
239
- audioGraph.releaseInit(props.analyzer);
240
- props.analyzer = undefined;
241
- }
229
+ }
230
+ else {
231
+ if (props.denoiseNode) {
232
+ audioGraph.disconnect([
233
+ source,
234
+ props.denoiseNode,
235
+ ...otherNodes,
236
+ destination,
237
+ ]);
238
+ audioGraph.connect([
239
+ source,
240
+ ...otherNodes,
241
+ destination,
242
+ ]);
243
+ audioGraph.releaseInit(props.denoiseNode);
244
+ props.denoiseNode = undefined;
242
245
  }
243
246
  }
244
- catch (error) {
245
- if (error instanceof Error) {
246
- logger.error({
247
- scope,
248
- constraints: constraints.audio,
249
- features,
250
- }, 'failed to apply audio constraints');
247
+ const analyzer = createAnalyzer();
248
+ if (analyzer) {
249
+ audioGraph.connect([source, analyzer]);
250
+ }
251
+ else {
252
+ if (props.analyzer) {
253
+ audioGraph.disconnect([source, props.analyzer]);
254
+ audioGraph.releaseInit(props.analyzer);
255
+ props.analyzer = undefined;
251
256
  }
252
257
  }
253
- });
254
- const release = async () => {
255
- logger.debug({ scope }, 'Release Media');
256
- stopMediaStream(stream);
257
- // Release Props
258
- await audioGraph.release();
259
- await media.release();
260
- props.denoiseNode = undefined;
261
- props.analyzer = undefined;
262
- props.audioGraph = undefined;
263
- };
264
- const muteAudio = (mute) => {
265
- media.muteAudio(mute);
266
- muteStreamTrack(stream)(mute, 'audio');
267
- };
268
- const muteVideo = (mute) => {
269
- media.muteVideo(mute);
270
- muteStreamTrack(stream)(mute, 'video');
271
- };
272
- const prevGetSettings = media.getSettings;
273
- return wrapToJSON(shallowCopy(media, {
274
- stream,
275
- applyConstraints,
276
- muteAudio,
277
- muteVideo,
278
- release,
279
- getSettings: () => {
280
- const { audio, video } = prevGetSettings();
281
- const denoise = !!props.denoiseNode &&
282
- source.hasConnectedTo(props.denoiseNode);
283
- const asd = !!props.asd;
284
- const vad = !!props.vad;
285
- const contentHint = stream.getAudioTracks().at(0)
286
- ?.contentHint ?? '';
287
- const audioSettings = {
288
- denoise,
289
- asd,
290
- vad,
291
- contentHint,
292
- };
293
- const settings = {
294
- audio: audio.map(settings => ({
295
- ...settings,
296
- ...audioSettings,
297
- })),
298
- video,
299
- };
300
- logger.debug({ scope, settings: audioSettings }, 'get audio processor settings');
301
- return settings;
302
- },
303
- }));
304
- }
305
- catch (error) {
306
- logger.error({ scope, error }, 'Unable to use WebAudio, return the raw media instead');
307
- return media;
308
- }
258
+ },
259
+ getSettings: () => {
260
+ const denoise = !!props.denoiseNode &&
261
+ source.hasConnectedTo(props.denoiseNode);
262
+ const asd = !!props.asd;
263
+ const vad = !!props.vad;
264
+ const contentHint = track.contentHint;
265
+ const audioSettings = {
266
+ denoise,
267
+ asd,
268
+ vad,
269
+ contentHint,
270
+ };
271
+ return audioSettings;
272
+ },
273
+ });
309
274
  };
310
275
  };
@@ -0,0 +1,7 @@
1
+ export declare const PROCESSOR_LABELS: Readonly<{
2
+ GetUserMedia: "GetUserMedia";
3
+ VideoProcessor: "VideoProcessor";
4
+ AudioProcessor: "AudioProcessor";
5
+ AudioMixingProcessor: "AudioMixingProcessor";
6
+ }>;
7
+ export declare const GET_USER_MEDIA_TIMEOUT_MS = 10000;
@@ -0,0 +1,7 @@
1
+ export const PROCESSOR_LABELS = Object.freeze({
2
+ GetUserMedia: 'GetUserMedia',
3
+ VideoProcessor: 'VideoProcessor',
4
+ AudioProcessor: 'AudioProcessor',
5
+ AudioMixingProcessor: 'AudioMixingProcessor',
6
+ });
7
+ export const GET_USER_MEDIA_TIMEOUT_MS = 10000;
package/dist/media.d.ts CHANGED
@@ -1,8 +1,18 @@
1
- import type { MediaOptions, MediaController } from './types';
1
+ import type { IndexedDevices, MediaDeviceRequest, InputDevicePermission } from '@pexip/media-control';
2
+ import type { GetUserMediaProcess, Media, MediaController, MediaOptions, MediaSignals, MediaTrack } from './types';
3
+ export interface MediaUpdaterOptions {
4
+ getUserMedia: GetUserMediaProcess;
5
+ getCurrentDevices: () => Promise<IndexedDevices>;
6
+ shouldDiscardMedia: () => boolean;
7
+ onMediaTracksChanged: (media: Media, tracks: MediaTrack[]) => void;
8
+ getInputDevicePermission?: () => Promise<InputDevicePermission>;
9
+ signals?: MediaSignals;
10
+ }
11
+ export declare const createMediaUpdater: ({ getUserMedia, getCurrentDevices, shouldDiscardMedia: shouldDisgardMedia, onMediaTracksChanged, getInputDevicePermission, signals, }: MediaUpdaterOptions) => (constraints: MediaDeviceRequest, currentMedia: Media | undefined) => Promise<void>;
2
12
  /**
3
13
  * Create an object to interact with the media scream, which is usually used for
4
14
  * our main stream.
5
15
  *
6
16
  * @param options - @see MediaOptions
7
17
  */
8
- export declare const createMedia: ({ getMuteState, signals, mediaProcessors, getDefaultConstraints, }: MediaOptions) => MediaController;
18
+ export declare const createMedia: ({ getMuteState, signals, audioProcessors, videoProcessors, getDefaultConstraints, }: MediaOptions) => MediaController;