Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ee/packages/media-calls/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CallFeature } from '@rocket.chat/media-signaling';

export const DEFAULT_CALL_FEATURES: CallFeature[] = ['audio'];
export const SIP_CALL_FEATURES: CallFeature[] = ['audio', 'transfer', 'hold'];
export const SIP_CALL_FEATURES: CallFeature[] = ['audio', 'transfer', 'hold', 'screen-share'];
6 changes: 6 additions & 0 deletions packages/media-signaling/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import client from '@rocket.chat/jest-presets/client';
import type { Config } from 'jest';

export default {
preset: client.preset,
} satisfies Config;
4 changes: 3 additions & 1 deletion packages/media-signaling/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"test": "jest"
"test": "jest",
"testunit": "jest",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@rocket.chat/emitter": "^0.33.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { IClientMediaCall } from '../../call';
import type { IMediaSignalLogger } from '../../logger';
import type { IMediaStreamManager } from '../../media/IMediaStreamManager';
import type { MediaStreamIdentification } from '../../media/MediaStreamIdentification';
import type { ServerMediaSignalRemoteSDP } from '../../signals';
import type { IServiceProcessor, ServiceProcessorEvents } from '../IServiceProcessor';

export type WebRTCInternalStateMap = {
Expand Down Expand Up @@ -48,7 +49,7 @@ export interface IWebRTCProcessor extends IServiceProcessor<WebRTCInternalStateM
isRemoteHeld(): boolean;
isRemoteMute(): boolean;

setRemoteIds(streams: MediaStreamIdentification[]): void;
setRemoteIds(signal: ServerMediaSignalRemoteSDP): void;
getLocalStreamIds(): MediaStreamIdentification[];
}

Expand Down
34 changes: 7 additions & 27 deletions packages/media-signaling/src/lib/Call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1051,18 +1051,6 @@ export class ClientMediaCall implements IClientMediaCall {
return false;
}

protected async processAnswerRequest(signal: ServerMediaSignalRemoteSDP): Promise<void> {
if (this.hidden || this.shouldIgnoreWebRTC()) {
return;
}

this.config.logger?.debug('ClientMediaCall.processAnswerRequest', signal);

this.requireWebRTC();

void this.negotiationManager.addNegotiation(signal.negotiationId, signal.sdp);
}

protected sendError(error: Partial<ClientMediaSignalError>): void {
this.config.logger?.debug('ClientMediaCall.sendError', error);

Expand All @@ -1080,30 +1068,22 @@ export class ClientMediaCall implements IClientMediaCall {
}

if (!this.isSignalTargetingThisSession(signal)) {
this.config.logger?.error('Received an offer request that is unsigned, or signed to a different session.');
this.config.logger?.error('Received a remote sdp that is not signed to this session.');
return;
}

if (this.shouldIgnoreWebRTC()) {
return;
}
if (!['offer', 'answer'].includes(signal.sdp.type)) {
this.config.logger?.error('Unsupported remote sdp type.', signal.sdp.type);
return;
}

this.requireWebRTC();

if (signal.streams) {
this.webrtcProcessor.setRemoteIds(signal.streams);
}
switch (signal.sdp.type) {
case 'offer':
await this.processAnswerRequest(signal);
break;
case 'answer':
await this.negotiationManager.setRemoteDescription(signal.negotiationId, signal.sdp);
break;
default:
this.config.logger?.error('Unsupported sdp type.');
return;
}
this.webrtcProcessor.setRemoteIds(signal);
await this.negotiationManager.setRemoteDescription(signal.negotiationId, signal.sdp);

this.receivedRemoteSdp = true;
this.updateClientState();
Expand Down
6 changes: 3 additions & 3 deletions packages/media-signaling/src/lib/media/MediaStreamManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ export class MediaStreamManager implements IMediaStreamManager {
return [this.mainRemote];
}

// A video track for an unidentified stream, let's ignore it
this.logger?.debug('unidentified stream, ignoring video track');
return [];
// A video track for an unidentified stream - since the only video we support now is screen share, assume that's what this is
this.logger?.debug('unidentified stream, assuming screen-share');
return [this.screenShareRemote];
}

