@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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,135 @@
1
1
  # @pexip/media
2
2
 
3
+ ## 19.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 878fcca: Decouple Audio/Video request
8
+
9
+ The library is no longer assuming the request constraints both video and
10
+ audio, and there is no retry mechanism builtin. The media request and
11
+ processing are decoupled as well to minimize the time to get the media
12
+ flowing.
13
+
14
+ Changes of `Media` object:
15
+
16
+ - `MediaTrack`-based construct: An `Media` construct is changed to base on
17
+ `MediaTrack` which is a wrapper to `MediaStreamTrack` with a linked list to
18
+ reference on different `MediaStreamTrack` produced by the processor as well
19
+ as the original source track obtained from `getUserMedia` API.
20
+ - All track related functions are implemented by `MediaTrack`, e.g.
21
+ `applyConstraints`, `getSettings` and `clone`, etc.
22
+ - `Media['getSettings']` returns
23
+ `{audio?: ExtendedMediaTrackSettings, video?: ExtendedMediaTrackSettings}`
24
+ - The following attributes/functions are added to media object:
25
+ - `active: boolean`;
26
+ - `id: string`;
27
+ - `getConstraints(): MediaDeviceRequest`;
28
+ - `setOriginalConstraints(constraints: MediaDeviceRequest): void`;
29
+ - `getOriginalConstraints(): MediaDeviceRequest`;
30
+ - `isAudioInputUnavailable(): boolean`;
31
+ - `isVideoInputUnavailable(): boolean`;
32
+ - `requestedAudio(): boolean`;
33
+ - `requestedVideo(): boolean`;
34
+ - `getTracks(): MediaTrack[]`;
35
+ - `getAudioTracks(): MediaTrack[]`;
36
+ - `getVideoTracks(): MediaTrack[]`;
37
+ - `addTrack(track: MediaTrack): void`;
38
+ - `removeTrack(track: MediaTrack): void`;
39
+ - `clone(): Media`;
40
+ - BREAKING CHANGE:
41
+ - `Media['rawStream']` is removed, and we no longer keep a reference of the
42
+ original `MediaStream`, we keep the source track instead. See
43
+ `MediaTrack['source']`.
44
+ - `Media['constraints']` is removed, please use `Media['getConstraints']` or
45
+ `Media['getOriginalConstraints']` to get the constraints.
46
+ - The `processors` parameter for `createMedia({processors, ...})` has been
47
+ split into `audioProcessors` and `videoProcessors`, i.e.
48
+ `createMedia({videoProcessors, audioProcessors, ...})`
49
+
50
+ Changes of processing pipeline:
51
+
52
+ - The pipeline is changed to based on `MediaTrack`.
53
+ - Video processing pipeline and Audio processing pipeline are run in parallel
54
+ whereas the pipeline itself is run in series
55
+ - `getUserMedia` and processing pipeline are decoupled
56
+ - `MediaStream` is created as soon as the `Media` object is created which
57
+ could contains no track.
58
+ - Any subsequent `getUserMedia` API call will update the track in the
59
+ `MediaStream` object. i.e. `MediaStream` will remain the same through out
60
+ the whole life cycle of the App and the tracks are updated according the
61
+ `getUserMedia` with an assumption that a `MediaStream` contains at most 1
62
+ audio track and 1 video track. `addtrack` and `removetrack` events are
63
+ triggered whenever there is any changes to the `MediaStream`
64
+
65
+ Changes of media signals;
66
+
67
+ The following signals are added
68
+
69
+ - `onAddTrack`: Emit when a new track is added to the `MediaStream`
70
+ - `onRemoveTrack`: Emit when a track from the `MediaStream` is removed
71
+ - `onUpdatingMedia`: Emit whenever a track is added or removed from the
72
+ `Media` object
73
+ - `onTrackReleased`: Emit when a track is stopped
74
+
75
+ The behavior of the following signals are changed:
76
+
77
+ - `onAudioMuteStateChanged`: Emit whenever the mute state of the `MediaTrack`
78
+ is changed or the mute state of the source track. Here the mute state
79
+ includes the following: - `MediaStreamTrack.enabled` -
80
+ `MediaStreamTrack.muted` triggered by `mute` and `unmute` event -
81
+ `readyState` is `ended` triggered by `ended` event
82
+ - `onVideoMuteStateChanged`: Emit whenever the mute state of the `MediaTrack`
83
+ is changed or the mute state of the source track. Here the mute state
84
+ includes the following: - `MediaStreamTrack.enabled` -
85
+ `MediaStreamTrack.muted` triggered by `mute` and `unmute` event -
86
+ `readyState` is `ended` triggered by `ended` event
87
+
88
+ ### Minor Changes
89
+
90
+ - 029198f: Fix device not found error mappinng
91
+ - b13010c: Handle timeout error
92
+ - 7d9eac2: Add processed track state signals
93
+
94
+ - onStreamTrackEndedFinal - Emitted when the final track is ended
95
+ - onStreamTrackMutedFinal - Emitted when the final track is muted
96
+ - onStreamTrackUnmutedFinal - Emitted when the final track is unmuted
97
+
98
+ ### Patch Changes
99
+
100
+ - Updated dependencies [6c42e61]
101
+ - Updated dependencies [e362a65]
102
+ - Updated dependencies [22a807b]
103
+ - Updated dependencies [878fcca]
104
+ - Updated dependencies [5b9daa4]
105
+ - Updated dependencies [164a3e2]
106
+ - Updated dependencies [b13010c]
107
+ - Updated dependencies [66a8f72]
108
+ - Updated dependencies [4fd97b2]
109
+ - Updated dependencies [db258ca]
110
+ - Updated dependencies [dfd1900]
111
+ - @pexip/media-control@19.0.0
112
+ - @pexip/media-processor@19.0.0
113
+ - @pexip/utils@16.13.0
114
+ - @pexip/signal@16.7.1
115
+
116
+ ## 18.5.0
117
+
118
+ ### Minor Changes
119
+
120
+ - f146682: Make `delegate` prop to be a fn
121
+
122
+ ### Patch Changes
123
+
124
+ - d98fe9d: Move to module type.
125
+ - Updated dependencies [d98fe9d]
126
+ - Updated dependencies [acd7aba]
127
+ - Updated dependencies [f146682]
128
+ - @pexip/media-processor@18.5.0
129
+ - @pexip/media-control@18.5.0
130
+ - @pexip/signal@16.7.1
131
+ - @pexip/utils@16.12.1
132
+
3
133
  ## 18.4.0
