sip-connector 16.1.0 → 17.0.0
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 +247 -44
- package/dist/{@SipConnector-BzMLHZRD.js → @SipConnector-DADbRZIb.js} +634 -514
- package/dist/@SipConnector-aB66gnC6.cjs +1 -0
- package/dist/ApiManager/@ApiManager.d.ts +1 -0
- package/dist/ApiManager/constants.d.ts +11 -2
- package/dist/ApiManager/eventNames.d.ts +3 -2
- package/dist/ApiManager/index.d.ts +1 -0
- package/dist/ApiManager/types.d.ts +6 -0
- package/dist/CallManager/@CallManager.d.ts +3 -0
- package/dist/CallManager/AbstractCallStrategy.d.ts +19 -1
- package/dist/CallManager/MCUCallStrategy.d.ts +17 -1
- package/dist/CallManager/TransceiverManager.d.ts +51 -0
- package/dist/CallManager/index.d.ts +2 -1
- package/dist/CallManager/types.d.ts +22 -2
- package/dist/PresentationManager/index.d.ts +1 -1
- package/dist/PresentationManager/types.d.ts +1 -1
- package/dist/SipConnector/@SipConnector.d.ts +5 -3
- package/dist/SipConnector/eventNames.d.ts +1 -1
- package/dist/__fixtures__/RTCPeerConnectionMock.d.ts +2 -1
- package/dist/__fixtures__/RTCRtpTransceiverMock.d.ts +16 -0
- package/dist/__fixtures__/RTCSessionMock.d.ts +7 -0
- package/dist/doMock.cjs +1 -1
- package/dist/doMock.js +169 -143
- package/dist/index.cjs +1 -1
- package/dist/index.js +2 -2
- package/package.json +17 -17
- package/dist/@SipConnector-BDt5-Q2O.cjs +0 -1
package/README.md
CHANGED
|
@@ -9,37 +9,30 @@
|
|
|
9
9
|
|
|
10
10
|
**sip-connector** — это TypeScript SDK для интеграции WebRTC-приложений с платформой Vinteo через SIP-протокол. Библиотека построена на базе `@krivega/jssip` и предоставляет высокоуровневый API для создания полнофункциональных видеоконференций.
|
|
11
11
|
|
|
12
|
-
###
|
|
13
|
-
|
|
14
|
-
**Важные изменения, требующие обновления кода:**
|
|
12
|
+
### 🎯 Основные возможности
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
- **Удален параметр `simulcastEncodings`**: Вместо него используется `sendEncodings` для настройки кодировок
|
|
18
|
-
- **Автоматический запуск балансировки**: VideoSendingBalancer теперь автоматически запускается через 10 секунд после начала звонка
|
|
19
|
-
- **Новые события балансировки**: Добавлены события `video-balancer:*` для мониторинга состояния балансировки
|
|
14
|
+
SDK предоставляет комплексное решение для:
|
|
20
15
|
|
|
21
|
-
|
|
16
|
+
| Категория | Возможности |
|
|
17
|
+
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
18
|
+
| **SIP-подключения** | Регистрация на сервере (SIP REGISTER), управление сессиями |
|
|
19
|
+
| **WebRTC-коммуникации** | Исходящие/входящие звонки (SIP INVITE/200 OK), медиа-потоки, управление transceiver'ами, автоматический перезапуск ICE |
|
|
20
|
+
| **Презентации** | Отправка второго потока (screen sharing, демонстрация экрана) |
|
|
21
|
+
| **Системные сообщения** | DTMF, SIP INFO, синхронизация медиа-состояния |
|
|
22
|
+
| **Событийная архитектура** | Подписка на события платформы в реальном времени |
|
|
23
|
+
| **Мониторинг** | WebRTC-статистика (RTCRtpStats, ICE candidate stats) |
|
|
24
|
+
| **Управление конференциями** | Перемещение участников между ролями (участник/зритель) |
|
|
25
|
+
| **Лицензирование** | Мониторинг использования лицензий и состояния презентаций |
|
|
22
26
|
|
|
23
27
|
- **Адаптивный polling**: Улучшенная система опроса для мониторинга изменений видеотреков
|
|
24
28
|
- **Поддержка maxBitrate в PresentationManager**: Автоматическое управление битрейтом для презентаций
|
|
25
29
|
- **Предпочтительные кодеки в SipConnector**: Настройка приоритетов кодеков на уровне коннектора
|
|
26
30
|
- **Обработка смены треков**: Автоматическая адаптация балансировки при изменении видеотреков
|
|
27
31
|
- **Улучшенная статистика**: Расширенные возможности сбора и анализа WebRTC статистики
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
| Категория | Возможности |
|
|
34
|
-
| ---------------------------- | ------------------------------------------------------------- |
|
|
35
|
-
| **SIP-подключения** | Регистрация на сервере (SIP REGISTER), управление сессиями |
|
|
36
|
-
| **WebRTC-коммуникации** | Исходящие/входящие звонки (SIP INVITE/200 OK), медиа-потоки |
|
|
37
|
-
| **Презентации** | Отправка второго потока (screen sharing, демонстрация экрана) |
|
|
38
|
-
| **Системные сообщения** | DTMF, SIP INFO, синхронизация медиа-состояния |
|
|
39
|
-
| **Событийная архитектура** | Подписка на события платформы в реальном времени |
|
|
40
|
-
| **Мониторинг** | WebRTC-статистика (RTCRtpStats, ICE candidate stats) |
|
|
41
|
-
| **Управление конференциями** | Перемещение участников между ролями (участник/зритель) |
|
|
42
|
-
| **Лицензирование** | Мониторинг использования лицензий и состояния презентаций |
|
|
32
|
+
- **Автоматический перезапуск ICE**: Обработка событий `restart` от сервера с автоматическим вызовом `restartIce`
|
|
33
|
+
- **Управление transceiver'ами**: Новый `TransceiverManager` для отслеживания и управления RTCRtpTransceiver'ами
|
|
34
|
+
- **Автоматическое добавление transceiver'ов**: Умное добавление презентационных transceiver'ов при событиях restart
|
|
35
|
+
|
|
|
43
36
|
|
|
44
37
|
### 🏗️ Архитектура
|
|
45
38
|
|
|
@@ -66,23 +59,6 @@ yarn add sip-connector
|
|
|
66
59
|
pnpm add sip-connector
|
|
67
60
|
```
|
|
68
61
|
|
|
69
|
-
### 📋 Системные требования
|
|
70
|
-
|
|
71
|
-
#### Обязательные зависимости
|
|
72
|
-
|
|
73
|
-
| Компонент | Требование | Описание |
|
|
74
|
-
| ------------------ | -------------------- | ------------------------------ |
|
|
75
|
-
| `@krivega/jssip` | peer dependency | Для SIP-функциональности |
|
|
76
|
-
| WebRTC API | Поддержка в браузере | Стандартные WebRTC возможности |
|
|
77
|
-
| JavaScript runtime | ES2017+ | Современный синтаксис |
|
|
78
|
-
|
|
79
|
-
#### Рекомендуемые зависимости
|
|
80
|
-
|
|
81
|
-
| Компонент | Версия | Назначение |
|
|
82
|
-
| ---------- | ------ | ------------------- |
|
|
83
|
-
| TypeScript | 4.5+ | Полная типизация |
|
|
84
|
-
| Node.js | 16+ | Сборка и разработка |
|
|
85
|
-
|
|
86
62
|
---
|
|
87
63
|
|
|
88
64
|
## 🎯 Быстрый старт
|
|
@@ -369,6 +345,165 @@ try {
|
|
|
369
345
|
}
|
|
370
346
|
```
|
|
371
347
|
|
|
348
|
+
### Перезапуск ICE-соединения
|
|
349
|
+
|
|
350
|
+
#### Ручной перезапуск
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
// Перезапуск ICE для восстановления соединения
|
|
354
|
+
try {
|
|
355
|
+
const success = await sipConnector.callManager.restartIce({
|
|
356
|
+
useUpdate: true, // Использовать SIP UPDATE вместо re-INVITE
|
|
357
|
+
extraHeaders: ['X-Restart-Reason: Connection-Reset'],
|
|
358
|
+
rtcOfferConstraints: {
|
|
359
|
+
offerToReceiveAudio: true,
|
|
360
|
+
offerToReceiveVideo: true,
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
if (success) {
|
|
365
|
+
console.log('ICE перезапущен успешно');
|
|
366
|
+
} else {
|
|
367
|
+
console.warn('Перезапуск ICE не удался');
|
|
368
|
+
}
|
|
369
|
+
} catch (error) {
|
|
370
|
+
console.error('Ошибка перезапуска ICE:', error);
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
#### Автоматический перезапуск по событию сервера
|
|
375
|
+
|
|
376
|
+
SDK автоматически обрабатывает события `restart` от сервера и инициирует перезапуск ICE-соединения с интеллектуальным управлением transceiver'ами:
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
// SDK автоматически подписывается на события restart от ApiManager
|
|
380
|
+
// и выполняет следующие действия:
|
|
381
|
+
// 1. Проверяет необходимость добавления презентационного transceiver'а
|
|
382
|
+
// 2. Вызывает callManager.restartIce()
|
|
383
|
+
|
|
384
|
+
// Мониторинг событий restart (опционально)
|
|
385
|
+
sipConnector.on('api:restart', (data) => {
|
|
386
|
+
console.log('Получено событие restart от сервера:', {
|
|
387
|
+
tracksDirection: data.tracksDirection, // 'incoming', 'outgoing', 'bidirectional'
|
|
388
|
+
audioTrackCount: data.audioTrackCount,
|
|
389
|
+
videoTrackCount: data.videoTrackCount,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// SDK автоматически:
|
|
393
|
+
// - Добавит презентационный transceiver если videoTrackCount === 2
|
|
394
|
+
// - Вызовет restartIce()
|
|
395
|
+
console.log('ICE будет перезапущен автоматически');
|
|
396
|
+
|
|
397
|
+
if (data.videoTrackCount === 2) {
|
|
398
|
+
console.log('Может быть добавлен презентационный transceiver');
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Параметры перезапуска ICE
|
|
404
|
+
|
|
405
|
+
| Параметр | Тип | Описание | По умолчанию |
|
|
406
|
+
| ----------------------- | -------- | ---------------------------------------- | ------------ |
|
|
407
|
+
| `useUpdate` | boolean | Использовать SIP UPDATE вместо re-INVITE | `false` |
|
|
408
|
+
| `extraHeaders` | string[] | Дополнительные SIP заголовки | `[]` |
|
|
409
|
+
| `rtcOfferConstraints` | object | Ограничения для создания SDP offer | `{}` |
|
|
410
|
+
| `sendEncodings` | array | Параметры кодирования для видеопотока | `[]` |
|
|
411
|
+
| `degradationPreference` | string | Приоритет при ухудшении качества | `undefined` |
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## 🎛️ Управление RTCRtpTransceiver'ами
|
|
416
|
+
|
|
417
|
+
### Обзор TransceiverManager
|
|
418
|
+
|
|
419
|
+
SDK автоматически отслеживает и управляет RTCRtpTransceiver'ами через новый класс `TransceiverManager`:
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
// Получение текущих transceiver'ов
|
|
423
|
+
const transceivers = sipConnector.callManager.getTransceivers();
|
|
424
|
+
|
|
425
|
+
console.log('Основной аудио transceiver:', transceivers.mainAudio);
|
|
426
|
+
console.log('Основной видео transceiver:', transceivers.mainVideo);
|
|
427
|
+
console.log('Презентационный видео transceiver:', transceivers.presentationVideo);
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Типы transceiver'ов
|
|
431
|
+
|
|
432
|
+
SDK автоматически классифицирует transceiver'ы по их `mid` значению:
|
|
433
|
+
|
|
434
|
+
| Тип transceiver'а | mid | Назначение |
|
|
435
|
+
| ------------------- | --- | --------------------------- |
|
|
436
|
+
| `mainAudio` | '0' | Основной аудио поток |
|
|
437
|
+
| `mainVideo` | '1' | Основной видео поток |
|
|
438
|
+
| `presentationVideo` | '2' | Презентационный видео поток |
|
|
439
|
+
|
|
440
|
+
### Автоматическое добавление transceiver'ов
|
|
441
|
+
|
|
442
|
+
При получении события `restart` с `videoTrackCount === 2`, SDK автоматически добавляет презентационный transceiver если он отсутствует:
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
// SDK автоматически обрабатывает события restart
|
|
446
|
+
sipConnector.on('api:restart', (data) => {
|
|
447
|
+
if (data.videoTrackCount === 2) {
|
|
448
|
+
// SDK проверит наличие presentationVideo transceiver'а
|
|
449
|
+
// и добавит его автоматически если необходимо
|
|
450
|
+
console.log('Будет добавлен презентационный transceiver');
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Ручное управление transceiver'ами
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
// Добавление нового transceiver'а
|
|
459
|
+
try {
|
|
460
|
+
const audioTransceiver = await sipConnector.callManager.addTransceiver('audio', {
|
|
461
|
+
direction: 'sendrecv',
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
const videoTransceiver = await sipConnector.callManager.addTransceiver('video', {
|
|
465
|
+
direction: 'sendonly',
|
|
466
|
+
sendEncodings: [
|
|
467
|
+
{ rid: 'low', maxBitrate: 500_000, scaleResolutionDownBy: 4 },
|
|
468
|
+
{ rid: 'high', maxBitrate: 2_000_000, scaleResolutionDownBy: 1 },
|
|
469
|
+
],
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
console.log('Transceiver'ы добавлены:', { audioTransceiver, videoTransceiver });
|
|
473
|
+
} catch (error) {
|
|
474
|
+
console.error('Ошибка добавления transceiver'а:', error);
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Мониторинг transceiver'ов
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
// Проверка состояния transceiver'ов
|
|
482
|
+
const checkTransceivers = () => {
|
|
483
|
+
const transceivers = sipConnector.callManager.getTransceivers();
|
|
484
|
+
|
|
485
|
+
console.log('Статус transceiver'ов:', {
|
|
486
|
+
hasAudio: transceivers.mainAudio !== undefined,
|
|
487
|
+
hasVideo: transceivers.mainVideo !== undefined,
|
|
488
|
+
hasPresentation: transceivers.presentationVideo !== undefined,
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// Детальная информация
|
|
492
|
+
if (transceivers.mainVideo) {
|
|
493
|
+
console.log('Основное видео:', {
|
|
494
|
+
mid: transceivers.mainVideo.mid,
|
|
495
|
+
direction: transceivers.mainVideo.direction,
|
|
496
|
+
currentDirection: transceivers.mainVideo.currentDirection,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
// Проверка после установки соединения
|
|
502
|
+
sipConnector.on('call:confirmed', () => {
|
|
503
|
+
checkTransceivers();
|
|
504
|
+
});
|
|
505
|
+
```
|
|
506
|
+
|
|
372
507
|
---
|
|
373
508
|
|
|
374
509
|
## 📡 События и их обработка
|
|
@@ -381,7 +516,7 @@ SDK использует **событийно-ориентированную а
|
|
|
381
516
|
| ------------------ | ------------------------ | ----------------------------------------- |
|
|
382
517
|
| `connection:*` | События подключения | `connected`, `disconnected` |
|
|
383
518
|
| `call:*` | События звонков | `accepted`, `ended`, `failed` |
|
|
384
|
-
| `api:*` | События от сервера | `enterRoom`, `useLicense`
|
|
519
|
+
| `api:*` | События от сервера | `enterRoom`, `useLicense`, `restart` |
|
|
385
520
|
| `incoming-call:*` | События входящих звонков | `incomingCall` |
|
|
386
521
|
| `presentation:*` | События презентаций | `started`, `stopped` |
|
|
387
522
|
| `stats:*` | События статистики | `collected` |
|
|
@@ -416,6 +551,10 @@ sipConnector.on('api:enterRoom', ({ room }) => {
|
|
|
416
551
|
sipConnector.on('api:useLicense', (license) => {
|
|
417
552
|
console.log('Лицензия:', license);
|
|
418
553
|
});
|
|
554
|
+
|
|
555
|
+
sipConnector.on('api:restart', (data) => {
|
|
556
|
+
console.log('Событие restart от сервера:', data);
|
|
557
|
+
});
|
|
419
558
|
```
|
|
420
559
|
|
|
421
560
|
### Продвинутые паттерны
|
|
@@ -432,7 +571,7 @@ const roomData = await sipConnector.wait('api:enterRoom');
|
|
|
432
571
|
console.log('Данные комнаты:', roomData);
|
|
433
572
|
```
|
|
434
573
|
|
|
435
|
-
### События балансировки видео
|
|
574
|
+
### События балансировки видео
|
|
436
575
|
|
|
437
576
|
```typescript
|
|
438
577
|
// Мониторинг автоматической балансировки видео
|
|
@@ -582,7 +721,7 @@ monitor.subscribe(videoSender, () => {
|
|
|
582
721
|
|
|
583
722
|
### Автоматическая балансировка
|
|
584
723
|
|
|
585
|
-
|
|
724
|
+
`VideoSendingBalancer` интегрирован в `SipConnector` и запускается автоматически:
|
|
586
725
|
|
|
587
726
|
```typescript
|
|
588
727
|
const sipConnector = new SipConnector(
|
|
@@ -650,7 +789,7 @@ await connectionQueueManager.register();
|
|
|
650
789
|
await connectionQueueManager.checkTelephony(params);
|
|
651
790
|
```
|
|
652
791
|
|
|
653
|
-
###
|
|
792
|
+
### Механизм работы
|
|
654
793
|
|
|
655
794
|
- **Очередь операций**: Использует `stack-promises` с `noRunIsNotActual: true`
|
|
656
795
|
- **Предотвращение конфликтов**: Исключает одновременные connect/disconnect операции
|
|
@@ -691,10 +830,31 @@ import {
|
|
|
691
830
|
SipConnector, // Низкоуровневый API
|
|
692
831
|
SipConnectorFacade, // Высокоуровневый фасад
|
|
693
832
|
StatsPeerConnection, // Сбор статистики
|
|
833
|
+
TransceiverManager, // Управление transceiver'ами
|
|
694
834
|
// ... другие экспорты
|
|
695
835
|
} from 'sip-connector';
|
|
696
836
|
```
|
|
697
837
|
|
|
838
|
+
### Методы управления соединением
|
|
839
|
+
|
|
840
|
+
```typescript
|
|
841
|
+
// SipConnectorFacade методы
|
|
842
|
+
const facade = new SipConnectorFacade(sipConnector);
|
|
843
|
+
|
|
844
|
+
// Замена медиа-потока
|
|
845
|
+
await facade.replaceMediaStream(mediaStream, options);
|
|
846
|
+
|
|
847
|
+
// Получение удаленных потоков
|
|
848
|
+
const streams = facade.getRemoteStreams();
|
|
849
|
+
|
|
850
|
+
// Управление transceiver'ами (низкоуровневый API)
|
|
851
|
+
const transceivers = sipConnector.callManager.getTransceivers();
|
|
852
|
+
await sipConnector.callManager.addTransceiver('video', { direction: 'sendrecv' });
|
|
853
|
+
|
|
854
|
+
// Перезапуск ICE-соединения (низкоуровневый API)
|
|
855
|
+
await sipConnector.callManager.restartIce(options);
|
|
856
|
+
```
|
|
857
|
+
|
|
698
858
|
### Утилиты и типы
|
|
699
859
|
|
|
700
860
|
```typescript
|
|
@@ -712,6 +872,8 @@ import {
|
|
|
712
872
|
type TContentHint, // Подсказки для кодирования
|
|
713
873
|
type TInboundStats, // Входящая статистика
|
|
714
874
|
type TOutboundStats, // Исходящая статистика
|
|
875
|
+
type TRestartData, // Данные события restart
|
|
876
|
+
type ITransceiverStorage, // Интерфейс хранения transceiver'ов
|
|
715
877
|
} from 'sip-connector';
|
|
716
878
|
```
|
|
717
879
|
|
|
@@ -768,6 +930,29 @@ try {
|
|
|
768
930
|
}
|
|
769
931
|
```
|
|
770
932
|
|
|
933
|
+
### Восстановление соединения
|
|
934
|
+
|
|
935
|
+
```typescript
|
|
936
|
+
// Мониторинг качества соединения
|
|
937
|
+
facade.onStats(({ outbound, inbound }) => {
|
|
938
|
+
// Проверка качества соединения
|
|
939
|
+
if (outbound.packetsLost > 0.05) {
|
|
940
|
+
// 5% потерь пакетов
|
|
941
|
+
console.warn('Высокие потери пакетов, перезапуск ICE');
|
|
942
|
+
|
|
943
|
+
// Перезапуск ICE для восстановления соединения
|
|
944
|
+
sipConnector.callManager
|
|
945
|
+
.restartIce({
|
|
946
|
+
useUpdate: true,
|
|
947
|
+
extraHeaders: ['X-Restart-Reason: High-Packet-Loss'],
|
|
948
|
+
})
|
|
949
|
+
.catch((error) => {
|
|
950
|
+
console.error('Ошибка перезапуска ICE:', error);
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
```
|
|
955
|
+
|
|
771
956
|
### Управление ресурсами
|
|
772
957
|
|
|
773
958
|
```typescript
|
|
@@ -828,6 +1013,19 @@ console.log('Зарегистрирован:', facade.isRegistered);
|
|
|
828
1013
|
|
|
829
1014
|
// Проверка конфигурации
|
|
830
1015
|
console.log('Настроен:', facade.isConfigured());
|
|
1016
|
+
|
|
1017
|
+
// Диагностика ICE-соединения
|
|
1018
|
+
const checkIceConnection = async () => {
|
|
1019
|
+
try {
|
|
1020
|
+
const success = await sipConnector.callManager.restartIce({
|
|
1021
|
+
useUpdate: true,
|
|
1022
|
+
extraHeaders: ['X-Debug: ICE-Check'],
|
|
1023
|
+
});
|
|
1024
|
+
console.log('ICE соединение:', success ? 'OK' : 'Проблемы');
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
console.error('ICE недоступен:', error.message);
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
831
1029
|
```
|
|
832
1030
|
|
|
833
1031
|
---
|
|
@@ -884,6 +1082,11 @@ if (!navigator.mediaDevices?.getUserMedia) {
|
|
|
884
1082
|
if (!navigator.mediaDevices?.getDisplayMedia) {
|
|
885
1083
|
console.warn('Screen sharing не поддерживается');
|
|
886
1084
|
}
|
|
1085
|
+
|
|
1086
|
+
// Проверка поддержки ICE restart
|
|
1087
|
+
if (!RTCPeerConnection.prototype.restartIce) {
|
|
1088
|
+
console.warn('ICE restart не поддерживается в этом браузере');
|
|
1089
|
+
}
|
|
887
1090
|
```
|
|
888
1091
|
|
|
889
1092
|
---
|