private createStream(remote: boolean, tag: string): MediaStreamWrapper {
Expand Down
29 changes: 28 additions & 1 deletion packages/media-signaling/src/lib/services/webrtc/Negotiation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Emitter } from '@rocket.chat/emitter';

import { SDP } from './sdp';
import type { IMediaSignalLogger, IWebRTCProcessor, NegotiationData, NegotiationEvents } from '../../../definition';

export class Negotiation {
Expand Down Expand Up @@ -206,13 +207,39 @@ export class Negotiation {
if (!sdp) {
throw new Error('No local description');
}
return sdp;
return this.mutateLocalDescription(sdp);
} catch (err) {
this.logger?.error(err);
this.fail('failed-to-get-local-description');
throw err;
}
}

protected mutateLocalDescription(this: WebRTCNegotiation, description: RTCSessionDescriptionInit): RTCSessionDescriptionInit {
const { sdp, type } = description;
if (!sdp) {
return description;
}

this.logger?.debug('MediaCallWebRTCProcessor.mutateLocalDescription', type);

const mainStreamId = this.webrtcProcessor.streams.mainLocal.stream.id;
const screenShareStreamId = this.webrtcProcessor.streams.screenShareLocal.stream.id;

const mutated = SDP.mutateSDPWithStreamContents(sdp, [
{ id: mainStreamId, content: 'main' },
{ id: screenShareStreamId, content: 'slides' },
]);

if (sdp !== mutated) {
this.logger?.debug('SDP was mutated');
}

return {
type,
sdp: mutated,
};
}
}

export abstract class WebRTCNegotiation extends Negotiation {
Expand Down
50 changes: 48 additions & 2 deletions packages/media-signaling/src/lib/services/webrtc/Processor.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Emitter } from '@rocket.chat/emitter';

import { SDP } from './sdp';
import type { IWebRTCProcessor, WebRTCInternalStateMap, WebRTCProcessorConfig, WebRTCProcessorEvents } from '../../../definition';
import type { MediaStreamIdentification } from '../../../definition/media/MediaStreamIdentification';
import type { ServiceStateValue } from '../../../definition/services/IServiceProcessor';
import type { ServerMediaSignalRemoteSDP } from '../../../definition/signals';
import { MediaStreamManager } from '../../media/MediaStreamManager';
import { getExternalWaiter, type PromiseWaiterData } from '../../utils/getExternalWaiter';

Expand Down Expand Up @@ -308,8 +310,52 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor {
await iceGatheringData.promise;
}

public setRemoteIds(streams: MediaStreamIdentification[]): void {
this.streams.setRemoteIds(streams);
public setRemoteIds(signal: ServerMediaSignalRemoteSDP): void {
const {
streams,
sdp: { sdp },
} = signal;

const streamsFromSDP = sdp ? this.getRemoteIdsFromSDP(sdp) : [];
const allStreams = this.combineRemoteIds(streams || [], streamsFromSDP);

if (allStreams.length) {
this.streams.setRemoteIds(allStreams);
}
}

protected combineRemoteIds(streams1: MediaStreamIdentification[], streams2: MediaStreamIdentification[]): MediaStreamIdentification[] {
if (!streams2.length) {
return streams1;
}
if (!streams1.length) {
return streams2;
}

const result = [...streams1];
for (const stream of streams2) {
if (result.find(({ id }) => id === stream.id)) {
continue;
}

result.push(stream);
}

return result;
}

protected getRemoteIdsFromSDP(sdp: string): MediaStreamIdentification[] {
const contentMap = SDP.getStreamContentMapFromSDP(sdp);
return Object.entries(contentMap)
.map(([id, content]) => {
const tag = SDP.getStreamTagByMediaContent(content);
if (!tag) {
return null;
}

return { id, tag };
})
.filter((stream): stream is MediaStreamIdentification => Boolean(stream));
}

public getLocalStreamIds(): MediaStreamIdentification[] {
Expand Down
1 change: 1 addition & 0 deletions packages/media-signaling/src/lib/services/webrtc/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './Processor';
export * from './sdp';
Loading
Loading