Skip to content
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

Add method to populate an offline room with simulated participants #531

Merged
merged 10 commits into from
Dec 9, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
add experimental method to simulate participants in a room without co…
…nnection
  • Loading branch information
lukasIO committed Dec 7, 2022
commit 47f1f901444e3a0d900c26fd4da0af2782974cbd
86 changes: 84 additions & 2 deletions src/room/Room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import {
Room as RoomModel,
ServerInfo,
SpeakerInfo,
TrackInfo,
TrackSource,
TrackType,
UserPacket,
} from '../proto/livekit_models';
import {
Expand All @@ -42,15 +45,24 @@ import type { ConnectionQuality } from './participant/Participant';
import RemoteParticipant from './participant/RemoteParticipant';
import RTCEngine from './RTCEngine';
import LocalAudioTrack from './track/LocalAudioTrack';
import type LocalTrackPublication from './track/LocalTrackPublication';
import LocalTrackPublication from './track/LocalTrackPublication';
import LocalVideoTrack from './track/LocalVideoTrack';
import type RemoteTrack from './track/RemoteTrack';
import RemoteTrackPublication from './track/RemoteTrackPublication';
import { Track } from './track/Track';
import type { TrackPublication } from './track/TrackPublication';
import type { AdaptiveStreamSettings } from './track/types';
import { getNewAudioContext } from './track/utils';
import { Future, isWeb, Mutex, supportsSetSinkId, unpackStreamId } from './utils';
import {
Future,
getDummyVideoStreamTrack,
getEmptyAudioStreamTrack,
getEmptyVideoStreamTrack,
isWeb,
Mutex,
supportsSetSinkId,
unpackStreamId,
} from './utils';

export enum ConnectionState {
Disconnected = 'disconnected',
Expand Down Expand Up @@ -1242,6 +1254,76 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
this.emit(RoomEvent.ParticipantPermissionsChanged, prevPermissions, this.localParticipant);
};

/**
* @experimental
*/
simulateParticipants(
count: number,
options: { audio?: boolean; video?: boolean; aspectRatios: Array<number> },
) {
this.emit(RoomEvent.SignalConnected);
this.emit(RoomEvent.Connected);
this.setAndEmitConnectionState(ConnectionState.Connected);
const pub = new LocalTrackPublication(
Track.Kind.Video,
TrackInfo.fromPartial({
source: TrackSource.CAMERA,
sid: Math.floor(Math.random() * 10_000).toString(),
type: TrackType.AUDIO,
name: 'video-dummy',
}),
new LocalVideoTrack(
getDummyVideoStreamTrack(160 * options.aspectRatios[0] ?? 1, 160, true, true),
),
);
// @ts-ignore
this.localParticipant.addTrackPublication(pub);
this.localParticipant.emit(ParticipantEvent.LocalTrackPublished, pub);
this.emit(RoomEvent.LocalTrackPublished, pub, this.localParticipant);
for (let i = 0; i < count - 1; i += 1) {
let info: ParticipantInfo = ParticipantInfo.fromPartial({
sid: Math.floor(Math.random() * 10_000).toString(),
identity: `simulated-${i}`,
state: ParticipantInfo_State.ACTIVE,
tracks: [],
metadata: '',
joinedAt: Date.now(),
region: 'cyberspace',
});
const p = this.getOrCreateParticipant(info.identity, info);
if (options.video) {
console.log('aspect index for ', i, i % options.aspectRatios.length);
const dummyVideo = getDummyVideoStreamTrack(
160 * options.aspectRatios[i % options.aspectRatios.length] ?? 1,
160,
false,
true,
);
const videoTrack = TrackInfo.fromPartial({
source: TrackSource.CAMERA,
sid: Math.floor(Math.random() * 10_000).toString(),
type: TrackType.AUDIO,
name: 'video-dummy',
});
p.addSubscribedMediaTrack(dummyVideo, videoTrack.sid, new MediaStream([dummyVideo]));
info.tracks = [...info.tracks, videoTrack];
}
if (options.audio) {
const dummyTrack = getEmptyAudioStreamTrack();
const audioTrack = TrackInfo.fromPartial({
source: TrackSource.MICROPHONE,
sid: Math.floor(Math.random() * 10_000).toString(),
type: TrackType.AUDIO,
name: 'audio-dummy',
});
p.addSubscribedMediaTrack(dummyTrack, audioTrack.sid, new MediaStream([dummyTrack]));
info.tracks = [...info.tracks, audioTrack];
}

p.updateInfo(info);
}
}

// /** @internal */
emit<E extends keyof RoomEventCallbacks>(
event: E,
Expand Down
43 changes: 31 additions & 12 deletions src/room/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,22 +181,41 @@ let emptyVideoStreamTrack: MediaStreamTrack | undefined;

export function getEmptyVideoStreamTrack() {
if (!emptyVideoStreamTrack) {
const canvas = document.createElement('canvas');
// the canvas size is set to 16, because electron apps seem to fail with smaller values
canvas.width = 16;
canvas.height = 16;
canvas.getContext('2d')?.fillRect(0, 0, canvas.width, canvas.height);
// @ts-ignore
const emptyStream = canvas.captureStream();
[emptyVideoStreamTrack] = emptyStream.getTracks();
if (!emptyVideoStreamTrack) {
throw Error('Could not get empty media stream video track');
}
emptyVideoStreamTrack.enabled = false;
emptyVideoStreamTrack = getDummyVideoStreamTrack();
}
return emptyVideoStreamTrack;
}

export function getDummyVideoStreamTrack(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
export function getDummyVideoStreamTrack(
export function createDummyVideoStreamTrack(

width: number = 16,
height: number = 16,
enabled: boolean = false,
paintContent: boolean = false,
) {
const canvas = document.createElement('canvas');
// the canvas size is set to 16 by default, because electron apps seem to fail with smaller values
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx?.fillRect(0, 0, canvas.width, canvas.height);
if (paintContent && ctx) {
ctx.beginPath();
ctx.arc(width / 2, height / 2, 50, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fillStyle = 'grey';
ctx.fill();
}
// @ts-ignore
const dummyStream = canvas.captureStream();
const [dummyTrack] = dummyStream.getTracks();
if (!dummyTrack) {
throw Error('Could not get empty media stream video track');
}
dummyTrack.enabled = enabled;

return dummyTrack;
}

let emptyAudioStreamTrack: MediaStreamTrack | undefined;

export function getEmptyAudioStreamTrack() {
Expand Down