-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: update effects APIs #68
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,23 @@ | ||
import { AddEvents, TypedEvent, WithEventsDummyType } from '@webex/ts-events'; | ||
import { BaseEffect, EffectEvent } from '@webex/web-media-effects'; | ||
import { WebrtcCoreError, WebrtcCoreErrorType } from '../errors'; | ||
import { logger } from '../util/logger'; | ||
import { Stream, StreamEventNames } from './stream'; | ||
|
||
export type TrackEffect = BaseEffect; | ||
|
||
export enum LocalStreamEventNames { | ||
ConstraintsChange = 'constraints-change', | ||
OutputTrackChange = 'output-track-change', | ||
EffectAdded = 'effect-added', | ||
} | ||
|
||
interface LocalStreamEvents { | ||
[LocalStreamEventNames.ConstraintsChange]: TypedEvent<() => void>; | ||
[LocalStreamEventNames.OutputTrackChange]: TypedEvent<(track: MediaStreamTrack) => void>; | ||
[LocalStreamEventNames.EffectAdded]: TypedEvent<(effect: TrackEffect) => void>; | ||
} | ||
|
||
export type TrackEffect = BaseEffect; | ||
|
||
type EffectItem = { name: string; effect: TrackEffect }; | ||
|
||
/** | ||
* A stream which originates on the local device. | ||
*/ | ||
|
@@ -24,7 +26,9 @@ abstract class _LocalStream extends Stream { | |
|
||
[LocalStreamEventNames.OutputTrackChange] = new TypedEvent<(track: MediaStreamTrack) => void>(); | ||
|
||
private effects: EffectItem[] = []; | ||
[LocalStreamEventNames.EffectAdded] = new TypedEvent<(effect: TrackEffect) => void>(); | ||
|
||
private effects: TrackEffect[] = []; | ||
|
||
private loadingEffects: Map<string, TrackEffect> = new Map(); | ||
|
||
|
@@ -141,55 +145,118 @@ abstract class _LocalStream extends Stream { | |
/** | ||
* Adds an effect to a local stream. | ||
* | ||
* @param name - The name of the effect. | ||
* @param effect - The effect to add. | ||
*/ | ||
async addEffect(name: string, effect: TrackEffect): Promise<void> { | ||
// Load the effect | ||
this.loadingEffects.set(name, effect); | ||
const outputTrack = await effect.load(this.outputTrack); | ||
async addEffect(effect: TrackEffect): Promise<void> { | ||
// Check if the effect has already been added. | ||
if (this.effects.some((e) => e.id === effect.id)) { | ||
return; | ||
} | ||
|
||
// Load the effect. Because loading is asynchronous, keep track of the loading effects. | ||
this.loadingEffects.set(effect.kind, effect); | ||
await effect.load(this.outputTrack); | ||
|
||
// Check that the loaded effect is the latest one and dispose if not | ||
if (effect !== this.loadingEffects.get(name)) { | ||
// After loading, check whether or not we still want to use this effect. If another effect of | ||
// the same kind was added while this effect was loading, we only want to use the latest effect, | ||
// so dispose this one. If the effects list was cleared while this effect was loading, also | ||
// dispose it. | ||
Comment on lines
+160
to
+163
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It feels a bit strange that we'll allow overriding an affect of the same type that was added previously if it didn't finish loading yet, but we error on adding an effect of the same type after it's been loaded. I think I'd expect that subsequent additions of the same type of effect (without removing the previous one) should either always work or always fail? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The previous behavior was to override the effect if it didn't finish loading, so I think we should keep that behavior (since the client does depend on it). We didn't handle adding a duplicate effect after it has loaded previously, so I can change it so that it will also override instead of error. |
||
if (effect !== this.loadingEffects.get(effect.kind)) { | ||
await effect.dispose(); | ||
throw new Error(`Effect "${name}" not required after loading`); | ||
throw new WebrtcCoreError( | ||
WebrtcCoreErrorType.ADD_EFFECT_FAILED, | ||
`Another effect with kind ${effect.kind} was added while effect ${effect.id} was loading, or the effects list was cleared.` | ||
); | ||
} | ||
this.loadingEffects.delete(effect.kind); | ||
|
||
// Use the effect | ||
this.loadingEffects.delete(name); | ||
this.effects.push({ name, effect }); | ||
this.changeOutputTrack(outputTrack); | ||
|
||
// When the effect's track is updated, update the next effect or output stream. | ||
// TODO: using EffectEvent.TrackUpdated will cause the entire web-media-effects lib to be built | ||
// and makes the size of the webrtc-core build much larger, so we use type assertion here as a | ||
// temporary workaround. | ||
effect.on('track-updated' as EffectEvent, (track: MediaStreamTrack) => { | ||
const effectIndex = this.effects.findIndex((e) => e.name === name); | ||
/** | ||
* Handle when the effect's output track has been changed. This will update the input of the | ||
* next effect in the effects list of the output of the stream. | ||
* | ||
* @param track - The new output track of the effect. | ||
*/ | ||
const handleEffectTrackUpdated = (track: MediaStreamTrack) => { | ||
const effectIndex = this.effects.findIndex((e) => e.id === effect.id); | ||
if (effectIndex === this.effects.length - 1) { | ||
this.changeOutputTrack(track); | ||
} else if (effectIndex >= 0) { | ||
this.effects[effectIndex + 1]?.replaceInputTrack(track); | ||
} else { | ||
this.effects[effectIndex + 1]?.effect.replaceInputTrack(track); | ||
logger.error(`Effect with ID ${effect.id} not found in effects list.`); | ||
} | ||
}; | ||
|
||
/** | ||
* Handle when the effect has been disposed. This will remove all event listeners from the | ||
* effect. | ||
*/ | ||
const handleEffectDisposed = () => { | ||
effect.off('track-updated' as EffectEvent, handleEffectTrackUpdated); | ||
effect.off('disposed' as EffectEvent, handleEffectDisposed); | ||
}; | ||
|
||
// TODO: using EffectEvent.TrackUpdated or EffectEvent.Disposed will cause the entire | ||
// web-media-effects lib to be rebuilt and inflates the size of the webrtc-core build, so | ||
// we use type assertion here as a temporary workaround. | ||
effect.on('track-updated' as EffectEvent, handleEffectTrackUpdated); | ||
effect.on('disposed' as EffectEvent, handleEffectDisposed); | ||
|
||
// Add the effect to the effects list. If an effect of the same kind has already been added, | ||
// dispose the existing effect and replace it with the new effect. If the existing effect was | ||
// enabled, also enable the new effect. | ||
const existingEffectIndex = this.effects.findIndex((e) => e.kind === effect.kind); | ||
if (existingEffectIndex >= 0) { | ||
const [existingEffect] = this.effects.splice(existingEffectIndex, 1, effect); | ||
if (existingEffect.isEnabled) { | ||
// If the existing effect is not the first effect in the effects list, then the input of the | ||
// new effect should be the output of the previous effect in the effects list. We know the | ||
// output track of the previous effect must exist because it must have been loaded (and all | ||
// loaded effects have an output track). | ||
const inputTrack = | ||
existingEffectIndex === 0 | ||
? this.inputTrack | ||
: (this.effects[existingEffectIndex - 1].getOutputTrack() as MediaStreamTrack); | ||
await effect.replaceInputTrack(inputTrack); | ||
// Enabling the new effect will trigger the track-updated event, which will handle the new | ||
// effect's updated output track. | ||
await effect.enable(); | ||
} | ||
}); | ||
await existingEffect.dispose(); | ||
} else { | ||
this.effects.push(effect); | ||
} | ||
|
||
// Emit an event with the effect so others can listen to the effect events. | ||
this[LocalStreamEventNames.EffectAdded].emit(effect); | ||
} | ||
|
||
/** | ||
* Get an effect from the effects list by ID. | ||
* | ||
* @param id - The id of the effect you want to get. | ||
* @returns The effect or undefined. | ||
*/ | ||
getEffectById(id: string): TrackEffect | undefined { | ||
return this.effects.find((effect) => effect.id === id); | ||
} | ||
|
||
/** | ||
* Get an effect from the effects list. | ||
* Get an effect from the effects list by kind. | ||
* | ||
* @param name - The name of the effect you want to get. | ||
* @param kind - The kind of the effect you want to get. | ||
* @returns The effect or undefined. | ||
*/ | ||
getEffect(name: string): TrackEffect | undefined { | ||
return this.effects.find((e) => e.name === name)?.effect; | ||
getEffectByKind(kind: string): TrackEffect | undefined { | ||
return this.effects.find((effect) => effect.kind === kind); | ||
} | ||
|
||
/** | ||
* Get all the effects from the effects list. | ||
* | ||
* @returns A list of effect items, each containing the name and the effect itself. | ||
* @returns A list of effects. | ||
*/ | ||
getAllEffects(): EffectItem[] { | ||
getEffects(): TrackEffect[] { | ||
return this.effects; | ||
} | ||
|
||
|
@@ -202,7 +269,7 @@ abstract class _LocalStream extends Stream { | |
// Dispose of any effects currently in use | ||
if (this.effects.length > 0) { | ||
this.changeOutputTrack(this.inputTrack); | ||
await Promise.all(this.effects.map((item: EffectItem) => item.effect.dispose())); | ||
await Promise.all(this.effects.map((effect) => effect.dispose())); | ||
this.effects = []; | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this now checks if the same effect instance is being added, but I think we need to check for an existing effect of the same kind to replace it with this one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I didn't notice that all the code below is in this same method, I see there's handling there for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's logic starting at line 205 that handles this case.