4
134
 
5
135
  ### Patch Changes
package/README.md CHANGED
@@ -19,10 +19,11 @@ Its major features:
19
19
  - Create a media pipeline to get and process the `MediaStream`, see
20
20
  `createMediaPipeline`. It also provides some media features including:
21
21
 
22
- - Video Processing - background blur
23
- - Audio Processing - Noise Suppression
24
- - Audio Signal Detection - To detect if the active audio input device is capturing any audio
25
- - Voice Activity Detection - To detect if there is any voice activity
22
+ - Video Processing - background blur
23
+ - Audio Processing - Noise Suppression
24
+ - Audio Signal Detection - To detect if the active audio input device is
25
+ capturing any audio
26
+ - Voice Activity Detection - To detect if there is any voice activity
26
27
 
27
28
  - Provide media input related information
28
29
  - Verify if we need to request a new `MediaStream`, see `updateMedia`
@@ -40,7 +41,7 @@ A processor to process video from the stream to be used together with
40
41
 
41
42
  ### `createPreviewController`
42
43
 
43
- It create's an object to control the provided media stream, e.g. change the
44
+ It creates an object to control the provided media stream, e.g. change the
44
45
  audio/video input device and apply changes to the main stream when necessary.
45
46
 
46
47
  ## Usage
@@ -195,10 +196,9 @@ const transformer = createCanvasTransform(segmenter, {
195
196
  backgroundImageUrl: states.backgroundImageUrl,
196
197
  });
