fix: support complet iOS/mobile pour PTT (audio + HTTPS/WSS)
Modifications majeures : - HTTPS obligatoire pour getUserMedia sur iOS (certificats mkcert) - Proxy WSS Vite pour LiveKit (contourner mixed content HTTPS→WS) - Audio unlock explicite iOS dans useLiveKit (AudioContext) - Demande permission microphone avant connexion LiveKit - Touch optimizations CSS (touch-action, tap-highlight) - Meta iOS PWA (apple-mobile-web-app-capable) - Logs debug pour troubleshooting mobile - Attente publication track audio avant utilisation PTT Tests validés : ✅ iPhone Safari : émission + réception audio OK ✅ Desktop Chrome : fonctionne toujours ✅ 3+ devices simultanés 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# TODO.md - Plan de développement PTT Live
|
# TODO.md - Plan de développement PTT Live
|
||||||
|
|
||||||
**Dernière mise à jour** : 2026-05-21
|
**Dernière mise à jour** : 2026-05-22
|
||||||
**Phase actuelle** : PHASE 1 - Fondations
|
**Phase actuelle** : PHASE 1 - Fondations (Tests finaux en cours)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -111,6 +111,7 @@ Valider la faisabilité technique : 2-4 clients, PTT basique, latence < 150ms, m
|
|||||||
- [x] Mode PTT : mute/unmute track selon bouton
|
- [x] Mode PTT : mute/unmute track selon bouton
|
||||||
- [x] Gestion touch events (mobile)
|
- [x] Gestion touch events (mobile)
|
||||||
- [x] Gestion mouse events (desktop)
|
- [x] Gestion mouse events (desktop)
|
||||||
|
- [x] **Fix iOS/mobile** : audio unlock, HTTPS obligatoire, proxy WSS LiveKit
|
||||||
|
|
||||||
#### Styles
|
#### Styles
|
||||||
- [x] CSS mobile-first
|
- [x] CSS mobile-first
|
||||||
@@ -122,17 +123,17 @@ Valider la faisabilité technique : 2-4 clients, PTT basique, latence < 150ms, m
|
|||||||
### 1.5 Tests et validation Phase 1
|
### 1.5 Tests et validation Phase 1
|
||||||
|
|
||||||
#### Tests unitaires
|
#### Tests unitaires
|
||||||
- [ ] Opus encode/decode (qualité audio)
|
- [x] Opus encode/decode (qualité audio)
|
||||||
- [ ] Jitter buffer (buffer size stable)
|
- [x] Jitter buffer (buffer size stable)
|
||||||
- [ ] CoreAudio device detection
|
- [ ] CoreAudio device detection (naudiodon crash - à résoudre plus tard)
|
||||||
|
|
||||||
#### Tests d'intégration
|
#### Tests d'intégration
|
||||||
- [ ] Serveur démarre sans erreur
|
- [x] Serveur démarre sans erreur
|
||||||
- [ ] Client obtient token valide
|
- [x] Client obtient token valide
|
||||||
- [ ] Client rejoint room LiveKit
|
- [x] Client rejoint room LiveKit
|
||||||
|
|
||||||
#### Tests end-to-end
|
#### Tests end-to-end
|
||||||
- [ ] **Test 1** : 2 clients, PTT alterné, audio bidirectionnel
|
- [x] **Test 1** : 2 clients, PTT alterné, audio bidirectionnel
|
||||||
- [ ] **Test 2** : Mesure latence (clap → réception < 150ms)
|
- [ ] **Test 2** : Mesure latence (clap → réception < 150ms)
|
||||||
- [ ] **Test 3** : Stabilité 5min sans coupure
|
- [ ] **Test 3** : Stabilité 5min sans coupure
|
||||||
- [ ] **Test 4** : Reconnexion après perte WiFi
|
- [ ] **Test 4** : Reconnexion après perte WiFi
|
||||||
|
|||||||
+6
-1
@@ -3,10 +3,15 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||||
<meta name="theme-color" content="#1a1a1a" />
|
<meta name="theme-color" content="#1a1a1a" />
|
||||||
<meta name="description" content="Professional WebRTC Intercom for Event Technicians" />
|
<meta name="description" content="Professional WebRTC Intercom for Event Technicians" />
|
||||||
|
|
||||||
|
<!-- iOS PWA -->
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
|
|
||||||
<title>PTT Live</title>
|
<title>PTT Live</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC0hpMHdlPKTWp6
|
||||||
|
8ixzelM1fh3TWtwpY4HmDFKF09VaBhIv6mSBdiMS+pLyN2d+AAA9gj2WH5PfWSDK
|
||||||
|
pRaDhJgJF55wDZys0f15odyijm3qu0tttj8IHxS6Li5vqttOyO1KE6A4UKbw9Op3
|
||||||
|
0egMJ2VMaIH98cD0TCC+AGChDEsoPuhM2b5Q52OBHcugVw/akhVfdetdNYuBue1c
|
||||||
|
Ii+UyZkhF0PV/S67n9dg4XRq+Jed/3zUZQiJf/CT3pnW3umcOTUw2e3R4/TBuSOG
|
||||||
|
Mex6OF3zM2pNIxCejiKVW8VO4jTp+VSm+8i6xvjBb47GePOG99apVELWPiwsukSw
|
||||||
|
CRGJjGhXAgMBAAECggEAOrEB/kwXI8+VjdFMaGLdyKdvFPcWWxJx+hQJhF8Bn1oX
|
||||||
|
8aIX+QsqjhIPUlZ2/D0N1vGQCk3L6rJ0ec3AixPBxjr6lN2oEXvYGAJq1CLQU59+
|
||||||
|
/3Vf+sj4GSvIhx+aW3vxwcKttYFrNS27SSdidQkd4wCbOq+tlv9lKcC/qbxwdu2p
|
||||||
|
CblxDiwivtotuf/2bfG+YgV5y8qiRgn1OVsHZK6xKZ/i8stURcNTq/6CrmLXpNBz
|
||||||
|
OdzFBQ9JIRQPezh4gfPQ7Bt8T50gwi38TfkzdsK9vzhxsmV6dPZNjDpPS4EChOzy
|
||||||
|
gQ+roS7m74vFaHfsDtSBb2sebt6pFBxxUnhDDe8OqQKBgQDdgIU7MePskr62AXRZ
|
||||||
|
QgoUC9Do40FPHvR7TPtLGATPqR4Jix2Mkxyy6gXaXyHobpQ7oXjeky+hVJY7wu1W
|
||||||
|
/QEA0Vk2WMEs3sauITAz7wbIQHZKHTXufZphOVOPQlmTx+QTt94Khhu7NWnIUhWW
|
||||||
|
QXSqO9oQusY6696258WZNSv3YwKBgQDQpEp7giVGebhoY4ABmwQpmnAJ2avJIaj9
|
||||||
|
w40zqygs74VA8MVhVk48ZNt/XM/xkkB4KbsxYJrxg9+B4PQJ/wiv+4jVbMqWZMth
|
||||||
|
ahU6Stb8oTTzXJ6O3qIYrUpDK+ByD09snq/SsVo5FYYYGsP7NZ88rDAmbdQ9rrP1
|
||||||
|
sxkyEIP/fQKBgGgdYgKaB82KiJQaiOrvrLcResgNEgSzwy012STKDHDjyFeqCWCr
|
||||||
|
QaEjeU7UyqZrW8fPtXXBb3EAxoEetdren5sXzDxMabjCmlb9CKBQqTp1emSJ6HDK
|
||||||
|
n0c13/4FrP9WxPEzyu3dbamIiMl9M+JlsAXYjj6w3D6T4iLNPMcwBBOLAoGBALN0
|
||||||
|
a//5e/g3H47h7irzW0wxYqaGS8RuqDzEYwIK+D5WMgYeUZccNaSql0Tf3peIVN1F
|
||||||
|
/5VD42FSLP84Lo8ehilfr1zq+wEKZwg9x05hKrMWMUYU5ug5w7B39IT8C0vvsT/a
|
||||||
|
6Z3OH60zvyeidejvQSxdafjTxJbdWjo9trEiFXa9AoGBAIwj0zxK0YiZkXQWRP1b
|
||||||
|
B4IG4ZQbgpVKAlCYRQl7PCmaO8Eb9jYO1AQVCTONtR7DhINm/HpUAhjaokAya3wc
|
||||||
|
ckN/BbOwiehOprz/N5c1XkQOLWOz5LnTSk77EeyC84KiOuyqK46qAhQJ6zuUngsG
|
||||||
|
s+cPPn7xL+vCZWmYmmz4n+C7
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEpjCCAw6gAwIBAgIQOw0sc56bEW4nOW9fKvr8gDANBgkqhkiG9w0BAQsFADCB
|
||||||
|
sTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMUMwQQYDVQQLDDpiZW5v
|
||||||
|
aXRATWFjQm9vay1Qcm8tMTYtTTMtZGUtQmVub2l0LmxvY2FsIChCZW5vaXQgU2No
|
||||||
|
d2FydHopMUowSAYDVQQDDEFta2NlcnQgYmVub2l0QE1hY0Jvb2stUHJvLTE2LU0z
|
||||||
|
LWRlLUJlbm9pdC5sb2NhbCAoQmVub2l0IFNjaHdhcnR6KTAeFw0yNjA1MjIyMDU4
|
||||||
|
NTBaFw0yODA4MjIyMDU4NTBaMG4xJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVu
|
||||||
|
dCBjZXJ0aWZpY2F0ZTFDMEEGA1UECww6YmVub2l0QE1hY0Jvb2stUHJvLTE2LU0z
|
||||||
|
LWRlLUJlbm9pdC5sb2NhbCAoQmVub2l0IFNjaHdhcnR6KTCCASIwDQYJKoZIhvcN
|
||||||
|
AQEBBQADggEPADCCAQoCggEBALSGkwd2U8pNanryLHN6UzV+HdNa3CljgeYMUoXT
|
||||||
|
1VoGEi/qZIF2IxL6kvI3Z34AAD2CPZYfk99ZIMqlFoOEmAkXnnANnKzR/Xmh3KKO
|
||||||
|
beq7S222PwgfFLouLm+q207I7UoToDhQpvD06nfR6AwnZUxogf3xwPRMIL4AYKEM
|
||||||
|
Syg+6EzZvlDnY4Edy6BXD9qSFV916101i4G57VwiL5TJmSEXQ9X9Lruf12DhdGr4
|
||||||
|
l53/fNRlCIl/8JPemdbe6Zw5NTDZ7dHj9MG5I4Yx7Ho4XfMzak0jEJ6OIpVbxU7i
|
||||||
|
NOn5VKb7yLrG+MFvjsZ484b31qlUQtY+LCy6RLAJEYmMaFcCAwEAAaN8MHowDgYD
|
||||||
|
VR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFEVk
|
||||||
|
aBcatJettT33mJ0OUx+ILYQRMDIGA1UdEQQrMCmCCWxvY2FsaG9zdIcEfwAAAYcE
|
||||||
|
CgEBb4cQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0BAQsFAAOCAYEAUZAIBw4n
|
||||||
|
8pBXLSnpgw84bnwgLeVvzg53SKcUzUSuymImv9luhKySmodiVLw4M89K0RSSm7F0
|
||||||
|
SeRjzMr3sMhYa6K4sZtn5QtRhAQn0c+r6PTUgKe0PU5FmMV27DoIa9iS1BcIqjVF
|
||||||
|
G0QSdFRB1UqXlPVyBVN7XeQx4XYqVEbStZLSV0LxgHOAc73c6zXh00OrDpFdox4t
|
||||||
|
UeA7GUpZyFMm/mxuiQaBdY2m5CGgBPbtGOxlq3JOHj/aNcww5DP3m3o+M5TfT3lK
|
||||||
|
6Ex5j4N2ym5cLixon5vtqTkAmJlX70xB2qh+TmdZ+BDZ4Y1C0otEPSv6vQX4zV/2
|
||||||
|
I9gWl3w+sm85BAXff0TBahW0+p98o44M+y62xwhwUQ4/E9cQLJElFnXk2Hi9eHPk
|
||||||
|
mchbDPHv7L8rGHArl6GOIa2MVQxZKDjdTRtx6k2gYYB/qWoeMfM1E8hc2n5SANTP
|
||||||
|
Q24BYvk8qFH4ECz0hhxTX9rwvxHeTLB1exBBoSEVi/neagG1UsD6OA4H
|
||||||
|
-----END CERTIFICATE-----
|
||||||
+29
-2
@@ -56,6 +56,18 @@ function App() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// IMPORTANT iOS : Demander permission microphone AVANT tout
|
||||||
|
console.log('🎤 Demande permission microphone...');
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
|
||||||
|
console.log('✓ Permission microphone accordée');
|
||||||
|
// Arrêter le stream test immédiatement
|
||||||
|
stream.getTracks().forEach(track => track.stop());
|
||||||
|
} catch (permErr) {
|
||||||
|
console.error('❌ Permission microphone refusée:', permErr);
|
||||||
|
throw new Error('Accès microphone refusé. Autorisez dans les réglages iOS : Safari > Microphone.');
|
||||||
|
}
|
||||||
|
|
||||||
// Obtenir token du serveur
|
// Obtenir token du serveur
|
||||||
const response = await fetch(`${API_URL}/token`, {
|
const response = await fetch(`${API_URL}/token`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -69,12 +81,27 @@ function App() {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Adapter l'URL LiveKit selon le protocole de la page
|
||||||
|
let livekitUrl = data.url;
|
||||||
|
if (window.location.protocol === 'https:') {
|
||||||
|
// En HTTPS, utiliser le proxy WSS local via Vite
|
||||||
|
livekitUrl = `${window.location.protocol}//${window.location.host}/livekit`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔗 Connexion LiveKit:', livekitUrl);
|
||||||
|
|
||||||
// Se connecter à LiveKit
|
// Se connecter à LiveKit
|
||||||
await connect(data.url, data.token);
|
await connect(livekitUrl, data.token);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Erreur connexion:', err);
|
console.error('Erreur connexion:', err);
|
||||||
setError('Connexion impossible. Vérifiez le serveur.');
|
|
||||||
|
// Message d'erreur spécifique selon le type
|
||||||
|
if (err.message && err.message.includes('Microphone')) {
|
||||||
|
setError(err.message);
|
||||||
|
} else {
|
||||||
|
setError('Connexion impossible. Vérifiez le serveur et les permissions microphone.');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,12 @@
|
|||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
/* Mobile touch optimizations */
|
||||||
|
touch-action: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ptt-button::before {
|
.ptt-button::before {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export default function PTTButton({ isTalking, onPressStart, onPressEnd }) {
|
|||||||
// Touch events (mobile)
|
// Touch events (mobile)
|
||||||
const handleTouchStart = (e) => {
|
const handleTouchStart = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
console.log('🖐️ Touch start');
|
||||||
if (!isPressingRef.current) {
|
if (!isPressingRef.current) {
|
||||||
isPressingRef.current = true;
|
isPressingRef.current = true;
|
||||||
onPressStart();
|
onPressStart();
|
||||||
@@ -29,6 +30,7 @@ export default function PTTButton({ isTalking, onPressStart, onPressEnd }) {
|
|||||||
|
|
||||||
const handleTouchEnd = (e) => {
|
const handleTouchEnd = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
console.log('🖐️ Touch end');
|
||||||
if (isPressingRef.current) {
|
if (isPressingRef.current) {
|
||||||
isPressingRef.current = false;
|
isPressingRef.current = false;
|
||||||
onPressEnd();
|
onPressEnd();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default function useLiveKit() {
|
|||||||
const audioContextRef = useRef(null);
|
const audioContextRef = useRef(null);
|
||||||
const analyserRef = useRef(null);
|
const analyserRef = useRef(null);
|
||||||
const animationFrameRef = useRef(null);
|
const animationFrameRef = useRef(null);
|
||||||
|
const isAudioUnlockedRef = useRef(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connexion à la room LiveKit
|
* Connexion à la room LiveKit
|
||||||
@@ -37,6 +38,8 @@ export default function useLiveKit() {
|
|||||||
// Events
|
// Events
|
||||||
room.on(RoomEvent.Connected, () => {
|
room.on(RoomEvent.Connected, () => {
|
||||||
console.log('✓ Connecté à LiveKit');
|
console.log('✓ Connecté à LiveKit');
|
||||||
|
console.log(' Room name:', room.name);
|
||||||
|
console.log(' Participants distants:', room.remoteParticipants.size);
|
||||||
setIsConnected(true);
|
setIsConnected(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,7 +50,8 @@ export default function useLiveKit() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
room.on(RoomEvent.ParticipantConnected, (participant) => {
|
room.on(RoomEvent.ParticipantConnected, (participant) => {
|
||||||
console.log('Participant rejoint:', participant.identity);
|
console.log('🟢 Participant rejoint:', participant.identity);
|
||||||
|
console.log(' Total participants distants:', room.remoteParticipants.size);
|
||||||
updateParticipants();
|
updateParticipants();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -77,19 +81,44 @@ export default function useLiveKit() {
|
|||||||
updateParticipants();
|
updateParticipants();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Connexion
|
// Event track local publié
|
||||||
await room.connect(url, token);
|
room.on(RoomEvent.LocalTrackPublished, (publication) => {
|
||||||
|
console.log('✓ Track local publié:', publication.kind);
|
||||||
// Activer microphone (muted par défaut)
|
if (publication.kind === Track.Kind.Audio) {
|
||||||
await room.localParticipant.setMicrophoneEnabled(true);
|
const track = publication.track;
|
||||||
const track = room.localParticipant.audioTracks.values().next().value?.track;
|
console.log(' Track audio disponible:', track);
|
||||||
|
console.log(' isMuted:', track.isMuted);
|
||||||
if (track) {
|
|
||||||
localTrackRef.current = track;
|
localTrackRef.current = track;
|
||||||
// Mute par défaut (PTT)
|
// Mute par défaut (PTT)
|
||||||
track.mute();
|
track.mute();
|
||||||
setupAudioAnalyser(track);
|
setupAudioAnalyser(track);
|
||||||
|
console.log('✓ Track audio configuré et muted pour PTT');
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connexion
|
||||||
|
await room.connect(url, token);
|
||||||
|
|
||||||
|
console.log('📞 Connexion établie, activation microphone...');
|
||||||
|
|
||||||
|
// Activer microphone (muted par défaut)
|
||||||
|
await room.localParticipant.setMicrophoneEnabled(true);
|
||||||
|
|
||||||
|
console.log('🎤 Microphone activé, attente publication track...');
|
||||||
|
|
||||||
|
// Attendre que le track soit publié (max 3s)
|
||||||
|
let retries = 0;
|
||||||
|
while (!localTrackRef.current && retries < 30) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
retries++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!localTrackRef.current) {
|
||||||
|
console.error('❌ Timeout : track audio non publié après 3s');
|
||||||
|
throw new Error('Microphone non disponible. Autorisez l\'accès au micro dans les réglages iOS.');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✓ Track audio prêt');
|
||||||
|
|
||||||
updateParticipants();
|
updateParticipants();
|
||||||
|
|
||||||
@@ -112,25 +141,65 @@ export default function useLiveKit() {
|
|||||||
setParticipants([]);
|
setParticipants([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Débloque l'audio sur mobile (iOS/Android)
|
||||||
|
* Doit être appelé dans un gestionnaire d'événement utilisateur
|
||||||
|
*/
|
||||||
|
const unlockAudio = useCallback(() => {
|
||||||
|
if (isAudioUnlockedRef.current) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Créer un contexte audio silencieux pour débloquer l'API
|
||||||
|
const ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
const oscillator = ctx.createOscillator();
|
||||||
|
const gainNode = ctx.createGain();
|
||||||
|
|
||||||
|
gainNode.gain.value = 0; // Silence
|
||||||
|
oscillator.connect(gainNode);
|
||||||
|
gainNode.connect(ctx.destination);
|
||||||
|
oscillator.start(0);
|
||||||
|
oscillator.stop(0.001);
|
||||||
|
|
||||||
|
isAudioUnlockedRef.current = true;
|
||||||
|
console.log('✓ Audio débloqué (mobile)');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Audio unlock échoué:', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Commencer à parler (unmute micro)
|
* Commencer à parler (unmute micro)
|
||||||
*/
|
*/
|
||||||
const startTalking = useCallback(async () => {
|
const startTalking = useCallback(async () => {
|
||||||
if (!localTrackRef.current) return;
|
console.log('🎤 startTalking appelé');
|
||||||
|
console.log(' localTrackRef.current:', localTrackRef.current);
|
||||||
|
|
||||||
|
if (!localTrackRef.current) {
|
||||||
|
console.warn('⚠️ Pas de track audio local disponible');
|
||||||
|
alert('Microphone non disponible. Réessayez.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await localTrackRef.current.unmute();
|
// Débloquer audio sur mobile au premier appui
|
||||||
|
unlockAudio();
|
||||||
|
|
||||||
|
// Feedback immédiat AVANT unmute
|
||||||
setIsTalking(true);
|
setIsTalking(true);
|
||||||
console.log('🎤 PTT: Talking');
|
|
||||||
|
await localTrackRef.current.unmute();
|
||||||
|
console.log('🎤 PTT: Talking (unmuted)');
|
||||||
|
|
||||||
// Vibration haptique (si supporté)
|
// Vibration haptique (si supporté)
|
||||||
if (navigator.vibrate) {
|
if (navigator.vibrate) {
|
||||||
navigator.vibrate(50);
|
navigator.vibrate(50);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur unmute:', error);
|
console.error('❌ Erreur unmute:', error);
|
||||||
|
setIsTalking(false);
|
||||||
|
alert(`Erreur microphone: ${error.message}`);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [unlockAudio]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Arrêter de parler (mute micro)
|
* Arrêter de parler (mute micro)
|
||||||
@@ -163,7 +232,8 @@ export default function useLiveKit() {
|
|||||||
|
|
||||||
// Participants distants
|
// Participants distants
|
||||||
room.remoteParticipants.forEach((participant) => {
|
room.remoteParticipants.forEach((participant) => {
|
||||||
const audioPublication = Array.from(participant.audioTracks.values())[0];
|
const audioTracks = participant.audioTracks ? Array.from(participant.audioTracks.values()) : [];
|
||||||
|
const audioPublication = audioTracks[0];
|
||||||
const isSpeaking = room.activeSpeakers.some(s => s.identity === participant.identity);
|
const isSpeaking = room.activeSpeakers.some(s => s.identity === participant.identity);
|
||||||
|
|
||||||
participantsList.push({
|
participantsList.push({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import { VitePWA } from 'vite-plugin-pwa';
|
import { VitePWA } from 'vite-plugin-pwa';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -57,11 +58,21 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
host: true,
|
host: true,
|
||||||
|
https: {
|
||||||
|
key: fs.readFileSync('./localhost+3-key.pem'),
|
||||||
|
cert: fs.readFileSync('./localhost+3.pem'),
|
||||||
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:3000',
|
target: 'http://localhost:3000',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/api/, '')
|
rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
|
},
|
||||||
|
'/livekit': {
|
||||||
|
target: 'ws://10.1.1.111:7880',
|
||||||
|
ws: true,
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/livekit/, '')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user