diff --git a/doc/instructions/coturn-install.md b/doc/instructions/coturn-install.md new file mode 100644 index 0000000..a714882 --- /dev/null +++ b/doc/instructions/coturn-install.md @@ -0,0 +1,75 @@ +# Установка TURN сервера (coturn) + +## 1. Что нужно +- Сервер с публичным IP (в примере: `37.214.58.208`). +- Доступ `root` по SSH. +- Открытые порты в firewall: + - `3478/tcp` + - `3478/udp` + - диапазон relay-портов, например `49160-49200/udp` + +## 2. Быстрая установка (рекомендуется) +В проекте уже есть готовый скрипт: + +```bash +sudo bash scripts/setup_turn_coturn.sh \ + --secret "CHANGE_ME_LONG_RANDOM_SECRET" \ + --realm "shineup.me" +``` + +Что делает скрипт: +- ставит `coturn`; +- включает режим `use-auth-secret` (временные логин/пароль); +- пишет `/etc/turnserver.conf`; +- включает и перезапускает сервис `coturn`. + +## 3. Проверка +Проверить статус: + +```bash +sudo systemctl status coturn --no-pager +``` + +Проверить, что порт слушается: + +```bash +sudo ss -lntup | grep 3478 +``` + +## 4. Ручная установка (если без скрипта) +```bash +sudo apt-get update +sudo apt-get install -y coturn +``` + +Пример `/etc/turnserver.conf`: + +```conf +listening-port=3478 +fingerprint +lt-cred-mech +use-auth-secret +static-auth-secret=CHANGE_ME_LONG_RANDOM_SECRET +realm=shineup.me +external-ip=37.214.58.208 +listening-ip=0.0.0.0 +relay-ip=37.214.58.208 +min-port=49160 +max-port=49200 +no-cli +simple-log +``` + +В `/etc/default/coturn`: + +```conf +TURNSERVER_ENABLED=1 +``` + +Запуск: + +```bash +sudo systemctl enable coturn +sudo systemctl restart coturn +``` + diff --git a/doc/instructions/turn-connect-to-shine-server.md b/doc/instructions/turn-connect-to-shine-server.md new file mode 100644 index 0000000..ababbc6 --- /dev/null +++ b/doc/instructions/turn-connect-to-shine-server.md @@ -0,0 +1,48 @@ +# Подключение TURN к SHiNE-серверу + +Начиная с текущей версии, клиент звонков запрашивает ICE-конфиг у backend через WS-операцию `GetCallIceConfig` и использует её для `RTCPeerConnection`. + +## 1. Настройки backend +Файл: `src/main/resources/application.properties` + +Ключи: + +```properties +call.ice.stun.urls=stun:stun.l.google.com:19302 +call.ice.turn.urls=turn:37.214.58.208:3478?transport=udp,turn:37.214.58.208:3478?transport=tcp +call.ice.turn.ttlSec=600 +call.ice.turn.userPrefix=shine +call.ice.turn.sharedSecret=CHANGE_ME_LONG_RANDOM_SECRET + +# fallback (если не используете shared-secret) +call.ice.turn.username= +call.ice.turn.password= +``` + +## 2. Рекомендуемый режим (временные credentials) +- На coturn и на SHiNE-сервере должен быть **одинаковый** secret: + - coturn: `static-auth-secret=...` + - SHiNE: `call.ice.turn.sharedSecret=...` +- Тогда SHiNE выдаёт короткоживущие `turnUsername/turnPassword` (TTL). + +## 3. Fallback режим (статический логин/пароль) +Если временные credentials не используются: + +```properties +call.ice.turn.sharedSecret= +call.ice.turn.username=turn_user +call.ice.turn.password=turn_password +``` + +## 4. Деплой после изменения +```bash +./gradlew deployServerNoCleanNoTests +./gradlew deployWEB +``` + +## 5. Проверка +1. Авторизоваться двумя клиентами. +2. Запустить звонок. +3. Проверить, что звонок устанавливается даже в сети, где прямой P2P затруднён. +4. Если TURN недоступен, клиент автоматически откатится к STUN-конфигу по умолчанию. + diff --git a/shine-UI/js/services/call-ui-service.js b/shine-UI/js/services/call-ui-service.js index 12dde59..4fc20af 100644 --- a/shine-UI/js/services/call-ui-service.js +++ b/shine-UI/js/services/call-ui-service.js @@ -46,7 +46,7 @@ function ensureUi() { acceptBtn = document.createElement('button'); acceptBtn.type = 'button'; - acceptBtn.className = 'primary-btn'; + acceptBtn.className = 'call-accept-btn'; acceptBtn.textContent = 'Ответить'; acceptBtn.addEventListener('click', async () => { await acceptIncomingCall(); diff --git a/shine-UI/js/services/pwa-push-service.js b/shine-UI/js/services/pwa-push-service.js index 2e2eb1b..ff5d971 100644 --- a/shine-UI/js/services/pwa-push-service.js +++ b/shine-UI/js/services/pwa-push-service.js @@ -1,4 +1,49 @@ const LS_KEY = 'shine-ui-webpush-subscription-v1'; +const SW_UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1000; +let swUpdateIntervalId = null; +let controllerChangeHandled = false; + +function setupServiceWorkerAutoUpdate(registration, log) { + if (!registration) return; + + if (!controllerChangeHandled && 'serviceWorker' in navigator) { + controllerChangeHandled = true; + navigator.serviceWorker.addEventListener('controllerchange', () => { + log({ + level: 'info', + source: 'web-push', + message: 'Service Worker обновился, перезагружаем UI', + }); + window.location.reload(); + }); + } + + registration.addEventListener('updatefound', () => { + const installing = registration.installing; + if (!installing) return; + log({ + level: 'info', + source: 'web-push', + message: 'Найдено обновление Service Worker', + }); + }); + + if (swUpdateIntervalId) { + window.clearInterval(swUpdateIntervalId); + swUpdateIntervalId = null; + } + + const checkNow = () => { + registration.update().catch(() => {}); + }; + + checkNow(); + swUpdateIntervalId = window.setInterval(checkNow, SW_UPDATE_CHECK_INTERVAL_MS); + document.addEventListener('visibilitychange', () => { + if (document.visibilityState !== 'visible') return; + checkNow(); + }); +} function urlBase64ToUint8Array(base64String) { const padding = '='.repeat((4 - (base64String.length % 4)) % 4); @@ -36,13 +81,16 @@ export async function initPwaPush({ authService, onLog = null }) { } try { - const registration = await navigator.serviceWorker.register('./firebase-messaging-sw.js'); + const registration = await navigator.serviceWorker.register('./firebase-messaging-sw.js', { + updateViaCache: 'none', + }); log({ level: 'info', source: 'web-push', message: 'Service Worker зарегистрирован', details: { scope: registration.scope }, }); + setupServiceWorkerAutoUpdate(registration, log); const permission = await Notification.requestPermission(); if (permission !== 'granted') { diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css index a481b7d..e4dca4f 100644 --- a/shine-UI/styles/components.css +++ b/shine-UI/styles/components.css @@ -868,6 +868,33 @@ gap: 8px; } +.call-accept-btn { + border: 1px solid rgba(122, 230, 166, 0.64); + border-radius: var(--radius-sm); + background: linear-gradient(120deg, rgba(98, 210, 146, 0.95), rgba(61, 164, 108, 0.94)); + color: #052113; + padding: 9px 12px; + min-height: 38px; + font-weight: 700; + cursor: pointer; + transition: 0.2s ease; +} + +.call-accept-btn:hover { + border-color: rgba(156, 246, 194, 0.88); + transform: translateY(-1px); +} + +.call-accept-btn:disabled { + opacity: 1; + color: #93a6cc; + background: linear-gradient(180deg, rgba(18, 30, 54, 0.8), rgba(11, 20, 39, 0.86)); + border-color: rgba(124, 145, 189, 0.28); + box-shadow: none; + cursor: not-allowed; + transform: none; +} + .pwa-diag-list { gap: 8px; }