197
198
 
198
- const delegate = isChromium() ? 'GPU' : 'CPU';
199
199
  const selfie = createSegmenter(taskVisionBasePath, {
200
200
  modelAsset,
201
- delegate,
201
+ delegate: () => 'GPU', // Use GPU
202
202
  });
203
203
 
204
204
  export const segmenters: Partial<Segmenters> = {
@@ -218,7 +218,8 @@ const videoStreamProcessor = createVideoStreamProcess({
218
218
  export const mediaController = createMedia({
219
219
  getMuteState: () => states.muted,
220
220
  signals,
221
- mediaProcessors: [videoStreamProcessor],
221
+ audioProcessors: [], // No audio processor
222
+ videoProcessors: [videoStreamProcessor],
222
223
  });
223
224
 
224
225
  // Hook-up the signals to get the update
@@ -1,6 +1,6 @@
1
1
  import type { MediaDeviceRequest } from '@pexip/media-control';
2
2
  import type { AudioGraph, AudioNodeInit } from '@pexip/media-processor';
3
- import type { Process, Media } from './types';
3
+ import type { TrackProcessor } from './types';
4
4
  interface AudioStreamProcessorProps {
5
5
  mixWithAdditionalMedia?: boolean;
6
6
  merger?: AudioNodeInit<ChannelMergerNode, ChannelMergerNode>;
@@ -14,5 +14,5 @@ export declare const updateFeatureProps: (constraints: MediaDeviceRequest['audio
14
14
  /**
15
15
  * Create a Audio Mixing Processor and will own the stream passed-in
16
16
  */
17
- export declare const createAudioMixingProcess: (getCurrrentMedia: () => MediaStream | undefined, scope?: string) => Process<Promise<Media>>;
17
+ export declare const createAudioMixingProcess: (getCurrentMedia: () => MediaStream | undefined, label?: "AudioMixingProcessor") => TrackProcessor;
18
18
  export {};
@@ -1,8 +1,9 @@
1
- import { muteStreamTrack, stopMediaStream, extractConstraintsWithKeys, } from '@pexip/media-control';
2
- import { isEmpty } from '@pexip/utils';
1
+ import { extractConstraintsWithKeys } from '@pexip/media-control';
2
+ import { isEmpty, assert } from '@pexip/utils';
3
3
  import { createAudioGraph, createAudioGraphProxy, createChannelMergerGraphNode, createStreamDestinationGraphNode, createStreamSourceGraphNode, resumeAudioOnUnmute, } from '@pexip/media-processor';
4
4
  import { logger } from './logger';
5
- import { applyExtendedConstraints, shallowCopy, wrapToJSON } from './utils';
5
+ import { createMediaTrack, isTrackMuted } from './utils';
6
+ import { PROCESSOR_LABELS } from './constants';
6
7
  const FEATURE_KEYS = ['mixWithAdditionalMedia'];
7
8
  const getAudioConstraints = extractConstraintsWithKeys(FEATURE_KEYS);
8
9
  export const updateFeatureProps = (constraints, props) => {
@@ -19,35 +20,31 @@ export const updateFeatureProps = (constraints, props) => {
19
20
  /**
20
21
  * Create a Audio Mixing Processor and will own the stream passed-in
21
22
  */
22
- export const createAudioMixingProcess = (getCurrrentMedia, scope = 'mixer') => {
23
+ export const createAudioMixingProcess = (getCurrentMedia, label = PROCESSOR_LABELS.AudioMixingProcessor) => {
23
24
  const props = {};
24
- return async (mediaP) => {
25
- const media = await mediaP;
26
- updateFeatureProps(media.constraints?.audio, props);
27
- const [mainTrack] = media.stream?.getAudioTracks() ?? [];
28
- const displayStream = getCurrrentMedia();
29
- const [displayTrack] = displayStream?.getAudioTracks?.() ?? [];
30
- const applyConstraints = applyExtendedConstraints(media, async (constraints) => {
31
- if (isEmpty(constraints.audio)) {
25
+ return async (prevMediaTrack) => {
26
+ updateFeatureProps(prevMediaTrack.getConstraints(), props);
27
+ const displayStream = getCurrentMedia();
28
+ const displayTrack = displayStream?.getAudioTracks?.().at(0);
29
+ const applyConstraints = async (constraints) => {
30
+ if (isEmpty(constraints)) {
32
31
  return;
33
32
  }
34
- const features = updateFeatureProps(constraints.audio, props);
35
- logger.debug({ scope, constraints: constraints.audio, features }, 'apply audio mixing constraints');
33
+ const features = updateFeatureProps(constraints, props);
34
+ logger.debug({ label, constraints: constraints, features }, 'apply audio mixing constraints');
36
35
  if (isEmpty(features) ||
37
36
  (props.audioGraph &&
38
37
  ['closed', 'closing'].includes(props.audioGraph.state))) {
39
38
  return;
40
39
  }
41
40
  if (features.mixWithAdditionalMedia) {
42
- const newStream = getCurrrentMedia();
43
- const [newTrack] = getCurrrentMedia()?.getAudioTracks() ?? [];
41
+ const newStream = getCurrentMedia();
42
+ const [newTrack] = getCurrentMedia()?.getAudioTracks() ?? [];
44
43
  if (!newTrack) {
45
44
  return;
46
45
  }
47
46
  if (!props.audioGraph || !props.merger) {
48
- // TODO: Should update the media stream directly
49
- // return replace(media.stream, newTrack);
50
- logger.debug({ scope }, 'Should update the media stream directly');
47
+ logger.debug({ label }, 'Should update the media stream directly');
51
48
  return;
52
49
  }
53
50
  if (props.displaySource && props.merger) {
@@ -78,96 +75,112 @@ export const createAudioMixingProcess = (getCurrrentMedia, scope = 'mixer') => {
78
75
  }
79
76
  }
80
77
  return Promise.resolve();
81
- });
82
- if (!media.stream || (!mainTrack && !displayTrack)) {
83
- return media;
84
- }
85
- if (!mainTrack && displayTrack) {
86
- media.stream?.addTrack(displayTrack);
87
- return shallowCopy(media, {
88
- muteAudio: () => {
78
+ };
79
+ // No need to mix and use as is
80
+ if (!prevMediaTrack.track) {
81
+ if (!displayTrack) {
82
+ // Do nothing
83
+ return prevMediaTrack;
84
+ }
85
+ return createMediaTrack({
86
+ label,
87
+ kind: 'audioinput',
88
+ previousMediaTrack: prevMediaTrack,
89
+ input: prevMediaTrack.input,
90
+ expectedInput: prevMediaTrack.expectedInput,
91
+ track: displayTrack,
92
+ constraints: prevMediaTrack.getConstraints(),
93
+ overrideMute: true,
94
+ mute: () => {
89
95
  /* Do Nothing */
90
96
  },
91
97
  applyConstraints,
92
- });
93
- }
94
- try {
95
- const mainSource = createStreamSourceGraphNode(media.stream);
96
- props.displaySource =
97
- displayStream && createStreamSourceGraphNode(displayStream);
98
- props.merger = createChannelMergerGraphNode();
99
- const destination = createStreamDestinationGraphNode({
100
- channelCount: 1,
101
- channelCountMode: 'explicit',
102
- });
103
- const initialAudioNodeConnection = [
104
- [mainSource, props.merger],
105
- [props.merger, destination],
106
- ];
107
- logger.debug({ initialAudioNodeConnection, scope }, 'Initial AudioNodeConnection');
108
- const audioGraph = createAudioGraphProxy(createAudioGraph(initialAudioNodeConnection), {
109
- connect: (target, args) => {
110
- logger.debug({ scope, target, args }, 'connect nodes');
111
- },
112
- disconnect: (target, args) => {
113
- logger.debug({ scope, target, args }, 'disconnect nodes');
114
- },
115
- });
116
- props.audioGraph = audioGraph;
117
- const unsubscribes = media.rawStream
118
- ?.getAudioTracks()
119
- .map(resumeAudioOnUnmute(audioGraph.context));
120
- if (props.displaySource) {
121
- props.audioGraph.connect([props.displaySource, props.merger]);
122
- }
123
- const tracks = [
124
- ...(destination?.node?.stream.getAudioTracks() ?? []),
125
- ...(media.stream?.getVideoTracks() ?? []),
126
- ];
127
- const stream = new MediaStream(tracks);
128
- const release = async () => {
129
- logger.debug({ scope }, 'Release Media');
130
- unsubscribes?.forEach(unsubscribe => unsubscribe());
131
- stopMediaStream(stream);
132
- // Release Props
133
- await props.audioGraph?.release();
134
- await media.release();
135
- props.audioGraph = undefined;
136
- props.merger = undefined;
137
- props.displaySource = undefined;
138
- };
139
- const muteAudio = (mute) => {
140
- media.muteAudio(mute);
141
- // TODO: dbl check if this is safe
142
- muteStreamTrack(mainSource.audioNode?.mediaStream)(mute, 'audio');
143
- };
144
- const prevGetSettings = media.getSettings;
145
- return wrapToJSON(shallowCopy(media, {
146
- stream,
147
- applyConstraints,
148
- muteAudio,
149
- release,
150
98
  getSettings: () => {
151
- const { audio, video } = prevGetSettings();
152
99
  const mixWithAdditionalMedia = !!props.mixWithAdditionalMedia;
153
- const audioSettings = {
100
+ const contentHint = displayTrack?.contentHint ?? '';
101
+ return {
154
102
  mixWithAdditionalMedia,
103
+ contentHint,
155
104
  };
156
- const settings = {
157
- audio: audio.map(settings => ({
158
- ...settings,
159
- ...audioSettings,
160
- })),
161
- video,
162
- };
163
- logger.debug({ scope, settings: audioSettings }, 'get audio mixing processor settings');
164
- return settings;
165
105
  },
166
- }));
106
+ });
167
107
  }
168
- catch (error) {
169
- logger.error({ scope, error }, 'Unable to use WebAudio, return the raw media instead');
170
- return media;
108
+ assert(prevMediaTrack.track, 'Main track should be available');
109
+ const mainSource = createStreamSourceGraphNode(new MediaStream([prevMediaTrack.track]));
110
+ props.displaySource =
111
+ displayStream && createStreamSourceGraphNode(displayStream);
112
+ props.merger = createChannelMergerGraphNode();
113
+ const destination = createStreamDestinationGraphNode({
114
+ channelCount: 1,
115
+ channelCountMode: 'explicit',
116
+ });
117
+ const initialAudioNodeConnection = [
118
+ [mainSource, props.merger],
119
+ [props.merger, destination],
120
+ ];
121
+ logger.debug({ initialAudioNodeConnection, label }, 'Initial AudioNodeConnection');
122
+ const audioGraph = createAudioGraphProxy(createAudioGraph(initialAudioNodeConnection), {
123
+ connect: (target, args) => {
124
+ logger.debug({ label, target, args }, 'connect nodes');
125
+ },
126
+ disconnect: (target, args) => {
127
+ logger.debug({ label, target, args }, 'disconnect nodes');
128
+ },
129
+ });
130
+ props.audioGraph = audioGraph;
131
+ const unsubscribe = resumeAudioOnUnmute(audioGraph.context)(prevMediaTrack.track);
132
+ if (props.displaySource) {
133
+ props.audioGraph.connect([props.displaySource, props.merger]);
171
134
  }
135
+ const track = destination?.node?.stream.getAudioTracks().at(0);
136
+ assert(track, 'No audio track mixed');
137
+ // Inherit contentHint from previous track
138
+ track.contentHint = prevMediaTrack.track.contentHint;
139
+ const release = async () => {
140
+ logger.debug({ label }, 'Release Media');
141
+ unsubscribe();
142
+ // Release Props
143
+ await props.audioGraph?.release();
144
+ props.audioGraph = undefined;
145
+ props.merger = undefined;
146
+ props.displaySource = undefined;
147
+ };
148
+ return Promise.resolve(createMediaTrack({
149
+ label,
150
+ kind: 'audioinput',
151
+ previousMediaTrack: prevMediaTrack,
152
+ input: prevMediaTrack.input,
153
+ expectedInput: prevMediaTrack.expectedInput,
154
+ track,
155
+ overrideMute: true,
156
+ mute: toMute => {
157
+ prevMediaTrack.mute(toMute);
158
+ mainSource.node?.mediaStream
159
+ .getAudioTracks()
160
+ .forEach(track => {
161
+ track.enabled = !toMute;
162
+ });
163
+ },
164
+ get muted() {
165
+ return (
166
+ // If the source track muted, the following processed track will
167
+ // be muted as well since there is no data to process
168
+ !!prevMediaTrack.muted ||
169
+ mainSource.node?.mediaStream
170
+ .getAudioTracks()
171
+ .some(isTrackMuted));
172
+ },
173
+ constraints: prevMediaTrack.getConstraints(),
174
+ applyConstraints,
175
+ release,
176
+ getSettings: () => {
177
+ const mixWithAdditionalMedia = !!props.mixWithAdditionalMedia;
178
+ const contentHint = track.contentHint ?? '';
179
+ return {
180
+ mixWithAdditionalMedia,
181
+ contentHint,
182
+ };
183
+ },
184
+ }));
172
185
  };
173
186
  };
@@ -1,6 +1,6 @@
1
1
  import type { AudioGraphOptions, AudioNodeInit, ThrottleOptions, DenoiseWorkletNodeInit, AnalyzerNodeInit, AudioGraph } from '@pexip/media-processor';
2
2
  import type { MediaDeviceRequest } from '@pexip/media-control';
3
- import type { Process, Media, DenoiseParams, AudioContentHint } from './types';
3
+ import type { TrackProcessor, DenoiseParams, AudioContentHint } from './types';
4
4
  type AudioNodeInits = AudioNodeInit[];
5
5
  /**
6
6
  * A function to be called to create the AudioNodes needed for the graph
@@ -8,7 +8,7 @@ type AudioNodeInits = AudioNodeInit[];
8
8
  *
9
9
  * @param media - Media to be used for the AudioGraph creation
10
10
  */
11
- type CreateNodes = (media: Media) => AudioNodeInits;
11
+ type CreateNodes = (track: MediaStreamTrack) => AudioNodeInits;
12
12
  interface AudioProcessOptions {
13
13
  /**
14
14
  * An option is being passed to AnalyserNode creation when used
@@ -59,7 +59,7 @@ interface AudioProcessOptions {
59
59
  * silent in FFTed time domain
60
60
  */
61
61
  silentThreshold?: number;
62
- scope?: string;
62
+ label?: string;
63
63
  }
64
64
  interface AudioStreamProcessorProps {
65
65
  audioGraphOptions?: AudioGraphOptions;
@@ -82,5 +82,5 @@ export declare const updateFeatureProps: (constraints: MediaDeviceRequest['audio
82
82
  /**
83
83
  * Create a Audio Stream Processor and will own the stream passed-in
84
84
  */
85
- export declare const createAudioStreamProcess: ({ analyzerUpdateFrequency, audioGraphOptions, audioSignalDetectionDuration, clock, createNodes, denoiseParams, fftSize, onAudioSignalDetected, onVoiceActivityDetected, shouldEnable, silentThreshold, throttleMs, scope, }: AudioProcessOptions & ThrottleOptions) => Process<Promise<Media>>;
85
+ export declare const createAudioStreamProcess: ({ analyzerUpdateFrequency, audioGraphOptions, audioSignalDetectionDuration, clock, createNodes, denoiseParams, fftSize, onAudioSignalDetected, onVoiceActivityDetected, shouldEnable, silentThreshold, throttleMs, label, }: AudioProcessOptions & ThrottleOptions) => TrackProcessor;
86
86
  export {};