Skip to content

Commit

Permalink
feat: Allow reusing call instances
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverlaz committed Aug 31, 2023
1 parent 7076c45 commit d9c7e75
Show file tree
Hide file tree
Showing 13 changed files with 139 additions and 98 deletions.
105 changes: 55 additions & 50 deletions packages/client/src/Call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ export class Call {
this.streamClient = streamClient;
this.clientStore = clientStore;
this.streamClientBasePath = `/call/${this.type}/${this.id}`;
this.camera = new CameraManager(this);
this.microphone = new MicrophoneManager(this);
this.logger = getLogger(['Call']);

const callTypeConfig = CallTypes.get(type);
Expand Down Expand Up @@ -266,43 +268,42 @@ export class Call {
(subscriptions) => this.sfuClient?.updateSubscriptions(subscriptions),
),
);
}

this.camera = new CameraManager(this);
this.microphone = new MicrophoneManager(this);

this.state.localParticipant$.subscribe(async (p) => {
// Mute via device manager
// If integrator doesn't use device manager, we mute using stopPublish
if (
!p?.publishedTracks.includes(TrackType.VIDEO) &&
this.publisher?.isPublishing(TrackType.VIDEO)
) {
this.logger(
'info',
`Local participant's video track is muted remotely`,
);
await this.camera.disable();
if (this.publisher.isPublishing(TrackType.VIDEO)) {
this.stopPublish(TrackType.VIDEO);
private registerEffects() {
this.leaveCallHooks.add(
createSubscription(this.state.localParticipant$, async (p) => {
// Mute via device manager
// If integrator doesn't use device manager, we mute using stopPublish
if (
!p?.publishedTracks.includes(TrackType.VIDEO) &&
this.publisher?.isPublishing(TrackType.VIDEO)
) {
this.logger(
'info',
`Local participant's video track is muted remotely`,
);
await this.camera.disable();
if (this.publisher.isPublishing(TrackType.VIDEO)) {
await this.stopPublish(TrackType.VIDEO);
}
}
}
if (
!p?.publishedTracks.includes(TrackType.AUDIO) &&
this.publisher?.isPublishing(TrackType.AUDIO)
) {
this.logger(
'info',
`Local participant's audio track is muted remotely`,
);
await this.microphone.disable();
if (this.publisher.isPublishing(TrackType.AUDIO)) {
this.stopPublish(TrackType.AUDIO);
if (
!p?.publishedTracks.includes(TrackType.AUDIO) &&
this.publisher?.isPublishing(TrackType.AUDIO)
) {
this.logger(
'info',
`Local participant's audio track is muted remotely`,
);
await this.microphone.disable();
if (this.publisher.isPublishing(TrackType.AUDIO)) {
await this.stopPublish(TrackType.AUDIO);
}
}
}
});
}
}),
);

private registerEffects() {
this.leaveCallHooks.add(
// handles updating the permissions context when the settings change.
createSubscription(this.state.settings$, (settings) => {
Expand Down Expand Up @@ -477,9 +478,9 @@ export class Call {
*/
leave = async ({ reject = false }: CallLeaveOptions = {}) => {
const callingState = this.state.callingState;
if (callingState === CallingState.LEFT) {
throw new Error('Cannot leave call that has already been left.');
}
// if (callingState === CallingState.LEFT) {
// throw new Error('Cannot leave call that has already been left.');
// }

if (callingState === CallingState.JOINING) {
await this.assertCallJoined();
Expand All @@ -504,18 +505,21 @@ export class Call {
this.subscriber?.close();
this.subscriber = undefined;

await this.microphone.disable();
await this.camera.disable();

this.publisher?.close();
this.publisher = undefined;

this.sfuClient?.close();
this.sfuClient = undefined;

this.dispatcher.offAll();
// this.dispatcher.offAll();

// Call all leave call hooks, e.g. to clean up global event handlers
this.leaveCallHooks.forEach((hook) => hook());
// this.leaveCallHooks.forEach((hook) => hook());

this.clientStore.unregisterCall(this);
// this.clientStore.unregisterCall(this);
this.state.setCallingState(CallingState.LEFT);
};

Expand Down Expand Up @@ -567,7 +571,6 @@ export class Call {

if (this.streamClient._hasConnectionID()) {
this.watching = true;
this.clientStore.registerCall(this);
}

return response;
Expand All @@ -594,7 +597,6 @@ export class Call {

if (this.streamClient._hasConnectionID()) {
this.watching = true;
this.clientStore.registerCall(this);
}

return response;
Expand Down Expand Up @@ -666,11 +668,11 @@ export class Call {
throw new Error(`Illegal State: Already joined.`);
}

if (callingState === CallingState.LEFT) {
throw new Error(
'Illegal State: Cannot join already left call. Create a new Call instance to join a call.',
);
}
// if (callingState === CallingState.LEFT) {
// throw new Error(
// 'Illegal State: Cannot join already left call. Create a new Call instance to join a call.',
// );
// }

const isMigrating = callingState === CallingState.MIGRATING;
this.state.setCallingState(CallingState.JOINING);
Expand Down Expand Up @@ -699,7 +701,6 @@ export class Call {

if (this.streamClient._hasConnectionID()) {
this.watching = true;
this.clientStore.registerCall(this);
}
} catch (error) {
// restore the previous call state if the join-flow fails
Expand Down Expand Up @@ -1011,7 +1012,11 @@ export class Call {
await this.initCamera();
await this.initMic();
} catch (error) {
this.logger('warn', 'Camera and/or mic init failed during join call');
this.logger(
'warn',
'Camera and/or mic init failed during join call',
error,
);
}
}

Expand Down Expand Up @@ -1893,10 +1898,10 @@ export class Call {
this.microphone.state.mediaStream &&
!this.publisher?.isPublishing(TrackType.AUDIO)
) {
this.publishAudioStream(this.microphone.state.mediaStream);
await this.publishAudioStream(this.microphone.state.mediaStream);
}

// Start mic if backend config speicifies, and there is no local setting
// Start mic if backend config specifies, and there is no local setting
if (
this.microphone.state.status === undefined &&
this.state.settings?.audio.mic_default_on
Expand Down
32 changes: 20 additions & 12 deletions packages/client/src/StreamVideoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export class StreamVideoClient {
const { call, members } = event;
if (user.id === call.created_by.id) {
this.logger(
'warn',
'debug',
'Received `call.created` sent by the current user',
);
return;
Expand Down Expand Up @@ -236,20 +236,23 @@ export class StreamVideoClient {
// if `call.created` was received before `call.ring`.
// In that case, we cleanup the already tracked call.
const prevCall = this.writeableStateStore.findCall(call.type, call.id);
await prevCall?.leave();
if (prevCall) {
await prevCall.leave();
this.writeableStateStore.unregisterCall(prevCall);
}
// we create a new call
const theCall = new Call({
const incomingCall = new Call({
streamClient: this.streamClient,
type: call.type,
id: call.id,
members,
clientStore: this.writeableStateStore,
ringing: true,
});
theCall.state.updateFromCallResponse(call);
incomingCall.state.updateFromCallResponse(call);
// we fetch the latest metadata for the call from the server
await theCall.get();
this.writeableStateStore.registerCall(theCall);
await incomingCall.get();
this.writeableStateStore.registerCall(incomingCall);
}),
);

Expand Down Expand Up @@ -311,12 +314,17 @@ export class StreamVideoClient {
* @param id the id of the call, if not provided a unique random value is used
*/
call = (type: string, id: string) => {
return new Call({
streamClient: this.streamClient,
id: id,
type: type,
clientStore: this.writeableStateStore,
});
return (
this.writeableStateStore.findCall(type, id) ??
this.writeableStateStore.registerCall(
new Call({
type: type,
id: id,
streamClient: this.streamClient,
clientStore: this.writeableStateStore,
}),
)
);
};

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/devices/CameraManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
*/
async flip() {
const newDirection = this.state.direction === 'front' ? 'back' : 'front';
this.selectDirection(newDirection);
await this.selectDirection(newDirection);
}

/**
Expand Down
9 changes: 3 additions & 6 deletions packages/client/src/devices/InputMediaDeviceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,8 @@ export abstract class InputMediaDeviceManager<
try {
await this.enablePromise;
this.state.setStatus('enabled');
} catch (error) {
} finally {
this.enablePromise = undefined;
throw error;
}
}

Expand All @@ -72,10 +71,8 @@ export abstract class InputMediaDeviceManager<
try {
await this.disablePromise;
this.state.setStatus('disabled');
} finally {
this.disablePromise = undefined;
} catch (error) {
this.disablePromise = undefined;
throw error;
}
}

Expand All @@ -87,7 +84,7 @@ export abstract class InputMediaDeviceManager<
this.state.prevStatus === 'enabled' &&
this.state.status === 'disabled'
) {
this.enable();
await this.enable();
}
}

Expand Down
4 changes: 3 additions & 1 deletion packages/client/src/helpers/sound-detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ export const createSoundDetector = (
// clean-up the AudioContext elements
microphone.disconnect();
analyser.disconnect();
await audioContext.close();
if (audioContext.state !== 'closed') {
await audioContext.close();
}

// stop the stream
if (destroyStreamOnStop) {
Expand Down
4 changes: 1 addition & 3 deletions packages/client/src/rtc/Publisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,9 +321,7 @@ export class Publisher {
? transceiver.sender.track.stop()
: (transceiver.sender.track.enabled = false);
// We don't need to notify SFU if unpublishing in response to remote soft mute
if (!this.state.localParticipant?.publishedTracks.includes(trackType)) {
return;
} else {
if (this.state.localParticipant?.publishedTracks.includes(trackType)) {
return this.notifyTrackMuteStateChanged(
undefined,
transceiver.sender.track,
Expand Down
7 changes: 5 additions & 2 deletions packages/client/src/store/rxUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@ export const getCurrentValue = <T>(observable$: Observable<T>) => {
* @param update the update to apply to the subject.
* @return the updated value.
*/
export const setCurrentValue = <T>(subject: Subject<T>, update: Patch<T>) => {
const next =
export const setCurrentValue = <T>(
subject: Subject<T>,
update: Patch<T>,
): T => {
const next: T =
// TypeScript needs more context to infer the type of update
typeof update === 'function' && update instanceof Function
? update(getCurrentValue(subject))
Expand Down
30 changes: 30 additions & 0 deletions sample-apps/client/ts-quickstart/src/controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,40 @@ const renderFlipButton = (call: Call) => {
return flipButton;
};

const renderCallLeaveButton = (call: Call) => {
const leaveButton = document.createElement('button');

leaveButton.addEventListener('click', async () => {
try {
await call.leave();
} catch (err) {
console.error(`Leave failed`, err);
}
});

leaveButton.innerText = 'Leave call';

return leaveButton;
};

const renderCallJoinButton = (call: Call) => {
const joinButton = document.createElement('button');

joinButton.addEventListener('click', async () => {
await call.join();
});

joinButton.innerText = 'Join call';

return joinButton;
};

export const renderControls = (call: Call) => {
return {
audioButton: renderAudioButton(call),
videoButton: renderVideoButton(call),
flipButton: renderFlipButton(call),
leaveButton: renderCallLeaveButton(call),
joinButton: renderCallJoinButton(call),
};
};
5 changes: 5 additions & 0 deletions sample-apps/client/ts-quickstart/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,17 @@ const client = new StreamVideoClient({
options: { logLevel: import.meta.env.VITE_STREAM_LOG_LEVEL },
});
const call = client.call('default', callId);
// @ts-expect-error exposed for debug purposes
window.call = call;

call.join({ create: true }).then(async () => {
// render mic and camera controls
const controls = renderControls(call);
const container = document.getElementById('call-controls')!;
container.appendChild(controls.audioButton);
container.appendChild(controls.videoButton);
container.appendChild(controls.joinButton);
container.appendChild(controls.leaveButton);

container.appendChild(renderAudioDeviceSelector(call));

Expand Down
2 changes: 1 addition & 1 deletion sample-apps/react/messenger-clone/.env-example
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
VITE_STREAM_API_KEY=<your_api_key>
VITE_STREAM_KEY=<your_api_key>
VITE_TOKEN_PROVIDER_URL=<your_token_provider_url>
Loading

0 comments on commit d9c7e75

Please sign in to comment.