mediasfu-angular 2.1.6 β 2.2.1
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/README.md +1560 -31
- package/dist/README.md +1560 -31
- package/dist/fesm2022/mediasfu-angular.mjs +1971 -1079
- package/dist/fesm2022/mediasfu-angular.mjs.map +1 -1
- package/dist/lib/@types/custom-component.types.d.ts +130 -0
- package/dist/lib/@types/types.d.ts +5 -0
- package/dist/lib/components/mediasfu-components/mediasfu-broadcast.component.d.ts +99 -17
- package/dist/lib/components/mediasfu-components/mediasfu-chat.component.d.ts +99 -17
- package/dist/lib/components/mediasfu-components/mediasfu-conference.component.d.ts +121 -21
- package/dist/lib/components/mediasfu-components/mediasfu-generic.component.d.ts +121 -21
- package/dist/lib/components/mediasfu-components/mediasfu-webinar.component.d.ts +121 -21
- package/dist/lib/consumers/add-videos-grid.service.d.ts +3 -0
- package/dist/lib/consumers/prepopulate-user-media.service.d.ts +3 -0
- package/dist/lib/methods/utils/create-room-on-media-sfu.service.d.ts +10 -8
- package/dist/lib/services/custom-component-injection.service.d.ts +86 -0
- package/dist/public-api.d.ts +1 -0
- package/package.json +15 -15
package/dist/README.md
CHANGED
|
@@ -4,22 +4,57 @@
|
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
6
|
<a href="https://twitter.com/media_sfu">
|
|
7
|
-
<img src="https://img.
|
|
7
|
+
<img src="https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white" alt="Twitter" />
|
|
8
8
|
</a>
|
|
9
9
|
<a href="https://www.mediasfu.com/forums">
|
|
10
|
-
<img src="https://img.
|
|
10
|
+
<img src="https://img.shields.io/badge/Community-Forum-blue?style=for-the-badge&logo=discourse&logoColor=white" alt="Community Forum" />
|
|
11
11
|
</a>
|
|
12
12
|
<a href="https://github.com/MediaSFU">
|
|
13
|
-
<img src="https://img.
|
|
13
|
+
<img src="https://img.shields.io/badge/GitHub-181717?style=for-the-badge&logo=github&logoColor=white" alt="Github" />
|
|
14
14
|
</a>
|
|
15
15
|
<a href="https://www.mediasfu.com/">
|
|
16
|
-
<img src="https://img.
|
|
16
|
+
<img src="https://img.shields.io/badge/Website-4285F4?style=for-the-badge&logo=google-chrome&logoColor=white" alt="Website" />
|
|
17
17
|
</a>
|
|
18
18
|
<a href="https://www.youtube.com/channel/UCELghZRPKMgjih5qrmXLtqw">
|
|
19
|
-
<img src="https://img.
|
|
19
|
+
<img src="https://img.shields.io/badge/YouTube-FF0000?style=for-the-badge&logo=youtube&logoColor=white" alt="Youtube" />
|
|
20
20
|
</a>
|
|
21
21
|
</p>
|
|
22
22
|
|
|
23
|
+
<p align="center">
|
|
24
|
+
<a href="https://opensource.org/licenses/MIT">
|
|
25
|
+
<img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square" alt="License: MIT" />
|
|
26
|
+
</a>
|
|
27
|
+
<a href="https://mediasfu.com">
|
|
28
|
+
<img src="https://img.shields.io/badge/Built%20with-MediaSFU-blue?style=flat-square" alt="Built with MediaSFU" />
|
|
29
|
+
</a>
|
|
30
|
+
<a href="https://angular.io">
|
|
31
|
+
<img src="https://img.shields.io/badge/Angular-DD0031?style=flat-square&logo=angular&logoColor=white" alt="Angular" />
|
|
32
|
+
</a>
|
|
33
|
+
<a href="https://www.typescriptlang.org">
|
|
34
|
+
<img src="https://img.shields.io/badge/TypeScript-007ACC?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript" />
|
|
35
|
+
</a>
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## π¨ **BREAKING: AI Phone Agents at $0.10 per 1,000 minutes**
|
|
41
|
+
|
|
42
|
+
π **Call our live AI demos right now:**
|
|
43
|
+
- πΊπΈ **+1 (785) 369-1724** - Mixed Support Demo
|
|
44
|
+
- π¬π§ **+44 7445 146575** - AI Conversation Demo
|
|
45
|
+
- π¨π¦ **+1 (587) 407-1990** - Technical Support Demo
|
|
46
|
+
- π¨π¦ **+1 (647) 558-6650** - Friendly AI Chat Demo
|
|
47
|
+
|
|
48
|
+
**Traditional providers charge $0.05 per minute. We charge $0.10 per 1,000 minutes. That's 500x cheaper.**
|
|
49
|
+
|
|
50
|
+
β
**Deploy AI phone agents in 30 minutes**
|
|
51
|
+
β
**Works with ANY SIP provider** (Twilio, Telnyx, Zadarma, etc.)
|
|
52
|
+
β
**Seamless AI-to-human handoffs**
|
|
53
|
+
β
**Real-time call analytics & transcription**
|
|
54
|
+
|
|
55
|
+
π **[Complete SIP/PSTN Documentation β](https://mediasfu.com/telephony)**
|
|
56
|
+
|
|
57
|
+
---
|
|
23
58
|
|
|
24
59
|
MediaSFU offers a cutting-edge streaming experience that empowers users to customize their recordings and engage their audience with high-quality streams. Whether you're a content creator, educator, or business professional, MediaSFU provides the tools you need to elevate your streaming game.
|
|
25
60
|
|
|
@@ -39,15 +74,22 @@ MediaSFU offers a cutting-edge streaming experience that empowers users to custo
|
|
|
39
74
|
|
|
40
75
|
**[Get started now on GitHub!](https://github.com/MediaSFU/MediaSFUOpen)**
|
|
41
76
|
|
|
77
|
+
### β
Angular SDK Setup Guide
|
|
78
|
+
Coming soon! Watch this space for our comprehensive video tutorial on setting up the Angular SDK.
|
|
79
|
+
|
|
42
80
|
---
|
|
43
81
|
|
|
44
82
|
## Table of Contents
|
|
45
83
|
|
|
46
84
|
- [Features](#features)
|
|
47
85
|
- [Getting Started](#getting-started)
|
|
48
|
-
- [
|
|
49
|
-
- [
|
|
50
|
-
- [
|
|
86
|
+
- [π Angular SDK Guide](#angular-sdk-guide)
|
|
87
|
+
- [Quick Start](#quick-start-5-minutes)
|
|
88
|
+
- [Understanding the Architecture](#understanding-mediasfu-architecture)
|
|
89
|
+
- [Core Concepts & Components](#core-concepts--components)
|
|
90
|
+
- [Working with Methods](#working-with-methods)
|
|
91
|
+
- [Media Streams & Participants](#media-streams--participants)
|
|
92
|
+
- [Customization & Styling](#customization--styling)
|
|
51
93
|
- [API Reference](#api-reference)
|
|
52
94
|
- [Troubleshooting](#troubleshooting)
|
|
53
95
|
- [Contributing](#contributing)
|
|
@@ -67,6 +109,74 @@ MediaSFU's Angular SDK comes with a host of powerful features out of the box:
|
|
|
67
109
|
9. **Cloud Recording (track-based)**: Customize recordings with track-based options, including watermarks, name tags, background colors, and more.
|
|
68
110
|
10. **Managed Events**: Manage events with features to handle abandoned and inactive participants, as well as enforce time and capacity limits.
|
|
69
111
|
|
|
112
|
+
## π **New Advanced Media Access**
|
|
113
|
+
|
|
114
|
+
The Angular SDK now includes powerful utility methods for fine-grained control over media devices and participant streams:
|
|
115
|
+
|
|
116
|
+
### **`getMediaDevicesList`** - Device Enumeration
|
|
117
|
+
|
|
118
|
+
Enumerate available cameras and microphones with automatic permission handling:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// Get all available cameras
|
|
122
|
+
const cameras = await sourceParameters.getMediaDevicesList('videoinput');
|
|
123
|
+
cameras.forEach(camera => {
|
|
124
|
+
console.log(`Camera: ${camera.label} (${camera.deviceId})`);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Get all available microphones
|
|
128
|
+
const microphones = await sourceParameters.getMediaDevicesList('audioinput');
|
|
129
|
+
microphones.forEach(mic => {
|
|
130
|
+
console.log(`Microphone: ${mic.label} (${mic.deviceId})`);
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Use Cases:**
|
|
135
|
+
- Build custom device selection interfaces
|
|
136
|
+
- Detect available media hardware
|
|
137
|
+
- Switch between multiple cameras/microphones
|
|
138
|
+
- Pre-flight device checks before joining
|
|
139
|
+
|
|
140
|
+
[See full documentation and examples β](#media-device-and-stream-utility-methods)
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
### **`getParticipantMedia`** - Stream Access
|
|
145
|
+
|
|
146
|
+
Retrieve specific participant's video or audio streams by ID or name:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
// Get participant video stream by producer ID
|
|
150
|
+
const videoStream = await sourceParameters.getParticipantMedia({
|
|
151
|
+
id: 'producer-123',
|
|
152
|
+
kind: 'video'
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Get participant audio stream by name
|
|
156
|
+
const audioStream = await sourceParameters.getParticipantMedia({
|
|
157
|
+
name: 'John Doe',
|
|
158
|
+
kind: 'audio'
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Use the stream (e.g., attach to video element)
|
|
162
|
+
if (videoStream) {
|
|
163
|
+
videoElement.srcObject = videoStream;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Use Cases:**
|
|
168
|
+
- Monitor specific participant streams
|
|
169
|
+
- Create custom video layouts with individual control
|
|
170
|
+
- Build stream recording features
|
|
171
|
+
- Implement advanced audio/video processing
|
|
172
|
+
- Create picture-in-picture views for specific users
|
|
173
|
+
|
|
174
|
+
[See full documentation and examples β](#media-device-and-stream-utility-methods)
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
These utilities enable advanced features like custom device selection interfaces, participant stream monitoring, and dynamic media routing. Both methods are fully integrated with Angular's reactive patterns using RxJS.
|
|
179
|
+
|
|
70
180
|
# Getting Started <a name="getting-started"></a>
|
|
71
181
|
|
|
72
182
|
This section will guide users through the initial setup and installation of the npm module.
|
|
@@ -159,42 +269,153 @@ If you plan to self-host MediaSFU or use it without MediaSFU Cloud services, you
|
|
|
159
269
|
This setup allows full flexibility and customization while bypassing the need for cloud-dependent credentials.
|
|
160
270
|
|
|
161
271
|
|
|
162
|
-
#
|
|
272
|
+
# π Angular SDK Guide <a name="angular-sdk-guide"></a>
|
|
163
273
|
|
|
164
|
-
|
|
274
|
+
This comprehensive guide will walk you through everything you need to know about building real-time communication apps with MediaSFU's Angular SDK. Whether you're a beginner or an experienced developer, you'll find clear explanations, practical examples, and best practices.
|
|
165
275
|
|
|
166
|
-
|
|
167
|
-
## Introduction
|
|
276
|
+
---
|
|
168
277
|
|
|
169
|
-
|
|
278
|
+
## Quick Start (5 Minutes) <a name="quick-start-5-minutes"></a>
|
|
170
279
|
|
|
171
|
-
|
|
280
|
+
Get your first MediaSFU app running in just a few minutes.
|
|
172
281
|
|
|
173
|
-
|
|
282
|
+
### Step 1: Install the Package
|
|
174
283
|
|
|
175
|
-
|
|
284
|
+
```bash
|
|
285
|
+
npm install mediasfu-angular
|
|
286
|
+
```
|
|
176
287
|
|
|
177
|
-
|
|
288
|
+
### Step 2: Import and Use
|
|
178
289
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
5. **MediasfuChat**: A room tailored for interactive chat sessions.
|
|
290
|
+
```typescript
|
|
291
|
+
// app.component.ts
|
|
292
|
+
import { Component } from '@angular/core';
|
|
293
|
+
import { MediasfuGeneric } from 'mediasfu-angular';
|
|
184
294
|
|
|
185
|
-
|
|
295
|
+
@Component({
|
|
296
|
+
selector: 'app-root',
|
|
297
|
+
standalone: true,
|
|
298
|
+
imports: [MediasfuGeneric],
|
|
299
|
+
template: `<app-mediasfu-generic></app-mediasfu-generic>`,
|
|
300
|
+
})
|
|
301
|
+
export class AppComponent { }
|
|
302
|
+
```
|
|
186
303
|
|
|
187
|
-
|
|
304
|
+
**Alternative with Credentials:**
|
|
188
305
|
|
|
189
|
-
|
|
306
|
+
```typescript
|
|
307
|
+
import { Component } from '@angular/core';
|
|
308
|
+
import { MediasfuGeneric, PreJoinPage } from 'mediasfu-angular';
|
|
190
309
|
|
|
191
|
-
|
|
192
|
-
|
|
310
|
+
@Component({
|
|
311
|
+
selector: 'app-root',
|
|
312
|
+
standalone: true,
|
|
313
|
+
imports: [MediasfuGeneric],
|
|
314
|
+
template: `
|
|
315
|
+
<app-mediasfu-generic
|
|
316
|
+
[PrejoinPage]="PreJoinPage"
|
|
317
|
+
[credentials]="credentials">
|
|
318
|
+
</app-mediasfu-generic>
|
|
319
|
+
`,
|
|
320
|
+
})
|
|
321
|
+
export class AppComponent {
|
|
322
|
+
PreJoinPage = PreJoinPage;
|
|
323
|
+
credentials = {
|
|
324
|
+
apiUserName: 'your_username',
|
|
325
|
+
apiKey: 'your_api_key',
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Step 3: Run Your App
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
ng serve
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**That's it!** You now have a fully functional video conferencing app with:
|
|
337
|
+
- β
Video and audio streaming
|
|
338
|
+
- β
Screen sharing
|
|
339
|
+
- β
Chat messaging
|
|
340
|
+
- β
Participant management
|
|
341
|
+
- β
Recording capabilities
|
|
342
|
+
- β
Breakout rooms
|
|
343
|
+
- β
Polls and whiteboards
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## Understanding MediaSFU Architecture <a name="understanding-mediasfu-architecture"></a>
|
|
348
|
+
|
|
349
|
+
Before diving deeper, let's understand how MediaSFU is structured.
|
|
350
|
+
|
|
351
|
+
### The Three-Layer Architecture
|
|
352
|
+
|
|
353
|
+
```
|
|
354
|
+
βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
355
|
+
β Your Angular Application β
|
|
356
|
+
β (components, services, business logic) β
|
|
357
|
+
βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
358
|
+
β
|
|
359
|
+
βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
360
|
+
β MediaSFU Components Layer β
|
|
361
|
+
β (MediasfuGeneric, MediasfuBroadcast, etc.) β
|
|
362
|
+
β - Pre-built UI components β
|
|
363
|
+
β - Event handling β
|
|
364
|
+
β - State management (RxJS) β
|
|
365
|
+
βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
366
|
+
β
|
|
367
|
+
βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
368
|
+
β MediaSFU Core Methods Layer β
|
|
369
|
+
β (Stream control, room management, β
|
|
370
|
+
β WebRTC handling, socket communication) β
|
|
371
|
+
βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
372
|
+
β
|
|
373
|
+
βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
374
|
+
β MediaSFU Backend Services β
|
|
375
|
+
β (MediaSFU Cloud or Community Edition) β
|
|
376
|
+
βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Key Concepts
|
|
193
380
|
|
|
194
|
-
|
|
381
|
+
#### 1. **Event Room Types**
|
|
382
|
+
|
|
383
|
+
MediaSFU provides 5 specialized room types, each optimized for specific use cases:
|
|
384
|
+
|
|
385
|
+
| Room Type | Best For | Key Features |
|
|
386
|
+
|-----------|----------|--------------|
|
|
387
|
+
| **MediasfuGeneric** | General purpose meetings | Flexible layout, all features enabled |
|
|
388
|
+
| **MediasfuBroadcast** | Live streaming events | Optimized for one-to-many communication |
|
|
389
|
+
| **MediasfuWebinar** | Educational sessions | Presenter focus, Q&A features |
|
|
390
|
+
| **MediasfuConference** | Business meetings | Equal participant layout, collaboration tools |
|
|
391
|
+
| **MediasfuChat** | Interactive discussions | Chat-first interface, quick connections |
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
// Choose the right room type for your use case
|
|
395
|
+
import {
|
|
396
|
+
MediasfuWebinar,
|
|
397
|
+
MediasfuBroadcast,
|
|
398
|
+
MediasfuConference
|
|
399
|
+
} from 'mediasfu-angular';
|
|
400
|
+
|
|
401
|
+
@Component({
|
|
402
|
+
// For a webinar
|
|
403
|
+
template: `<app-mediasfu-webinar [credentials]="credentials"></app-mediasfu-webinar>`,
|
|
404
|
+
// For a broadcast
|
|
405
|
+
// template: `<app-mediasfu-broadcast [credentials]="credentials"></app-mediasfu-broadcast>`,
|
|
406
|
+
// For a conference
|
|
407
|
+
// template: `<app-mediasfu-conference [credentials]="credentials"></app-mediasfu-conference>`,
|
|
408
|
+
})
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
#### 2. **The Three Usage Modes**
|
|
412
|
+
|
|
413
|
+
MediaSFU offers three progressive levels of customization:
|
|
414
|
+
|
|
415
|
+
##### Mode 1: Default UI (Simplest)
|
|
416
|
+
Use MediaSFU's complete pre-built interface - perfect for rapid development.
|
|
195
417
|
|
|
196
418
|
```typescript
|
|
197
|
-
// app.component.ts
|
|
198
419
|
import { Component } from '@angular/core';
|
|
199
420
|
import { MediasfuGeneric } from 'mediasfu-angular';
|
|
200
421
|
|
|
@@ -202,12 +423,1320 @@ import { MediasfuGeneric } from 'mediasfu-angular';
|
|
|
202
423
|
selector: 'app-root',
|
|
203
424
|
standalone: true,
|
|
204
425
|
imports: [MediasfuGeneric],
|
|
426
|
+
template: `<app-mediasfu-generic [credentials]="credentials"></app-mediasfu-generic>`,
|
|
427
|
+
})
|
|
428
|
+
export class AppComponent {
|
|
429
|
+
credentials = { apiUserName: 'username', apiKey: 'key' };
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**When to use:**
|
|
434
|
+
- β
Prototyping or MVP development
|
|
435
|
+
- β
Need a production-ready UI quickly
|
|
436
|
+
- β
Standard video conferencing features are sufficient
|
|
437
|
+
|
|
438
|
+
##### Mode 2: Custom UI with MediaSFU Backend (Most Flexible)
|
|
439
|
+
Build your own UI while using MediaSFU's powerful backend infrastructure.
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
import { Component, OnInit } from '@angular/core';
|
|
443
|
+
import { MediasfuGeneric } from 'mediasfu-angular';
|
|
444
|
+
|
|
445
|
+
@Component({
|
|
446
|
+
selector: 'app-root',
|
|
447
|
+
standalone: true,
|
|
448
|
+
imports: [MediasfuGeneric, CommonModule],
|
|
205
449
|
template: `
|
|
206
|
-
<app-mediasfu-generic
|
|
450
|
+
<app-mediasfu-generic
|
|
451
|
+
[returnUI]="false"
|
|
452
|
+
[sourceParameters]="sourceParameters"
|
|
453
|
+
[updateSourceParameters]="updateSourceParameters.bind(this)"
|
|
454
|
+
[credentials]="credentials"
|
|
455
|
+
[noUIPreJoinOptions]="preJoinOptions">
|
|
456
|
+
</app-mediasfu-generic>
|
|
457
|
+
|
|
458
|
+
<!-- Your custom UI -->
|
|
459
|
+
@if (sourceParameters) {
|
|
460
|
+
<div class="custom-controls">
|
|
461
|
+
<button (click)="toggleVideo()">
|
|
462
|
+
{{ sourceParameters.videoAlreadyOn ? 'Stop Video' : 'Start Video' }}
|
|
463
|
+
</button>
|
|
464
|
+
<button (click)="toggleAudio()">
|
|
465
|
+
{{ sourceParameters.audioAlreadyOn ? 'Mute' : 'Unmute' }}
|
|
466
|
+
</button>
|
|
467
|
+
<button (click)="toggleScreenShare()">
|
|
468
|
+
{{ sourceParameters.screenAlreadyOn ? 'Stop Sharing' : 'Share Screen' }}
|
|
469
|
+
</button>
|
|
470
|
+
</div>
|
|
471
|
+
}
|
|
207
472
|
`,
|
|
208
473
|
})
|
|
209
|
-
export class AppComponent {
|
|
474
|
+
export class AppComponent implements OnInit {
|
|
475
|
+
sourceParameters: any = null;
|
|
476
|
+
credentials = { apiUserName: 'username', apiKey: 'key' };
|
|
477
|
+
preJoinOptions = {
|
|
478
|
+
action: 'create',
|
|
479
|
+
userName: 'Your Name',
|
|
480
|
+
capacity: 50,
|
|
481
|
+
duration: 30,
|
|
482
|
+
eventType: 'conference'
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
updateSourceParameters(params: any) {
|
|
486
|
+
this.sourceParameters = params;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
toggleVideo() {
|
|
490
|
+
this.sourceParameters?.clickVideo({ parameters: this.sourceParameters });
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
toggleAudio() {
|
|
494
|
+
this.sourceParameters?.clickAudio({ parameters: this.sourceParameters });
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
toggleScreenShare() {
|
|
498
|
+
this.sourceParameters?.clickScreenShare({ parameters: this.sourceParameters });
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
**When to use:**
|
|
504
|
+
- β
Need complete control over UI/UX
|
|
505
|
+
- β
Building a custom branded experience
|
|
506
|
+
- β
Integrating into existing app design
|
|
507
|
+
|
|
508
|
+
##### Mode 3: Component Replacement (Balanced)
|
|
509
|
+
Replace specific MediaSFU components while keeping the rest of the infrastructure.
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
import { Component } from '@angular/core';
|
|
513
|
+
import {
|
|
514
|
+
MediasfuGeneric,
|
|
515
|
+
FlexibleVideo,
|
|
516
|
+
FlexibleGrid
|
|
517
|
+
} from 'mediasfu-angular';
|
|
518
|
+
|
|
519
|
+
@Component({
|
|
520
|
+
selector: 'app-custom-main',
|
|
521
|
+
standalone: true,
|
|
522
|
+
imports: [FlexibleVideo, FlexibleGrid, CommonModule],
|
|
523
|
+
template: `
|
|
524
|
+
<div class="custom-layout">
|
|
525
|
+
<!-- Custom header -->
|
|
526
|
+
<div class="custom-header">
|
|
527
|
+
<h1>{{ parameters.roomName }}</h1>
|
|
528
|
+
<span>{{ parameters.participants.length }} participants</span>
|
|
529
|
+
</div>
|
|
530
|
+
|
|
531
|
+
<!-- Use MediaSFU's components in your layout -->
|
|
532
|
+
<app-flexible-video
|
|
533
|
+
[customWidth]="windowWidth"
|
|
534
|
+
[customHeight]="600"
|
|
535
|
+
[parameters]="parameters">
|
|
536
|
+
</app-flexible-video>
|
|
537
|
+
|
|
538
|
+
<app-flexible-grid
|
|
539
|
+
[customWidth]="windowWidth"
|
|
540
|
+
[customHeight]="400"
|
|
541
|
+
[parameters]="parameters">
|
|
542
|
+
</app-flexible-grid>
|
|
543
|
+
|
|
544
|
+
<!-- Custom footer -->
|
|
545
|
+
<div class="custom-footer">
|
|
546
|
+
<button (click)="toggleVideo()">
|
|
547
|
+
{{ parameters.videoAlreadyOn ? 'Stop Video' : 'Start Video' }}
|
|
548
|
+
</button>
|
|
549
|
+
</div>
|
|
550
|
+
</div>
|
|
551
|
+
`,
|
|
552
|
+
styles: [`
|
|
553
|
+
.custom-layout {
|
|
554
|
+
display: flex;
|
|
555
|
+
flex-direction: column;
|
|
556
|
+
height: 100vh;
|
|
557
|
+
}
|
|
558
|
+
.custom-header {
|
|
559
|
+
padding: 20px;
|
|
560
|
+
background: #1976d2;
|
|
561
|
+
color: white;
|
|
562
|
+
}
|
|
563
|
+
`]
|
|
564
|
+
})
|
|
565
|
+
export class CustomMainComponent {
|
|
566
|
+
parameters: any;
|
|
567
|
+
windowWidth = window.innerWidth;
|
|
568
|
+
|
|
569
|
+
toggleVideo() {
|
|
570
|
+
this.parameters?.clickVideo({ parameters: this.parameters });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
@Component({
|
|
575
|
+
selector: 'app-root',
|
|
576
|
+
standalone: true,
|
|
577
|
+
imports: [MediasfuGeneric],
|
|
578
|
+
template: `
|
|
579
|
+
<app-mediasfu-generic
|
|
580
|
+
[credentials]="credentials"
|
|
581
|
+
[PrejoinPage]="PreJoinPage"
|
|
582
|
+
[customComponent]="CustomMainComponent">
|
|
583
|
+
</app-mediasfu-generic>
|
|
584
|
+
`,
|
|
585
|
+
})
|
|
586
|
+
export class AppComponent {
|
|
587
|
+
PreJoinPage = PreJoinPage;
|
|
588
|
+
CustomMainComponent = CustomMainComponent;
|
|
589
|
+
credentials = { apiUserName: 'username', apiKey: 'key' };
|
|
590
|
+
}
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
**When to use:**
|
|
594
|
+
- β
Need custom main interface but want to keep MediaSFU's components
|
|
595
|
+
- β
Partial customization with minimal effort
|
|
596
|
+
- β
Want to maintain MediaSFU's functionality while customizing layout
|
|
597
|
+
|
|
598
|
+
#### 3. **Parameters: Your Control Center**
|
|
599
|
+
|
|
600
|
+
The `sourceParameters` object (or `parameters` in custom components) is your gateway to all MediaSFU functionality. It's powered by RxJS BehaviorSubjects for reactive state management:
|
|
601
|
+
|
|
602
|
+
```typescript
|
|
603
|
+
// Available in sourceParameters or parameters object
|
|
604
|
+
{
|
|
605
|
+
// Media Controls (Methods)
|
|
606
|
+
clickVideo: (options) => {},
|
|
607
|
+
clickAudio: (options) => {},
|
|
608
|
+
clickScreenShare: (options) => {},
|
|
609
|
+
|
|
610
|
+
// Room State (BehaviorSubject values)
|
|
611
|
+
roomName: 'meeting-123',
|
|
612
|
+
participants: [...],
|
|
613
|
+
allVideoStreams: [...],
|
|
614
|
+
allAudioStreams: [...],
|
|
615
|
+
|
|
616
|
+
// UI State (BehaviorSubject values)
|
|
617
|
+
videoAlreadyOn: false,
|
|
618
|
+
audioAlreadyOn: false,
|
|
619
|
+
screenAlreadyOn: false,
|
|
620
|
+
|
|
621
|
+
// Update Functions (BehaviorSubject next())
|
|
622
|
+
updateVideoAlreadyOn: (value) => {},
|
|
623
|
+
updateAudioAlreadyOn: (value) => {},
|
|
624
|
+
|
|
625
|
+
// And 200+ more properties and methods...
|
|
626
|
+
}
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
**Access patterns:**
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
// In Mode 1 (Default UI): Parameters are managed internally
|
|
633
|
+
// You don't need to access them directly
|
|
634
|
+
|
|
635
|
+
// In Mode 2 (Custom UI): Access via sourceParameters
|
|
636
|
+
sourceParameters?.clickVideo({ parameters: sourceParameters });
|
|
637
|
+
|
|
638
|
+
// In Mode 3 (Component Replacement): Passed to your custom component
|
|
639
|
+
@Component({
|
|
640
|
+
template: `<button (click)="toggleVideo()">Toggle</button>`
|
|
641
|
+
})
|
|
642
|
+
export class CustomComponent {
|
|
643
|
+
@Input() parameters: any;
|
|
644
|
+
|
|
645
|
+
toggleVideo() {
|
|
646
|
+
this.parameters.clickVideo({ parameters: this.parameters });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Subscribing to reactive state changes
|
|
651
|
+
sourceParameters.participants.subscribe((participants) => {
|
|
652
|
+
console.log('Participants updated:', participants);
|
|
653
|
+
});
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
---
|
|
657
|
+
|
|
658
|
+
## Core Concepts & Components <a name="core-concepts--components"></a>
|
|
659
|
+
|
|
660
|
+
Now that you understand the architecture, let's explore the building blocks.
|
|
661
|
+
|
|
662
|
+
### 1. Display Components: Building Your Video Layout
|
|
663
|
+
|
|
664
|
+
MediaSFU provides powerful components for organizing and displaying media streams.
|
|
665
|
+
|
|
666
|
+
#### Primary Layout Components
|
|
667
|
+
|
|
668
|
+
**FlexibleVideo** - Main video display area
|
|
669
|
+
|
|
670
|
+
```typescript
|
|
671
|
+
import { FlexibleVideo } from 'mediasfu-angular';
|
|
672
|
+
|
|
673
|
+
@Component({
|
|
674
|
+
template: `
|
|
675
|
+
<app-flexible-video
|
|
676
|
+
[customWidth]="windowWidth"
|
|
677
|
+
[customHeight]="600"
|
|
678
|
+
[parameters]="parameters">
|
|
679
|
+
</app-flexible-video>
|
|
680
|
+
`
|
|
681
|
+
})
|
|
682
|
+
```
|
|
683
|
+
- Automatically handles main presenter or screen share
|
|
684
|
+
- Smooth transitions between different video sources
|
|
685
|
+
- Responsive sizing
|
|
686
|
+
|
|
687
|
+
**FlexibleGrid** - Participant grid layout
|
|
688
|
+
|
|
689
|
+
```typescript
|
|
690
|
+
import { FlexibleGrid } from 'mediasfu-angular';
|
|
691
|
+
|
|
692
|
+
@Component({
|
|
693
|
+
template: `
|
|
694
|
+
<app-flexible-grid
|
|
695
|
+
[customWidth]="windowWidth"
|
|
696
|
+
[customHeight]="800"
|
|
697
|
+
[parameters]="parameters">
|
|
698
|
+
</app-flexible-grid>
|
|
699
|
+
`
|
|
700
|
+
})
|
|
701
|
+
```
|
|
702
|
+
- Intelligent grid sizing (2x2, 3x3, 4x4, etc.)
|
|
703
|
+
- Pagination for large participant lists
|
|
704
|
+
- Automatic reflow on window resize
|
|
705
|
+
|
|
706
|
+
**AudioGrid** - Audio-only participants
|
|
707
|
+
|
|
708
|
+
```typescript
|
|
709
|
+
import { AudioGrid } from 'mediasfu-angular';
|
|
710
|
+
|
|
711
|
+
@Component({
|
|
712
|
+
template: `<app-audio-grid [parameters]="parameters"></app-audio-grid>`
|
|
713
|
+
})
|
|
714
|
+
```
|
|
715
|
+
- Displays participants without video
|
|
716
|
+
- Audio level indicators
|
|
717
|
+
- Compact layout for efficiency
|
|
718
|
+
|
|
719
|
+
#### Container Components
|
|
720
|
+
|
|
721
|
+
| Component | Purpose | Use Case |
|
|
722
|
+
|-----------|---------|----------|
|
|
723
|
+
| **MainContainerComponent** | Primary content wrapper | Wraps all main content areas |
|
|
724
|
+
| **MainAspectComponent** | Aspect ratio container | Maintains proper video proportions |
|
|
725
|
+
| **MainScreenComponent** | Screen layout manager | Organizes screen regions |
|
|
726
|
+
| **SubAspectComponent** | Secondary content container | For picture-in-picture, sidebars |
|
|
727
|
+
|
|
728
|
+
**Example: Building a custom layout**
|
|
729
|
+
|
|
730
|
+
```typescript
|
|
731
|
+
import { Component } from '@angular/core';
|
|
732
|
+
import {
|
|
733
|
+
MainContainerComponent,
|
|
734
|
+
FlexibleVideo,
|
|
735
|
+
FlexibleGrid,
|
|
736
|
+
AudioGrid
|
|
737
|
+
} from 'mediasfu-angular';
|
|
738
|
+
|
|
739
|
+
@Component({
|
|
740
|
+
selector: 'app-custom-layout',
|
|
741
|
+
standalone: true,
|
|
742
|
+
imports: [
|
|
743
|
+
MainContainerComponent,
|
|
744
|
+
FlexibleVideo,
|
|
745
|
+
FlexibleGrid,
|
|
746
|
+
AudioGrid,
|
|
747
|
+
CommonModule
|
|
748
|
+
],
|
|
749
|
+
template: `
|
|
750
|
+
<app-main-container-component>
|
|
751
|
+
<div class="layout-container">
|
|
752
|
+
<!-- Main video area -->
|
|
753
|
+
<div class="main-video">
|
|
754
|
+
<app-flexible-video
|
|
755
|
+
[customWidth]="windowWidth"
|
|
756
|
+
[customHeight]="windowHeight * 0.6"
|
|
757
|
+
[parameters]="parameters">
|
|
758
|
+
</app-flexible-video>
|
|
759
|
+
</div>
|
|
760
|
+
|
|
761
|
+
<!-- Participant grid -->
|
|
762
|
+
<div class="participant-grid">
|
|
763
|
+
<app-flexible-grid
|
|
764
|
+
[customWidth]="windowWidth"
|
|
765
|
+
[customHeight]="windowHeight * 0.3"
|
|
766
|
+
[parameters]="parameters">
|
|
767
|
+
</app-flexible-grid>
|
|
768
|
+
</div>
|
|
769
|
+
|
|
770
|
+
<!-- Audio-only participants -->
|
|
771
|
+
<div class="audio-participants">
|
|
772
|
+
<app-audio-grid [parameters]="parameters"></app-audio-grid>
|
|
773
|
+
</div>
|
|
774
|
+
</div>
|
|
775
|
+
</app-main-container-component>
|
|
776
|
+
`,
|
|
777
|
+
styles: [`
|
|
778
|
+
.layout-container {
|
|
779
|
+
display: flex;
|
|
780
|
+
flex-direction: column;
|
|
781
|
+
height: 100vh;
|
|
782
|
+
}
|
|
783
|
+
.main-video { flex: 3; }
|
|
784
|
+
.participant-grid { flex: 2; }
|
|
785
|
+
.audio-participants { height: 80px; }
|
|
786
|
+
`]
|
|
787
|
+
})
|
|
788
|
+
export class CustomLayoutComponent {
|
|
789
|
+
@Input() parameters: any;
|
|
790
|
+
windowWidth = window.innerWidth;
|
|
791
|
+
windowHeight = window.innerHeight;
|
|
792
|
+
}
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
### 2. Control Components: User Interactions
|
|
796
|
+
|
|
797
|
+
**ControlButtonsComponent** - Standard control bar
|
|
798
|
+
|
|
799
|
+
```typescript
|
|
800
|
+
import { ControlButtonsComponent } from 'mediasfu-angular';
|
|
801
|
+
|
|
802
|
+
@Component({
|
|
803
|
+
template: `
|
|
804
|
+
<app-control-buttons-component
|
|
805
|
+
[parameters]="parameters"
|
|
806
|
+
[position]="'bottom'">
|
|
807
|
+
</app-control-buttons-component>
|
|
808
|
+
`
|
|
809
|
+
})
|
|
210
810
|
```
|
|
811
|
+
Includes: mute, video, screenshare, participants, chat, settings, etc.
|
|
812
|
+
|
|
813
|
+
**ControlButtonsAltComponent** - Alternative layout
|
|
814
|
+
|
|
815
|
+
```typescript
|
|
816
|
+
import { ControlButtonsAltComponent } from 'mediasfu-angular';
|
|
817
|
+
|
|
818
|
+
@Component({
|
|
819
|
+
template: `
|
|
820
|
+
<app-control-buttons-alt-component
|
|
821
|
+
[parameters]="parameters"
|
|
822
|
+
[position]="'top'">
|
|
823
|
+
</app-control-buttons-alt-component>
|
|
824
|
+
`
|
|
825
|
+
})
|
|
826
|
+
```
|
|
827
|
+
Different button arrangement optimized for specific layouts.
|
|
828
|
+
|
|
829
|
+
**ControlButtonsComponentTouch** - Touch-optimized controls
|
|
830
|
+
|
|
831
|
+
```typescript
|
|
832
|
+
import { ControlButtonsComponentTouch } from 'mediasfu-angular';
|
|
833
|
+
|
|
834
|
+
@Component({
|
|
835
|
+
template: `
|
|
836
|
+
<app-control-buttons-component-touch
|
|
837
|
+
[parameters]="parameters">
|
|
838
|
+
</app-control-buttons-component-touch>
|
|
839
|
+
`
|
|
840
|
+
})
|
|
841
|
+
```
|
|
842
|
+
Floating action buttons optimized for mobile/tablet interfaces.
|
|
843
|
+
|
|
844
|
+
### 3. Modal Components: Feature Interfaces
|
|
845
|
+
|
|
846
|
+
MediaSFU includes modals for various features:
|
|
847
|
+
|
|
848
|
+
```typescript
|
|
849
|
+
import {
|
|
850
|
+
ParticipantsModal,
|
|
851
|
+
MessagesModal,
|
|
852
|
+
SettingsModal,
|
|
853
|
+
DisplaySettingsModal,
|
|
854
|
+
RecordingModal,
|
|
855
|
+
PollModal,
|
|
856
|
+
BreakoutRoomsModal
|
|
857
|
+
} from 'mediasfu-angular';
|
|
858
|
+
|
|
859
|
+
// These are automatically rendered when enabled
|
|
860
|
+
// Control their visibility via parameters
|
|
861
|
+
parameters.updateIsParticipantsModalVisible.next(true);
|
|
862
|
+
parameters.updateIsMessagesModalVisible.next(true);
|
|
863
|
+
parameters.updateIsSettingsModalVisible.next(true);
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
Available modals:
|
|
867
|
+
- **ParticipantsModal** - Participant list management
|
|
868
|
+
- **MessagesModal** - Chat interface
|
|
869
|
+
- **SettingsModal** - Event and room settings
|
|
870
|
+
- **DisplaySettingsModal** - Layout and display options
|
|
871
|
+
- **RecordingModal** - Recording controls and settings
|
|
872
|
+
- **PollModal** - Create and manage polls
|
|
873
|
+
- **BreakoutRoomsModal** - Breakout room management
|
|
874
|
+
- **MediaSettingsModal** - Camera/microphone selection
|
|
875
|
+
- **BackgroundModal** - Virtual background settings
|
|
876
|
+
- **ConfigureWhiteboardModal** - Whiteboard configuration
|
|
877
|
+
|
|
878
|
+
**Example: Programmatically showing modals**
|
|
879
|
+
|
|
880
|
+
```typescript
|
|
881
|
+
@Component({
|
|
882
|
+
selector: 'app-custom-toolbar',
|
|
883
|
+
template: `
|
|
884
|
+
<div class="custom-toolbar">
|
|
885
|
+
<button (click)="showParticipants()">
|
|
886
|
+
Show Participants ({{ participantCount }})
|
|
887
|
+
</button>
|
|
888
|
+
|
|
889
|
+
<button (click)="openChat()">
|
|
890
|
+
Open Chat
|
|
891
|
+
</button>
|
|
892
|
+
|
|
893
|
+
<button (click)="createPoll()">
|
|
894
|
+
Create Poll
|
|
895
|
+
</button>
|
|
896
|
+
</div>
|
|
897
|
+
`
|
|
898
|
+
})
|
|
899
|
+
export class CustomToolbarComponent {
|
|
900
|
+
@Input() parameters: any;
|
|
901
|
+
|
|
902
|
+
get participantCount() {
|
|
903
|
+
return this.parameters?.participants?.length || 0;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
showParticipants() {
|
|
907
|
+
this.parameters?.updateIsParticipantsModalVisible.next(true);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
openChat() {
|
|
911
|
+
this.parameters?.updateIsMessagesModalVisible.next(true);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
createPoll() {
|
|
915
|
+
this.parameters?.launchPoll?.launchPoll({ parameters: this.parameters });
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
### 4. Video Cards: Individual Participant Display
|
|
921
|
+
|
|
922
|
+
**VideoCard** - Individual participant video element
|
|
923
|
+
|
|
924
|
+
```typescript
|
|
925
|
+
import { VideoCard } from 'mediasfu-angular';
|
|
926
|
+
|
|
927
|
+
@Component({
|
|
928
|
+
template: `
|
|
929
|
+
<app-video-card
|
|
930
|
+
[videoStream]="participantStream"
|
|
931
|
+
[remoteProducerId]="'producer-id'"
|
|
932
|
+
[eventType]="'conference'"
|
|
933
|
+
[forceFullDisplay]="false"
|
|
934
|
+
[participant]="participantObject"
|
|
935
|
+
[backgroundColor]="'#000000'"
|
|
936
|
+
[showControls]="true"
|
|
937
|
+
[showInfo]="true"
|
|
938
|
+
[name]="'Participant Name'"
|
|
939
|
+
[parameters]="parameters">
|
|
940
|
+
</app-video-card>
|
|
941
|
+
`
|
|
942
|
+
})
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
**AudioCard** - Individual audio-only participant
|
|
946
|
+
|
|
947
|
+
```typescript
|
|
948
|
+
import { AudioCard } from 'mediasfu-angular';
|
|
949
|
+
|
|
950
|
+
@Component({
|
|
951
|
+
template: `
|
|
952
|
+
<app-audio-card
|
|
953
|
+
[name]="'Participant Name'"
|
|
954
|
+
[barColor]="'#4CAF50'"
|
|
955
|
+
[textColor]="'#FFFFFF'"
|
|
956
|
+
[customStyle]="{ borderRadius: '10px' }"
|
|
957
|
+
[controlsPosition]="'topLeft'"
|
|
958
|
+
[infoPosition]="'topRight'"
|
|
959
|
+
[participant]="participantObject"
|
|
960
|
+
[parameters]="parameters">
|
|
961
|
+
</app-audio-card>
|
|
962
|
+
`
|
|
963
|
+
})
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
**MiniCard** - Compact participant display (for grids)
|
|
967
|
+
|
|
968
|
+
```typescript
|
|
969
|
+
import { MiniCard } from 'mediasfu-angular';
|
|
970
|
+
|
|
971
|
+
@Component({
|
|
972
|
+
template: `
|
|
973
|
+
<app-mini-card
|
|
974
|
+
[participant]="participantObject"
|
|
975
|
+
[showControls]="false"
|
|
976
|
+
[parameters]="parameters">
|
|
977
|
+
</app-mini-card>
|
|
978
|
+
`
|
|
979
|
+
})
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
**Example: Custom Video Card**
|
|
983
|
+
|
|
984
|
+
```typescript
|
|
985
|
+
@Component({
|
|
986
|
+
selector: 'app-my-custom-video-card',
|
|
987
|
+
standalone: true,
|
|
988
|
+
template: `
|
|
989
|
+
<div class="custom-video-card">
|
|
990
|
+
<video
|
|
991
|
+
#videoElement
|
|
992
|
+
[srcObject]="stream"
|
|
993
|
+
autoplay
|
|
994
|
+
[muted]="true"
|
|
995
|
+
playsinline>
|
|
996
|
+
</video>
|
|
997
|
+
|
|
998
|
+
<div class="participant-info">
|
|
999
|
+
{{ participant.name }}
|
|
1000
|
+
@if (participant.muted) {
|
|
1001
|
+
<span>π</span>
|
|
1002
|
+
}
|
|
1003
|
+
</div>
|
|
1004
|
+
</div>
|
|
1005
|
+
`,
|
|
1006
|
+
styles: [`
|
|
1007
|
+
.custom-video-card {
|
|
1008
|
+
border: 3px solid #00ff88;
|
|
1009
|
+
border-radius: 15px;
|
|
1010
|
+
overflow: hidden;
|
|
1011
|
+
position: relative;
|
|
1012
|
+
}
|
|
1013
|
+
video {
|
|
1014
|
+
width: 100%;
|
|
1015
|
+
height: 100%;
|
|
1016
|
+
object-fit: cover;
|
|
1017
|
+
}
|
|
1018
|
+
.participant-info {
|
|
1019
|
+
position: absolute;
|
|
1020
|
+
bottom: 0;
|
|
1021
|
+
left: 0;
|
|
1022
|
+
right: 0;
|
|
1023
|
+
background: rgba(0, 255, 136, 0.8);
|
|
1024
|
+
color: black;
|
|
1025
|
+
padding: 8px;
|
|
1026
|
+
font-weight: bold;
|
|
1027
|
+
}
|
|
1028
|
+
`]
|
|
1029
|
+
})
|
|
1030
|
+
export class MyCustomVideoCardComponent {
|
|
1031
|
+
@Input() stream!: MediaStream;
|
|
1032
|
+
@Input() participant!: any;
|
|
1033
|
+
@Input() parameters!: any;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// Use it in MediasfuGeneric
|
|
1037
|
+
@Component({
|
|
1038
|
+
template: `
|
|
1039
|
+
<app-mediasfu-generic
|
|
1040
|
+
[credentials]="credentials"
|
|
1041
|
+
[customVideoCard]="CustomVideoCard">
|
|
1042
|
+
</app-mediasfu-generic>
|
|
1043
|
+
`
|
|
1044
|
+
})
|
|
1045
|
+
export class AppComponent {
|
|
1046
|
+
CustomVideoCard = MyCustomVideoCardComponent;
|
|
1047
|
+
credentials = { apiUserName: 'username', apiKey: 'key' };
|
|
1048
|
+
}
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
---
|
|
1052
|
+
|
|
1053
|
+
## Working with Methods <a name="working-with-methods"></a>
|
|
1054
|
+
|
|
1055
|
+
MediaSFU provides 200+ methods for controlling every aspect of your real-time communication experience. Let's explore the most important categories.
|
|
1056
|
+
|
|
1057
|
+
### Media Control Methods
|
|
1058
|
+
|
|
1059
|
+
#### Video Control
|
|
1060
|
+
|
|
1061
|
+
```typescript
|
|
1062
|
+
// Toggle video on/off
|
|
1063
|
+
parameters.clickVideo({ parameters });
|
|
1064
|
+
|
|
1065
|
+
// Switch camera (front/back on mobile)
|
|
1066
|
+
parameters.switchVideoAlt({ parameters });
|
|
1067
|
+
|
|
1068
|
+
// Switch to specific camera by ID
|
|
1069
|
+
const cameras = await parameters.getMediaDevicesList('videoinput');
|
|
1070
|
+
parameters.switchUserVideo({
|
|
1071
|
+
videoPreference: cameras[1].deviceId,
|
|
1072
|
+
parameters
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
// Get current video state
|
|
1076
|
+
const isVideoOn = parameters.videoAlreadyOn;
|
|
1077
|
+
|
|
1078
|
+
// Subscribe to video state changes (RxJS)
|
|
1079
|
+
parameters.videoAlreadyOn.subscribe((isOn: boolean) => {
|
|
1080
|
+
console.log('Video is now:', isOn ? 'ON' : 'OFF');
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
// Update video state programmatically
|
|
1084
|
+
parameters.updateVideoAlreadyOn.next(true);
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
#### Audio Control
|
|
1088
|
+
|
|
1089
|
+
```typescript
|
|
1090
|
+
// Toggle audio on/off
|
|
1091
|
+
parameters.clickAudio({ parameters });
|
|
1092
|
+
|
|
1093
|
+
// Switch microphone
|
|
1094
|
+
const microphones = await parameters.getMediaDevicesList('audioinput');
|
|
1095
|
+
parameters.switchUserAudio({
|
|
1096
|
+
audioPreference: microphones[1].deviceId,
|
|
1097
|
+
parameters
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
// Get current audio state
|
|
1101
|
+
const isAudioOn = parameters.audioAlreadyOn;
|
|
1102
|
+
const hasHostPermission = parameters.micAction; // Host approval status
|
|
1103
|
+
|
|
1104
|
+
// Subscribe to audio state changes
|
|
1105
|
+
parameters.audioAlreadyOn.subscribe((isOn: boolean) => {
|
|
1106
|
+
console.log('Audio is now:', isOn ? 'ON' : 'OFF');
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
// Mute/unmute specific participant (host only)
|
|
1110
|
+
parameters.controlMedia({
|
|
1111
|
+
participantId: 'participant-id',
|
|
1112
|
+
participantName: 'John Doe',
|
|
1113
|
+
type: 'audio',
|
|
1114
|
+
socket: parameters.socket,
|
|
1115
|
+
roomName: parameters.roomName
|
|
1116
|
+
});
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
#### Screen Sharing
|
|
1120
|
+
|
|
1121
|
+
```typescript
|
|
1122
|
+
// Start screen sharing
|
|
1123
|
+
parameters.clickScreenShare({ parameters });
|
|
1124
|
+
|
|
1125
|
+
// Stop screen sharing
|
|
1126
|
+
parameters.stopShareScreen({ parameters });
|
|
1127
|
+
|
|
1128
|
+
// Check if screen sharing is available
|
|
1129
|
+
const canShare = await parameters.checkScreenShare({ parameters });
|
|
1130
|
+
|
|
1131
|
+
// Get screen share state
|
|
1132
|
+
const isSharing = parameters.screenAlreadyOn;
|
|
1133
|
+
const shareAudio = parameters.shareScreenStarted; // Sharing with audio
|
|
1134
|
+
|
|
1135
|
+
// Subscribe to screen share state
|
|
1136
|
+
parameters.screenAlreadyOn.subscribe((isSharing: boolean) => {
|
|
1137
|
+
console.log('Screen sharing:', isSharing ? 'ACTIVE' : 'INACTIVE');
|
|
1138
|
+
});
|
|
1139
|
+
```
|
|
1140
|
+
|
|
1141
|
+
### Media Device and Stream Utility Methods
|
|
1142
|
+
|
|
1143
|
+
#### Get Available Media Devices
|
|
1144
|
+
|
|
1145
|
+
```typescript
|
|
1146
|
+
// Get available cameras
|
|
1147
|
+
const cameras = await parameters.getMediaDevicesList('videoinput');
|
|
1148
|
+
cameras.forEach(camera => {
|
|
1149
|
+
console.log(`Camera: ${camera.label} (${camera.deviceId})`);
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
// Get available microphones
|
|
1153
|
+
const microphones = await parameters.getMediaDevicesList('audioinput');
|
|
1154
|
+
microphones.forEach(mic => {
|
|
1155
|
+
console.log(`Microphone: ${mic.label} (${mic.deviceId})`);
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
// Building a device selector UI
|
|
1159
|
+
@Component({
|
|
1160
|
+
selector: 'app-device-selector',
|
|
1161
|
+
template: `
|
|
1162
|
+
<div class="device-selector">
|
|
1163
|
+
<select (change)="onCameraChange($event)">
|
|
1164
|
+
<option value="">Select Camera</option>
|
|
1165
|
+
@for (camera of cameras; track camera.deviceId) {
|
|
1166
|
+
<option [value]="camera.deviceId">
|
|
1167
|
+
{{ camera.label }}
|
|
1168
|
+
</option>
|
|
1169
|
+
}
|
|
1170
|
+
</select>
|
|
1171
|
+
|
|
1172
|
+
<select (change)="onMicrophoneChange($event)">
|
|
1173
|
+
<option value="">Select Microphone</option>
|
|
1174
|
+
@for (mic of microphones; track mic.deviceId) {
|
|
1175
|
+
<option [value]="mic.deviceId">
|
|
1176
|
+
{{ mic.label }}
|
|
1177
|
+
</option>
|
|
1178
|
+
}
|
|
1179
|
+
</select>
|
|
1180
|
+
</div>
|
|
1181
|
+
`
|
|
1182
|
+
})
|
|
1183
|
+
export class DeviceSelectorComponent implements OnInit {
|
|
1184
|
+
@Input() parameters: any;
|
|
1185
|
+
cameras: MediaDeviceInfo[] = [];
|
|
1186
|
+
microphones: MediaDeviceInfo[] = [];
|
|
1187
|
+
|
|
1188
|
+
async ngOnInit() {
|
|
1189
|
+
await this.loadDevices();
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
async loadDevices() {
|
|
1193
|
+
this.cameras = await this.parameters.getMediaDevicesList('videoinput');
|
|
1194
|
+
this.microphones = await this.parameters.getMediaDevicesList('audioinput');
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
onCameraChange(event: any) {
|
|
1198
|
+
this.parameters.switchUserVideo({
|
|
1199
|
+
videoPreference: event.target.value,
|
|
1200
|
+
parameters: this.parameters
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
onMicrophoneChange(event: any) {
|
|
1205
|
+
this.parameters.switchUserAudio({
|
|
1206
|
+
audioPreference: event.target.value,
|
|
1207
|
+
parameters: this.parameters
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
#### Get Participant Media Streams
|
|
1214
|
+
|
|
1215
|
+
```typescript
|
|
1216
|
+
// Get participant video stream by ID
|
|
1217
|
+
const videoStream = await parameters.getParticipantMedia({
|
|
1218
|
+
id: 'producer-123',
|
|
1219
|
+
kind: 'video'
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
// Get participant audio stream by name
|
|
1223
|
+
const audioStream = await parameters.getParticipantMedia({
|
|
1224
|
+
name: 'John Doe',
|
|
1225
|
+
kind: 'audio'
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
// Example: Custom participant stream monitor
|
|
1229
|
+
@Component({
|
|
1230
|
+
selector: 'app-stream-monitor',
|
|
1231
|
+
template: `
|
|
1232
|
+
<div class="stream-monitor">
|
|
1233
|
+
<h3>Participant Streams</h3>
|
|
1234
|
+
@for (participant of participants; track participant.id) {
|
|
1235
|
+
<div class="participant-item">
|
|
1236
|
+
<span>{{ participant.name }}</span>
|
|
1237
|
+
<button (click)="viewStream(participant)">View Stream</button>
|
|
1238
|
+
<button (click)="monitorAudio(participant)">Monitor Audio</button>
|
|
1239
|
+
</div>
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
@if (selectedStream) {
|
|
1243
|
+
<div class="stream-viewer">
|
|
1244
|
+
<video #streamVideo autoplay playsinline></video>
|
|
1245
|
+
</div>
|
|
1246
|
+
}
|
|
1247
|
+
</div>
|
|
1248
|
+
`,
|
|
1249
|
+
styles: [`
|
|
1250
|
+
.stream-monitor { padding: 20px; }
|
|
1251
|
+
.participant-item {
|
|
1252
|
+
margin: 10px 0;
|
|
1253
|
+
display: flex;
|
|
1254
|
+
gap: 10px;
|
|
1255
|
+
align-items: center;
|
|
1256
|
+
}
|
|
1257
|
+
.stream-viewer video {
|
|
1258
|
+
width: 100%;
|
|
1259
|
+
max-width: 640px;
|
|
1260
|
+
border: 2px solid #1976d2;
|
|
1261
|
+
}
|
|
1262
|
+
`]
|
|
1263
|
+
})
|
|
1264
|
+
export class StreamMonitorComponent {
|
|
1265
|
+
@Input() parameters: any;
|
|
1266
|
+
@ViewChild('streamVideo') videoElement!: ElementRef<HTMLVideoElement>;
|
|
1267
|
+
|
|
1268
|
+
selectedStream: MediaStream | null = null;
|
|
1269
|
+
|
|
1270
|
+
get participants() {
|
|
1271
|
+
return this.parameters?.participants || [];
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
async viewStream(participant: any) {
|
|
1275
|
+
const stream = await this.parameters.getParticipantMedia({
|
|
1276
|
+
id: participant.videoID,
|
|
1277
|
+
name: participant.name,
|
|
1278
|
+
kind: 'video'
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
if (stream && this.videoElement) {
|
|
1282
|
+
this.selectedStream = stream;
|
|
1283
|
+
this.videoElement.nativeElement.srcObject = stream;
|
|
1284
|
+
} else {
|
|
1285
|
+
console.log('No video stream found for participant');
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
async monitorAudio(participant: any) {
|
|
1290
|
+
const stream = await this.parameters.getParticipantMedia({
|
|
1291
|
+
id: participant.audioID,
|
|
1292
|
+
name: participant.name,
|
|
1293
|
+
kind: 'audio'
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
if (stream) {
|
|
1297
|
+
// Create audio context for analysis
|
|
1298
|
+
const audioContext = new AudioContext();
|
|
1299
|
+
const analyser = audioContext.createAnalyser();
|
|
1300
|
+
const source = audioContext.createMediaStreamSource(stream);
|
|
1301
|
+
source.connect(analyser);
|
|
1302
|
+
|
|
1303
|
+
console.log('Monitoring audio for:', participant.name);
|
|
1304
|
+
// Add your audio analysis logic here
|
|
1305
|
+
} else {
|
|
1306
|
+
console.log('No audio stream found for participant');
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
```
|
|
1311
|
+
|
|
1312
|
+
### Participant Management Methods
|
|
1313
|
+
|
|
1314
|
+
```typescript
|
|
1315
|
+
// Get all participants
|
|
1316
|
+
const participants = parameters.participants;
|
|
1317
|
+
const participantCount = parameters.participantsCounter;
|
|
1318
|
+
|
|
1319
|
+
// Subscribe to participant changes
|
|
1320
|
+
parameters.participants.subscribe((participants: any[]) => {
|
|
1321
|
+
console.log('Participants updated:', participants);
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
// Filter participants
|
|
1325
|
+
const videoParticipants = participants.filter((p: any) => p.videoOn);
|
|
1326
|
+
const audioOnlyParticipants = participants.filter((p: any) => !p.videoOn);
|
|
1327
|
+
const mutedParticipants = participants.filter((p: any) => p.muted);
|
|
1328
|
+
|
|
1329
|
+
// Find specific participant
|
|
1330
|
+
const participant = participants.find((p: any) => p.name === 'John Doe');
|
|
1331
|
+
|
|
1332
|
+
// Remove participant from room (host only)
|
|
1333
|
+
parameters.disconnectUserInitiate({
|
|
1334
|
+
member: participantId,
|
|
1335
|
+
roomName: parameters.roomName,
|
|
1336
|
+
socket: parameters.socket
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
// Change participant role (host only)
|
|
1340
|
+
parameters.updateParticipant({
|
|
1341
|
+
participantId: 'participant-id',
|
|
1342
|
+
islevel: '2', // '2' = host, '1' = co-host, '0' = participant
|
|
1343
|
+
parameters
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
// Request to unmute participant (sends request)
|
|
1347
|
+
parameters.requestScreenShare({ parameters });
|
|
1348
|
+
```
|
|
1349
|
+
|
|
1350
|
+
### Chat & Messaging Methods
|
|
1351
|
+
|
|
1352
|
+
```typescript
|
|
1353
|
+
// Send a group message
|
|
1354
|
+
parameters.sendMessage({
|
|
1355
|
+
message: 'Hello everyone!',
|
|
1356
|
+
type: 'group',
|
|
1357
|
+
parameters
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
// Send direct message
|
|
1361
|
+
parameters.sendMessage({
|
|
1362
|
+
message: 'Private message',
|
|
1363
|
+
type: 'direct',
|
|
1364
|
+
receivers: ['participant-id'],
|
|
1365
|
+
parameters
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
// Access message history
|
|
1369
|
+
const messages = parameters.messages;
|
|
1370
|
+
|
|
1371
|
+
// Subscribe to new messages (RxJS)
|
|
1372
|
+
parameters.messages.subscribe((messages: any[]) => {
|
|
1373
|
+
console.log('Messages updated:', messages);
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
// Example: Custom chat component
|
|
1377
|
+
@Component({
|
|
1378
|
+
selector: 'app-custom-chat',
|
|
1379
|
+
standalone: true,
|
|
1380
|
+
imports: [CommonModule, FormsModule],
|
|
1381
|
+
template: `
|
|
1382
|
+
<div class="chat-container">
|
|
1383
|
+
<div class="messages">
|
|
1384
|
+
@for (msg of messages; track msg.timestamp) {
|
|
1385
|
+
<div class="message">
|
|
1386
|
+
<strong>{{ msg.sender }}:</strong> {{ msg.message }}
|
|
1387
|
+
</div>
|
|
1388
|
+
}
|
|
1389
|
+
</div>
|
|
1390
|
+
<div class="input-area">
|
|
1391
|
+
<input
|
|
1392
|
+
[(ngModel)]="message"
|
|
1393
|
+
(keyup.enter)="sendMessage()"
|
|
1394
|
+
placeholder="Type a message..."
|
|
1395
|
+
/>
|
|
1396
|
+
<button (click)="sendMessage()">Send</button>
|
|
1397
|
+
</div>
|
|
1398
|
+
</div>
|
|
1399
|
+
`,
|
|
1400
|
+
styles: [`
|
|
1401
|
+
.chat-container {
|
|
1402
|
+
display: flex;
|
|
1403
|
+
flex-direction: column;
|
|
1404
|
+
height: 400px;
|
|
1405
|
+
}
|
|
1406
|
+
.messages {
|
|
1407
|
+
flex: 1;
|
|
1408
|
+
overflow-y: auto;
|
|
1409
|
+
padding: 10px;
|
|
1410
|
+
}
|
|
1411
|
+
.message {
|
|
1412
|
+
margin: 5px 0;
|
|
1413
|
+
padding: 8px;
|
|
1414
|
+
background: #f5f5f5;
|
|
1415
|
+
border-radius: 4px;
|
|
1416
|
+
}
|
|
1417
|
+
.input-area {
|
|
1418
|
+
display: flex;
|
|
1419
|
+
gap: 10px;
|
|
1420
|
+
padding: 10px;
|
|
1421
|
+
border-top: 1px solid #ddd;
|
|
1422
|
+
}
|
|
1423
|
+
input {
|
|
1424
|
+
flex: 1;
|
|
1425
|
+
padding: 8px;
|
|
1426
|
+
border: 1px solid #ddd;
|
|
1427
|
+
border-radius: 4px;
|
|
1428
|
+
}
|
|
1429
|
+
button {
|
|
1430
|
+
padding: 8px 16px;
|
|
1431
|
+
background: #1976d2;
|
|
1432
|
+
color: white;
|
|
1433
|
+
border: none;
|
|
1434
|
+
border-radius: 4px;
|
|
1435
|
+
cursor: pointer;
|
|
1436
|
+
}
|
|
1437
|
+
`]
|
|
1438
|
+
})
|
|
1439
|
+
export class CustomChatComponent implements OnInit {
|
|
1440
|
+
@Input() parameters: any;
|
|
1441
|
+
message = '';
|
|
1442
|
+
messages: any[] = [];
|
|
1443
|
+
|
|
1444
|
+
ngOnInit() {
|
|
1445
|
+
// Subscribe to messages
|
|
1446
|
+
this.parameters?.messages?.subscribe((msgs: any[]) => {
|
|
1447
|
+
this.messages = msgs;
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
sendMessage() {
|
|
1452
|
+
if (this.message.trim()) {
|
|
1453
|
+
this.parameters?.sendMessage({
|
|
1454
|
+
message: this.message,
|
|
1455
|
+
type: 'group',
|
|
1456
|
+
parameters: this.parameters
|
|
1457
|
+
});
|
|
1458
|
+
this.message = '';
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
```
|
|
1463
|
+
|
|
1464
|
+
### Recording Methods
|
|
1465
|
+
|
|
1466
|
+
```typescript
|
|
1467
|
+
// Start recording
|
|
1468
|
+
parameters.startRecording({ parameters });
|
|
1469
|
+
|
|
1470
|
+
// Stop recording
|
|
1471
|
+
parameters.stopRecording({ parameters });
|
|
1472
|
+
|
|
1473
|
+
// Pause recording
|
|
1474
|
+
parameters.pauseRecording({ parameters });
|
|
1475
|
+
|
|
1476
|
+
// Resume recording
|
|
1477
|
+
parameters.resumeRecording({ parameters });
|
|
1478
|
+
|
|
1479
|
+
// Configure recording settings
|
|
1480
|
+
parameters.updateRecording({
|
|
1481
|
+
recordingMediaOptions: 'video', // or 'audio'
|
|
1482
|
+
recordingAudioOptions: 'all', // or 'host'
|
|
1483
|
+
recordingVideoOptions: 'all', // or 'host'
|
|
1484
|
+
recordingVideoType: 'fullDisplay', // or 'bestDisplay', 'all'
|
|
1485
|
+
recordingDisplayType: 'video', // 'media', 'video', 'all'
|
|
1486
|
+
recordingBackgroundColor: '#000000',
|
|
1487
|
+
recordingNameTagsColor: '#ffffff',
|
|
1488
|
+
recordingOrientationVideo: 'landscape', // or 'portrait'
|
|
1489
|
+
recordingNameTags: true,
|
|
1490
|
+
recordingAddHLS: false,
|
|
1491
|
+
parameters
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
// Check recording state
|
|
1495
|
+
const isRecording = parameters.recordStarted;
|
|
1496
|
+
const isPaused = parameters.recordPaused;
|
|
1497
|
+
const recordingTime = parameters.recordElapsedTime;
|
|
1498
|
+
|
|
1499
|
+
// Subscribe to recording state changes
|
|
1500
|
+
parameters.recordStarted.subscribe((isRecording: boolean) => {
|
|
1501
|
+
console.log('Recording:', isRecording ? 'ACTIVE' : 'STOPPED');
|
|
1502
|
+
});
|
|
1503
|
+
```
|
|
1504
|
+
|
|
1505
|
+
### Polls & Surveys Methods
|
|
1506
|
+
|
|
1507
|
+
```typescript
|
|
1508
|
+
// Create a poll
|
|
1509
|
+
parameters.handleCreatePoll({
|
|
1510
|
+
poll: {
|
|
1511
|
+
question: 'What time works best?',
|
|
1512
|
+
type: 'multiple', // or 'single'
|
|
1513
|
+
options: ['10 AM', '2 PM', '5 PM']
|
|
1514
|
+
},
|
|
1515
|
+
parameters
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
// Vote on a poll
|
|
1519
|
+
parameters.handleVotePoll({
|
|
1520
|
+
pollId: 'poll-id',
|
|
1521
|
+
optionIndex: 1,
|
|
1522
|
+
parameters
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
// End a poll
|
|
1526
|
+
parameters.handleEndPoll({
|
|
1527
|
+
pollId: 'poll-id',
|
|
1528
|
+
parameters
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1531
|
+
// Access poll data
|
|
1532
|
+
const polls = parameters.polls;
|
|
1533
|
+
|
|
1534
|
+
// Subscribe to polls changes
|
|
1535
|
+
parameters.polls.subscribe((polls: any[]) => {
|
|
1536
|
+
const activePoll = polls.find(p => p.status === 'active');
|
|
1537
|
+
console.log('Active poll:', activePoll);
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
// Example: Custom poll component
|
|
1541
|
+
@Component({
|
|
1542
|
+
selector: 'app-custom-poll',
|
|
1543
|
+
standalone: true,
|
|
1544
|
+
imports: [CommonModule],
|
|
1545
|
+
template: `
|
|
1546
|
+
@if (activePoll) {
|
|
1547
|
+
<div class="poll-container">
|
|
1548
|
+
<h3>{{ activePoll.question }}</h3>
|
|
1549
|
+
<div class="poll-options">
|
|
1550
|
+
@for (option of activePoll.options; track $index) {
|
|
1551
|
+
<button
|
|
1552
|
+
class="poll-option"
|
|
1553
|
+
(click)="vote($index)">
|
|
1554
|
+
{{ option }}
|
|
1555
|
+
<span class="votes">({{ activePoll.votes?.[$index] || 0 }} votes)</span>
|
|
1556
|
+
</button>
|
|
1557
|
+
}
|
|
1558
|
+
</div>
|
|
1559
|
+
@if (isHost) {
|
|
1560
|
+
<button class="end-poll" (click)="endPoll()">
|
|
1561
|
+
End Poll
|
|
1562
|
+
</button>
|
|
1563
|
+
}
|
|
1564
|
+
</div>
|
|
1565
|
+
}
|
|
1566
|
+
`,
|
|
1567
|
+
styles: [`
|
|
1568
|
+
.poll-container {
|
|
1569
|
+
padding: 20px;
|
|
1570
|
+
background: white;
|
|
1571
|
+
border-radius: 8px;
|
|
1572
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
1573
|
+
}
|
|
1574
|
+
.poll-options {
|
|
1575
|
+
display: flex;
|
|
1576
|
+
flex-direction: column;
|
|
1577
|
+
gap: 10px;
|
|
1578
|
+
margin: 20px 0;
|
|
1579
|
+
}
|
|
1580
|
+
.poll-option {
|
|
1581
|
+
padding: 12px;
|
|
1582
|
+
border: 2px solid #1976d2;
|
|
1583
|
+
background: white;
|
|
1584
|
+
border-radius: 4px;
|
|
1585
|
+
cursor: pointer;
|
|
1586
|
+
transition: all 0.3s;
|
|
1587
|
+
display: flex;
|
|
1588
|
+
justify-content: space-between;
|
|
1589
|
+
}
|
|
1590
|
+
.poll-option:hover {
|
|
1591
|
+
background: #1976d2;
|
|
1592
|
+
color: white;
|
|
1593
|
+
}
|
|
1594
|
+
.votes {
|
|
1595
|
+
font-size: 0.9em;
|
|
1596
|
+
opacity: 0.7;
|
|
1597
|
+
}
|
|
1598
|
+
.end-poll {
|
|
1599
|
+
padding: 10px 20px;
|
|
1600
|
+
background: #d32f2f;
|
|
1601
|
+
color: white;
|
|
1602
|
+
border: none;
|
|
1603
|
+
border-radius: 4px;
|
|
1604
|
+
cursor: pointer;
|
|
1605
|
+
}
|
|
1606
|
+
`]
|
|
1607
|
+
})
|
|
1608
|
+
export class CustomPollComponent implements OnInit {
|
|
1609
|
+
@Input() parameters: any;
|
|
1610
|
+
activePoll: any = null;
|
|
1611
|
+
|
|
1612
|
+
ngOnInit() {
|
|
1613
|
+
this.parameters?.polls?.subscribe((polls: any[]) => {
|
|
1614
|
+
this.activePoll = polls.find((p: any) => p.status === 'active');
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
get isHost() {
|
|
1619
|
+
return this.parameters?.islevel === '2';
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
vote(optionIndex: number) {
|
|
1623
|
+
if (this.activePoll) {
|
|
1624
|
+
this.parameters?.handleVotePoll({
|
|
1625
|
+
pollId: this.activePoll.id,
|
|
1626
|
+
optionIndex,
|
|
1627
|
+
parameters: this.parameters
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
endPoll() {
|
|
1633
|
+
if (this.activePoll) {
|
|
1634
|
+
this.parameters?.handleEndPoll({
|
|
1635
|
+
pollId: this.activePoll.id,
|
|
1636
|
+
parameters: this.parameters
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
```
|
|
1642
|
+
|
|
1643
|
+
### Breakout Rooms Methods
|
|
1644
|
+
|
|
1645
|
+
```typescript
|
|
1646
|
+
// Create breakout rooms
|
|
1647
|
+
parameters.createBreakoutRooms({
|
|
1648
|
+
numberOfRooms: 3,
|
|
1649
|
+
participants: parameters.participants,
|
|
1650
|
+
parameters
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
// Assign participant to room
|
|
1654
|
+
parameters.assignParticipantToRoom({
|
|
1655
|
+
participantId: 'participant-id',
|
|
1656
|
+
roomIndex: 0,
|
|
1657
|
+
parameters
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
// Start breakout rooms
|
|
1661
|
+
parameters.startBreakoutRooms({ parameters });
|
|
1662
|
+
|
|
1663
|
+
// Stop breakout rooms
|
|
1664
|
+
parameters.stopBreakoutRooms({ parameters });
|
|
1665
|
+
|
|
1666
|
+
// Access breakout room data
|
|
1667
|
+
const breakoutRooms = parameters.breakoutRooms;
|
|
1668
|
+
const currentRoom = parameters.currentBreakoutRoom;
|
|
1669
|
+
|
|
1670
|
+
// Subscribe to breakout room changes
|
|
1671
|
+
parameters.breakoutRooms.subscribe((rooms: any[]) => {
|
|
1672
|
+
console.log('Breakout rooms:', rooms);
|
|
1673
|
+
});
|
|
1674
|
+
```
|
|
1675
|
+
|
|
1676
|
+
### Whiteboard Methods
|
|
1677
|
+
|
|
1678
|
+
```typescript
|
|
1679
|
+
// Show/hide whiteboard
|
|
1680
|
+
parameters.updateWhiteboardStarted.next(true);
|
|
1681
|
+
parameters.updateWhiteboardEnded.next(false);
|
|
1682
|
+
|
|
1683
|
+
// Configure whiteboard
|
|
1684
|
+
parameters.launchConfigureWhiteboard?.launchConfigureWhiteboard({ parameters });
|
|
1685
|
+
|
|
1686
|
+
// Access whiteboard state
|
|
1687
|
+
const isWhiteboardActive = parameters.whiteboardStarted;
|
|
1688
|
+
|
|
1689
|
+
// Subscribe to whiteboard state
|
|
1690
|
+
parameters.whiteboardStarted.subscribe((isActive: boolean) => {
|
|
1691
|
+
console.log('Whiteboard:', isActive ? 'ACTIVE' : 'INACTIVE');
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
// Access whiteboard users
|
|
1695
|
+
const whiteboardUsers = parameters.whiteboardUsers;
|
|
1696
|
+
```
|
|
1697
|
+
|
|
1698
|
+
### Utility Methods
|
|
1699
|
+
|
|
1700
|
+
```typescript
|
|
1701
|
+
// Check permissions
|
|
1702
|
+
const hasPermission = await parameters.checkPermission({
|
|
1703
|
+
permissionType: 'video', // or 'audio'
|
|
1704
|
+
parameters
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
// Format large numbers
|
|
1708
|
+
const formatted = parameters.formatNumber(1250000); // Returns "1.25M"
|
|
1709
|
+
|
|
1710
|
+
// Sleep/delay
|
|
1711
|
+
await parameters.sleep({ ms: 1000 });
|
|
1712
|
+
|
|
1713
|
+
// Update display settings
|
|
1714
|
+
parameters.updateMainWindow.next(true); // Show/hide main window
|
|
1715
|
+
|
|
1716
|
+
// Trigger layout recalculation
|
|
1717
|
+
parameters.onScreenChanges({ changed: true, parameters });
|
|
1718
|
+
|
|
1719
|
+
// Get room information
|
|
1720
|
+
const roomInfo = {
|
|
1721
|
+
name: parameters.roomName,
|
|
1722
|
+
host: parameters.host,
|
|
1723
|
+
capacity: parameters.capacity,
|
|
1724
|
+
eventType: parameters.eventType,
|
|
1725
|
+
participants: parameters.participants,
|
|
1726
|
+
isRecording: parameters.recordStarted
|
|
1727
|
+
};
|
|
1728
|
+
|
|
1729
|
+
// Subscribe to room state changes
|
|
1730
|
+
parameters.roomName.subscribe((name: string) => {
|
|
1731
|
+
console.log('Room name:', name);
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
parameters.participantsCounter.subscribe((count: number) => {
|
|
1735
|
+
console.log('Participant count:', count);
|
|
1736
|
+
});
|
|
1737
|
+
```
|
|
1738
|
+
|
|
1739
|
+
---
|
|
211
1740
|
|
|
212
1741
|
## Programmatically Fetching Tokens
|
|
213
1742
|
|