mediasfu-angular 2.2.0 → 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 +1514 -432
- package/dist/README.md +1514 -432
- package/dist/fesm2022/mediasfu-angular.mjs +235 -0
- package/dist/fesm2022/mediasfu-angular.mjs.map +1 -1
- package/dist/lib/components/mediasfu-components/mediasfu-broadcast.component.d.ts +52 -0
- package/dist/lib/components/mediasfu-components/mediasfu-chat.component.d.ts +52 -0
- package/dist/lib/components/mediasfu-components/mediasfu-conference.component.d.ts +64 -0
- package/dist/lib/components/mediasfu-components/mediasfu-generic.component.d.ts +64 -0
- package/dist/lib/components/mediasfu-components/mediasfu-webinar.component.d.ts +64 -0
- package/package.json +1 -1
package/dist/README.md
CHANGED
|
@@ -4,19 +4,34 @@
|
|
|
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
|
+
</a>
|
|
21
|
+
</p>
|
|
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" />
|
|
20
35
|
</a>
|
|
21
36
|
</p>
|
|
22
37
|
|
|
@@ -59,16 +74,22 @@ MediaSFU offers a cutting-edge streaming experience that empowers users to custo
|
|
|
59
74
|
|
|
60
75
|
**[Get started now on GitHub!](https://github.com/MediaSFU/MediaSFUOpen)**
|
|
61
76
|
|
|
77
|
+
### ✅ Angular SDK Setup Guide
|
|
78
|
+
Coming soon! Watch this space for our comprehensive video tutorial on setting up the Angular SDK.
|
|
79
|
+
|
|
62
80
|
---
|
|
63
81
|
|
|
64
82
|
## Table of Contents
|
|
65
83
|
|
|
66
84
|
- [Features](#features)
|
|
67
85
|
- [Getting Started](#getting-started)
|
|
68
|
-
- [
|
|
69
|
-
- [
|
|
70
|
-
- [
|
|
71
|
-
- [
|
|
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)
|
|
72
93
|
- [API Reference](#api-reference)
|
|
73
94
|
- [Troubleshooting](#troubleshooting)
|
|
74
95
|
- [Contributing](#contributing)
|
|
@@ -88,6 +109,74 @@ MediaSFU's Angular SDK comes with a host of powerful features out of the box:
|
|
|
88
109
|
9. **Cloud Recording (track-based)**: Customize recordings with track-based options, including watermarks, name tags, background colors, and more.
|
|
89
110
|
10. **Managed Events**: Manage events with features to handle abandoned and inactive participants, as well as enforce time and capacity limits.
|
|
90
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
|
+
|
|
91
180
|
# Getting Started <a name="getting-started"></a>
|
|
92
181
|
|
|
93
182
|
This section will guide users through the initial setup and installation of the npm module.
|
|
@@ -180,153 +269,1475 @@ If you plan to self-host MediaSFU or use it without MediaSFU Cloud services, you
|
|
|
180
269
|
This setup allows full flexibility and customization while bypassing the need for cloud-dependent credentials.
|
|
181
270
|
|
|
182
271
|
|
|
183
|
-
#
|
|
272
|
+
# 📘 Angular SDK Guide <a name="angular-sdk-guide"></a>
|
|
273
|
+
|
|
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.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Quick Start (5 Minutes) <a name="quick-start-5-minutes"></a>
|
|
279
|
+
|
|
280
|
+
Get your first MediaSFU app running in just a few minutes.
|
|
281
|
+
|
|
282
|
+
### Step 1: Install the Package
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
npm install mediasfu-angular
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Step 2: Import and Use
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
// app.component.ts
|
|
292
|
+
import { Component } from '@angular/core';
|
|
293
|
+
import { MediasfuGeneric } from 'mediasfu-angular';
|
|
294
|
+
|
|
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
|
+
```
|
|
303
|
+
|
|
304
|
+
**Alternative with Credentials:**
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
import { Component } from '@angular/core';
|
|
308
|
+
import { MediasfuGeneric, PreJoinPage } from 'mediasfu-angular';
|
|
309
|
+
|
|
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
|
|
380
|
+
|
|
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.
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
import { Component } from '@angular/core';
|
|
420
|
+
import { MediasfuGeneric } from 'mediasfu-angular';
|
|
421
|
+
|
|
422
|
+
@Component({
|
|
423
|
+
selector: 'app-root',
|
|
424
|
+
standalone: true,
|
|
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],
|
|
449
|
+
template: `
|
|
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
|
+
}
|
|
472
|
+
`,
|
|
473
|
+
})
|
|
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
|
+
})
|
|
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
|
|
184
1120
|
|
|
185
|
-
|
|
1121
|
+
```typescript
|
|
1122
|
+
// Start screen sharing
|
|
1123
|
+
parameters.clickScreenShare({ parameters });
|
|
186
1124
|
|
|
187
|
-
|
|
188
|
-
|
|
1125
|
+
// Stop screen sharing
|
|
1126
|
+
parameters.stopShareScreen({ parameters });
|
|
189
1127
|
|
|
190
|
-
|
|
1128
|
+
// Check if screen sharing is available
|
|
1129
|
+
const canShare = await parameters.checkScreenShare({ parameters });
|
|
191
1130
|
|
|
192
|
-
|
|
1131
|
+
// Get screen share state
|
|
1132
|
+
const isSharing = parameters.screenAlreadyOn;
|
|
1133
|
+
const shareAudio = parameters.shareScreenStarted; // Sharing with audio
|
|
193
1134
|
|
|
194
|
-
|
|
1135
|
+
// Subscribe to screen share state
|
|
1136
|
+
parameters.screenAlreadyOn.subscribe((isSharing: boolean) => {
|
|
1137
|
+
console.log('Screen sharing:', isSharing ? 'ACTIVE' : 'INACTIVE');
|
|
1138
|
+
});
|
|
1139
|
+
```
|
|
195
1140
|
|
|
196
|
-
|
|
1141
|
+
### Media Device and Stream Utility Methods
|
|
197
1142
|
|
|
198
|
-
|
|
1143
|
+
#### Get Available Media Devices
|
|
199
1144
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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[] = [];
|
|
205
1187
|
|
|
206
|
-
|
|
1188
|
+
async ngOnInit() {
|
|
1189
|
+
await this.loadDevices();
|
|
1190
|
+
}
|
|
207
1191
|
|
|
208
|
-
|
|
1192
|
+
async loadDevices() {
|
|
1193
|
+
this.cameras = await this.parameters.getMediaDevicesList('videoinput');
|
|
1194
|
+
this.microphones = await this.parameters.getMediaDevicesList('audioinput');
|
|
1195
|
+
}
|
|
209
1196
|
|
|
210
|
-
|
|
1197
|
+
onCameraChange(event: any) {
|
|
1198
|
+
this.parameters.switchUserVideo({
|
|
1199
|
+
videoPreference: event.target.value,
|
|
1200
|
+
parameters: this.parameters
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
211
1203
|
|
|
212
|
-
|
|
213
|
-
|
|
1204
|
+
onMicrophoneChange(event: any) {
|
|
1205
|
+
this.parameters.switchUserAudio({
|
|
1206
|
+
audioPreference: event.target.value,
|
|
1207
|
+
parameters: this.parameters
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
```
|
|
214
1212
|
|
|
215
|
-
|
|
1213
|
+
#### Get Participant Media Streams
|
|
216
1214
|
|
|
217
1215
|
```typescript
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
222
1229
|
@Component({
|
|
223
|
-
selector: 'app-
|
|
224
|
-
standalone: true,
|
|
225
|
-
imports: [MediasfuGeneric],
|
|
1230
|
+
selector: 'app-stream-monitor',
|
|
226
1231
|
template: `
|
|
227
|
-
<
|
|
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>
|
|
228
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
|
+
`]
|
|
229
1263
|
})
|
|
230
|
-
export class
|
|
231
|
-
|
|
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
|
+
}
|
|
232
1273
|
|
|
233
|
-
|
|
1274
|
+
async viewStream(participant: any) {
|
|
1275
|
+
const stream = await this.parameters.getParticipantMedia({
|
|
1276
|
+
id: participant.videoID,
|
|
1277
|
+
name: participant.name,
|
|
1278
|
+
kind: 'video'
|
|
1279
|
+
});
|
|
234
1280
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
+
}
|
|
242
1288
|
|
|
243
|
-
|
|
1289
|
+
async monitorAudio(participant: any) {
|
|
1290
|
+
const stream = await this.parameters.getParticipantMedia({
|
|
1291
|
+
id: participant.audioID,
|
|
1292
|
+
name: participant.name,
|
|
1293
|
+
kind: 'audio'
|
|
1294
|
+
});
|
|
244
1295
|
|
|
245
|
-
|
|
246
|
-
|
|
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
|
+
```
|
|
247
1311
|
|
|
248
|
-
###
|
|
249
|
-
Choose from pre-built, production-ready templates:
|
|
1312
|
+
### Participant Management Methods
|
|
250
1313
|
|
|
251
1314
|
```typescript
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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 });
|
|
259
1348
|
```
|
|
260
1349
|
|
|
261
|
-
###
|
|
262
|
-
|
|
263
|
-
**Replace Any Component:** Don't like our video card? Swap it with yours:
|
|
1350
|
+
### Chat & Messaging Methods
|
|
264
1351
|
|
|
265
1352
|
```typescript
|
|
266
|
-
//
|
|
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
|
|
267
1377
|
@Component({
|
|
268
|
-
selector: '
|
|
269
|
-
|
|
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
|
+
`]
|
|
270
1438
|
})
|
|
271
|
-
export class
|
|
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
|
+
```
|
|
272
1463
|
|
|
273
|
-
|
|
274
|
-
const customVideoCard = this.customComponentService.createComponentWithInjector(
|
|
275
|
-
MyVideoCard, { participant: participantData }
|
|
276
|
-
);
|
|
1464
|
+
### Recording Methods
|
|
277
1465
|
|
|
278
|
-
|
|
279
|
-
|
|
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
|
+
});
|
|
280
1503
|
```
|
|
281
1504
|
|
|
282
|
-
|
|
1505
|
+
### Polls & Surveys Methods
|
|
283
1506
|
|
|
284
1507
|
```typescript
|
|
285
|
-
//
|
|
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
|
|
286
1541
|
@Component({
|
|
1542
|
+
selector: 'app-custom-poll',
|
|
1543
|
+
standalone: true,
|
|
1544
|
+
imports: [CommonModule],
|
|
287
1545
|
template: `
|
|
288
|
-
|
|
289
|
-
<
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
+
`]
|
|
293
1607
|
})
|
|
294
|
-
export class
|
|
1608
|
+
export class CustomPollComponent implements OnInit {
|
|
1609
|
+
@Input() parameters: any;
|
|
1610
|
+
activePoll: any = null;
|
|
295
1611
|
|
|
296
|
-
|
|
297
|
-
this.
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
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
|
+
}
|
|
301
1641
|
```
|
|
302
1642
|
|
|
303
|
-
###
|
|
1643
|
+
### Breakout Rooms Methods
|
|
304
1644
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
+
```
|
|
312
1675
|
|
|
313
|
-
###
|
|
1676
|
+
### Whiteboard Methods
|
|
314
1677
|
|
|
315
1678
|
```typescript
|
|
316
|
-
//
|
|
317
|
-
|
|
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;
|
|
318
1688
|
|
|
319
|
-
//
|
|
320
|
-
|
|
1689
|
+
// Subscribe to whiteboard state
|
|
1690
|
+
parameters.whiteboardStarted.subscribe((isActive: boolean) => {
|
|
1691
|
+
console.log('Whiteboard:', isActive ? 'ACTIVE' : 'INACTIVE');
|
|
1692
|
+
});
|
|
321
1693
|
|
|
322
|
-
//
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
|
327
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
|
+
});
|
|
328
1737
|
```
|
|
329
1738
|
|
|
1739
|
+
---
|
|
1740
|
+
|
|
330
1741
|
## Programmatically Fetching Tokens
|
|
331
1742
|
|
|
332
1743
|
If you prefer to fetch the required tokens programmatically without visiting MediaSFU's website, you can use the `PreJoinPage` component and pass your credentials as inputs.
|
|
@@ -2590,335 +4001,6 @@ Once your custom components are built, modify the imports of `prepopulateUserMed
|
|
|
2590
4001
|
|
|
2591
4002
|
This allows for full flexibility in how media is displayed in both the main and mini grids, giving you the ability to tailor the user experience to your specific needs.
|
|
2592
4003
|
|
|
2593
|
-
## Custom Component Injection Service
|
|
2594
|
-
|
|
2595
|
-
MediaSFU provides a powerful `CustomComponentInjectionService` that enables advanced custom component integration across all MediaSFU components. This service offers a comprehensive set of utilities for managing custom component rendering, validation, and lifecycle management.
|
|
2596
|
-
|
|
2597
|
-
### Features
|
|
2598
|
-
|
|
2599
|
-
- **Type-safe component validation**: Check if components are Angular components, HTML elements, or function components
|
|
2600
|
-
- **Dynamic component injection**: Create components with custom parameters and injectors
|
|
2601
|
-
- **Safe rendering utilities**: Handle different component types with proper error handling and fallbacks
|
|
2602
|
-
- **Component lifecycle management**: Proper cleanup and destruction of custom components
|
|
2603
|
-
- **Template integration helpers**: Utilities for the custom component pattern used throughout MediaSFU
|
|
2604
|
-
|
|
2605
|
-
### Basic Usage
|
|
2606
|
-
|
|
2607
|
-
First, import the service and types:
|
|
2608
|
-
|
|
2609
|
-
```typescript
|
|
2610
|
-
import {
|
|
2611
|
-
CustomComponentInjectionService,
|
|
2612
|
-
CustomComponent,
|
|
2613
|
-
CustomComponentType,
|
|
2614
|
-
CustomComponentParameters,
|
|
2615
|
-
CustomComponentContext
|
|
2616
|
-
} from 'mediasfu-angular';
|
|
2617
|
-
```
|
|
2618
|
-
|
|
2619
|
-
#### Injecting the Service
|
|
2620
|
-
|
|
2621
|
-
```typescript
|
|
2622
|
-
import { Component, inject } from '@angular/core';
|
|
2623
|
-
|
|
2624
|
-
@Component({
|
|
2625
|
-
selector: 'app-my-component',
|
|
2626
|
-
template: `...`
|
|
2627
|
-
})
|
|
2628
|
-
export class MyComponent {
|
|
2629
|
-
private customComponentService = inject(CustomComponentInjectionService);
|
|
2630
|
-
}
|
|
2631
|
-
```
|
|
2632
|
-
|
|
2633
|
-
#### Creating Custom Components with Parameters
|
|
2634
|
-
|
|
2635
|
-
```typescript
|
|
2636
|
-
// Create a custom component with injected parameters
|
|
2637
|
-
const customVideoCard = this.customComponentService.createComponentWithInjector(
|
|
2638
|
-
MyCustomVideoCard,
|
|
2639
|
-
{
|
|
2640
|
-
participant: participantData,
|
|
2641
|
-
options: videoOptions,
|
|
2642
|
-
callbacks: eventHandlers
|
|
2643
|
-
}
|
|
2644
|
-
);
|
|
2645
|
-
|
|
2646
|
-
// Use in MediaSFU component
|
|
2647
|
-
const params = {
|
|
2648
|
-
customVideoCard: customVideoCard,
|
|
2649
|
-
// ... other parameters
|
|
2650
|
-
};
|
|
2651
|
-
```
|
|
2652
|
-
|
|
2653
|
-
#### Component Type Validation
|
|
2654
|
-
|
|
2655
|
-
```typescript
|
|
2656
|
-
// Check component types
|
|
2657
|
-
if (this.customComponentService.isCustomComponent(component)) {
|
|
2658
|
-
// Handle Angular component with injector
|
|
2659
|
-
console.log('Angular component detected');
|
|
2660
|
-
} else if (this.customComponentService.isHTMLElement(component)) {
|
|
2661
|
-
// Handle HTML element
|
|
2662
|
-
console.log('HTML element detected');
|
|
2663
|
-
} else if (this.customComponentService.isFunctionComponent(component)) {
|
|
2664
|
-
// Handle function component
|
|
2665
|
-
console.log('Function component detected');
|
|
2666
|
-
}
|
|
2667
|
-
```
|
|
2668
|
-
|
|
2669
|
-
#### Safe HTML Element Access
|
|
2670
|
-
|
|
2671
|
-
```typescript
|
|
2672
|
-
// Safely get outerHTML from components
|
|
2673
|
-
const htmlContent = this.customComponentService.getHtmlElementOuterHTML(component);
|
|
2674
|
-
if (htmlContent) {
|
|
2675
|
-
// Use the HTML content
|
|
2676
|
-
element.innerHTML = htmlContent;
|
|
2677
|
-
}
|
|
2678
|
-
```
|
|
2679
|
-
|
|
2680
|
-
#### Component Resolution Pattern
|
|
2681
|
-
|
|
2682
|
-
```typescript
|
|
2683
|
-
// Resolve between custom and default components
|
|
2684
|
-
const resolvedComponent = this.customComponentService.resolveComponent(
|
|
2685
|
-
customComponent, // May be undefined
|
|
2686
|
-
defaultComponent // Fallback component
|
|
2687
|
-
);
|
|
2688
|
-
|
|
2689
|
-
// Check if custom main component should be used
|
|
2690
|
-
const useCustomLayout = this.customComponentService.shouldUseCustomMainComponent(
|
|
2691
|
-
customMainComponent
|
|
2692
|
-
);
|
|
2693
|
-
```
|
|
2694
|
-
|
|
2695
|
-
### Advanced Usage Examples
|
|
2696
|
-
|
|
2697
|
-
#### 1. Dynamic Component Rendering
|
|
2698
|
-
|
|
2699
|
-
```typescript
|
|
2700
|
-
@Component({
|
|
2701
|
-
selector: 'app-dynamic-renderer',
|
|
2702
|
-
template: `
|
|
2703
|
-
<ng-container #dynamicContainer></ng-container>
|
|
2704
|
-
`
|
|
2705
|
-
})
|
|
2706
|
-
export class DynamicRenderer {
|
|
2707
|
-
@ViewChild('dynamicContainer', { read: ViewContainerRef })
|
|
2708
|
-
container!: ViewContainerRef;
|
|
2709
|
-
|
|
2710
|
-
private customComponentService = inject(CustomComponentInjectionService);
|
|
2711
|
-
|
|
2712
|
-
renderCustomComponent(customComponent: CustomComponentType<any>) {
|
|
2713
|
-
const context: CustomComponentContext = {
|
|
2714
|
-
parameters: {
|
|
2715
|
-
data: this.componentData,
|
|
2716
|
-
events: this.eventHandlers
|
|
2717
|
-
},
|
|
2718
|
-
injector: this.injector,
|
|
2719
|
-
config: {
|
|
2720
|
-
enabled: true,
|
|
2721
|
-
fallbackToDefault: true
|
|
2722
|
-
}
|
|
2723
|
-
};
|
|
2724
|
-
|
|
2725
|
-
const componentRef = this.customComponentService.renderCustomComponent(
|
|
2726
|
-
customComponent,
|
|
2727
|
-
this.container,
|
|
2728
|
-
context
|
|
2729
|
-
);
|
|
2730
|
-
|
|
2731
|
-
// Store reference for cleanup
|
|
2732
|
-
this.activeComponents.push(componentRef);
|
|
2733
|
-
}
|
|
2734
|
-
|
|
2735
|
-
ngOnDestroy() {
|
|
2736
|
-
// Clean up all custom components
|
|
2737
|
-
this.activeComponents.forEach(component => {
|
|
2738
|
-
this.customComponentService.destroyCustomComponent(component);
|
|
2739
|
-
});
|
|
2740
|
-
}
|
|
2741
|
-
}
|
|
2742
|
-
```
|
|
2743
|
-
|
|
2744
|
-
#### 2. Custom Video Card Implementation
|
|
2745
|
-
|
|
2746
|
-
```typescript
|
|
2747
|
-
// Define your custom video card component
|
|
2748
|
-
@Component({
|
|
2749
|
-
selector: 'app-custom-video-card',
|
|
2750
|
-
template: `
|
|
2751
|
-
<div class="custom-video-wrapper">
|
|
2752
|
-
<video [srcObject]="videoStream" autoplay playsinline></video>
|
|
2753
|
-
<div class="custom-controls">
|
|
2754
|
-
<button (click)="toggleAudio()">🎤</button>
|
|
2755
|
-
<button (click)="toggleVideo()">📹</button>
|
|
2756
|
-
</div>
|
|
2757
|
-
<div class="custom-info">{{ participantName }}</div>
|
|
2758
|
-
</div>
|
|
2759
|
-
`,
|
|
2760
|
-
imports: [CommonModule]
|
|
2761
|
-
})
|
|
2762
|
-
export class CustomVideoCard {
|
|
2763
|
-
@Input() participant!: Participant;
|
|
2764
|
-
@Input() videoStream!: MediaStream;
|
|
2765
|
-
@Input() options!: any;
|
|
2766
|
-
|
|
2767
|
-
get participantName() {
|
|
2768
|
-
return this.participant?.name || 'Unknown';
|
|
2769
|
-
}
|
|
2770
|
-
|
|
2771
|
-
toggleAudio() {
|
|
2772
|
-
// Custom audio toggle logic
|
|
2773
|
-
}
|
|
2774
|
-
|
|
2775
|
-
toggleVideo() {
|
|
2776
|
-
// Custom video toggle logic
|
|
2777
|
-
}
|
|
2778
|
-
}
|
|
2779
|
-
|
|
2780
|
-
// In your main component
|
|
2781
|
-
export class MyMediaComponent {
|
|
2782
|
-
private customComponentService = inject(CustomComponentInjectionService);
|
|
2783
|
-
|
|
2784
|
-
ngOnInit() {
|
|
2785
|
-
// Create custom video card with parameters
|
|
2786
|
-
const customVideoCard = this.customComponentService.createComponentWithInjector(
|
|
2787
|
-
CustomVideoCard,
|
|
2788
|
-
{
|
|
2789
|
-
participant: this.currentParticipant,
|
|
2790
|
-
options: this.videoOptions
|
|
2791
|
-
}
|
|
2792
|
-
);
|
|
2793
|
-
|
|
2794
|
-
// Use in MediaSFU parameters
|
|
2795
|
-
this.mediasfuParameters = {
|
|
2796
|
-
customVideoCard: customVideoCard,
|
|
2797
|
-
// ... other parameters
|
|
2798
|
-
};
|
|
2799
|
-
}
|
|
2800
|
-
}
|
|
2801
|
-
```
|
|
2802
|
-
|
|
2803
|
-
#### 3. Custom Main Component with Template Restructuring
|
|
2804
|
-
|
|
2805
|
-
```typescript
|
|
2806
|
-
@Component({
|
|
2807
|
-
selector: 'app-custom-main-layout',
|
|
2808
|
-
template: `
|
|
2809
|
-
<div class="custom-main-layout">
|
|
2810
|
-
<header class="custom-header">
|
|
2811
|
-
<h1>My Custom Stream</h1>
|
|
2812
|
-
<div class="custom-controls">
|
|
2813
|
-
<!-- Custom control buttons -->
|
|
2814
|
-
</div>
|
|
2815
|
-
</header>
|
|
2816
|
-
|
|
2817
|
-
<main class="custom-content">
|
|
2818
|
-
<div class="video-area">
|
|
2819
|
-
<!-- Your custom video layout -->
|
|
2820
|
-
</div>
|
|
2821
|
-
<div class="participant-list">
|
|
2822
|
-
<!-- Custom participant list -->
|
|
2823
|
-
</div>
|
|
2824
|
-
</main>
|
|
2825
|
-
|
|
2826
|
-
<footer class="custom-footer">
|
|
2827
|
-
<!-- Custom footer content -->
|
|
2828
|
-
</footer>
|
|
2829
|
-
</div>
|
|
2830
|
-
`,
|
|
2831
|
-
imports: [CommonModule]
|
|
2832
|
-
})
|
|
2833
|
-
export class CustomMainLayout {
|
|
2834
|
-
@Input() parameters!: any;
|
|
2835
|
-
@Input() credentials!: any;
|
|
2836
|
-
}
|
|
2837
|
-
|
|
2838
|
-
// Usage
|
|
2839
|
-
const customMainComponent = this.customComponentService.createComponentWithInjector(
|
|
2840
|
-
CustomMainLayout,
|
|
2841
|
-
{
|
|
2842
|
-
theme: 'dark',
|
|
2843
|
-
layout: 'grid'
|
|
2844
|
-
}
|
|
2845
|
-
);
|
|
2846
|
-
|
|
2847
|
-
// MediaSFU will use this instead of default layout
|
|
2848
|
-
this.mediasfuParameters = {
|
|
2849
|
-
customMainComponent: customMainComponent,
|
|
2850
|
-
// ... other parameters
|
|
2851
|
-
};
|
|
2852
|
-
```
|
|
2853
|
-
|
|
2854
|
-
### Component Validation and Error Handling
|
|
2855
|
-
|
|
2856
|
-
```typescript
|
|
2857
|
-
// Validate before using
|
|
2858
|
-
if (this.customComponentService.validateCustomComponent(customComponent)) {
|
|
2859
|
-
// Component is valid, proceed with rendering
|
|
2860
|
-
this.renderComponent(customComponent);
|
|
2861
|
-
} else {
|
|
2862
|
-
// Handle invalid component, use fallback
|
|
2863
|
-
console.warn('Invalid custom component, using default');
|
|
2864
|
-
this.renderComponent(this.defaultComponent);
|
|
2865
|
-
}
|
|
2866
|
-
|
|
2867
|
-
// Merge options safely
|
|
2868
|
-
const mergedOptions = this.customComponentService.mergeComponentOptions(
|
|
2869
|
-
customOptions,
|
|
2870
|
-
defaultOptions
|
|
2871
|
-
);
|
|
2872
|
-
```
|
|
2873
|
-
|
|
2874
|
-
### TypeScript Types
|
|
2875
|
-
|
|
2876
|
-
The service works with these main types:
|
|
2877
|
-
|
|
2878
|
-
```typescript
|
|
2879
|
-
// Union type for all supported component types
|
|
2880
|
-
type CustomComponentType<T = any> = CustomComponent<T> | HTMLElement | CustomComponentFunction;
|
|
2881
|
-
|
|
2882
|
-
// Angular component with injector
|
|
2883
|
-
interface CustomComponent<T = any> {
|
|
2884
|
-
component: Type<T>;
|
|
2885
|
-
injector?: Injector;
|
|
2886
|
-
}
|
|
2887
|
-
|
|
2888
|
-
// Function that returns HTMLElement
|
|
2889
|
-
type CustomComponentFunction = () => HTMLElement;
|
|
2890
|
-
|
|
2891
|
-
// Parameters for component injection
|
|
2892
|
-
interface CustomComponentParameters {
|
|
2893
|
-
[key: string]: any;
|
|
2894
|
-
}
|
|
2895
|
-
|
|
2896
|
-
// Context for component rendering
|
|
2897
|
-
interface CustomComponentContext {
|
|
2898
|
-
parameters: CustomComponentParameters;
|
|
2899
|
-
injector?: Injector;
|
|
2900
|
-
config?: ComponentInjectionConfig;
|
|
2901
|
-
}
|
|
2902
|
-
|
|
2903
|
-
// Configuration options
|
|
2904
|
-
interface ComponentInjectionConfig {
|
|
2905
|
-
enabled: boolean;
|
|
2906
|
-
overrideDefaults: boolean;
|
|
2907
|
-
fallbackToDefault: boolean;
|
|
2908
|
-
}
|
|
2909
|
-
```
|
|
2910
|
-
|
|
2911
|
-
### Best Practices
|
|
2912
|
-
|
|
2913
|
-
1. **Always validate components** before rendering
|
|
2914
|
-
2. **Use proper cleanup** to prevent memory leaks
|
|
2915
|
-
3. **Leverage type guards** for safe component handling
|
|
2916
|
-
4. **Provide fallbacks** for failed component rendering
|
|
2917
|
-
5. **Use meaningful parameter names** when creating injectors
|
|
2918
|
-
6. **Test custom components** in isolation before integration
|
|
2919
|
-
|
|
2920
|
-
The `CustomComponentInjectionService` provides a robust foundation for building highly customizable MediaSFU applications while maintaining type safety and proper Angular practices.
|
|
2921
|
-
|
|
2922
4004
|
|
|
2923
4005
|
# API Reference <a name="api-reference"></a>
|
|
2924
4006
|
|