loop control and external links
Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
16
README.md
16
README.md
@@ -19,10 +19,13 @@
|
|||||||
- **Miniaturas en Vivo**: Previsualizaciones de video actualizadas cada 2 segundos con hover preview
|
- **Miniaturas en Vivo**: Previsualizaciones de video actualizadas cada 2 segundos con hover preview
|
||||||
- **Visualización Remota**: Haz click en cualquier usuario para ver su reproductor en tiempo real
|
- **Visualización Remota**: Haz click en cualquier usuario para ver su reproductor en tiempo real
|
||||||
- **Streaming bajo Demanda**: WebRTC se activa solo cuando alguien quiere ver tu contenido
|
- **Streaming bajo Demanda**: WebRTC se activa solo cuando alguien quiere ver tu contenido
|
||||||
|
- **Enlaces Compartibles**: Genera enlaces para que usuarios externos se unan directamente a tu stream
|
||||||
|
- **Auto-conexión**: Los usuarios que reciben un link compartido se conectan automáticamente al stream
|
||||||
|
|
||||||
### 💬 Chat en Tiempo Real
|
### 💬 Chat en Tiempo Real
|
||||||
- Sistema de chat multiusuario con Socket.IO
|
- Sistema de chat multiusuario con Socket.IO
|
||||||
- Lista de usuarios conectados con indicadores visuales
|
- Lista de usuarios conectados con indicadores visuales
|
||||||
|
- Indicador visual (👁️) para ver quién está viendo tu stream
|
||||||
- Notificaciones de entrada/salida de usuarios
|
- Notificaciones de entrada/salida de usuarios
|
||||||
- Limitación de mensajes para prevenir spam
|
- Limitación de mensajes para prevenir spam
|
||||||
|
|
||||||
@@ -31,6 +34,7 @@
|
|||||||
- **Límite de Conexiones**: Máximo 5 conexiones simultáneas por IP
|
- **Límite de Conexiones**: Máximo 5 conexiones simultáneas por IP
|
||||||
- **Validación de Datos**: Sanitización automática de mensajes y nombres
|
- **Validación de Datos**: Sanitización automática de mensajes y nombres
|
||||||
- **CORS Configurado**: Seguridad en comunicaciones cross-origin
|
- **CORS Configurado**: Seguridad en comunicaciones cross-origin
|
||||||
|
- **Protección contra Loops**: Prevención automática de bucles de video infinitos
|
||||||
|
|
||||||
### 🌐 Proxy de Streams
|
### 🌐 Proxy de Streams
|
||||||
- Endpoints integrados para streams RTVE (La 1, La 2, 24H)
|
- Endpoints integrados para streams RTVE (La 1, La 2, 24H)
|
||||||
@@ -128,7 +132,17 @@ docker-compose up -d
|
|||||||
3. Haz click en el usuario para cargar su stream en tu reproductor
|
3. Haz click en el usuario para cargar su stream en tu reproductor
|
||||||
4. El video se transmitirá directamente vía WebRTC (P2P)
|
4. El video se transmitirá directamente vía WebRTC (P2P)
|
||||||
|
|
||||||
### 5. Volver a tu Video
|
### 5. Compartir tu Stream con Enlaces
|
||||||
|
|
||||||
|
1. Haz click en el botón "🔗 Compartir" en el header del chat
|
||||||
|
2. El enlace se copiará automáticamente al portapapeles
|
||||||
|
3. Comparte el enlace con quien quieras
|
||||||
|
4. Cuando alguien abra el enlace, se le pedirá un nombre de usuario
|
||||||
|
5. Después de registrarse, se conectará automáticamente a tu stream
|
||||||
|
|
||||||
|
**Formato del enlace**: `https://tu-dominio.com?watch=tu_nombre_usuario`
|
||||||
|
|
||||||
|
### 6. Volver a tu Video
|
||||||
|
|
||||||
- Haz click en el botón "✕ Cerrar" en el banner morado
|
- Haz click en el botón "✕ Cerrar" en el banner morado
|
||||||
- Volverás a tu reproductor local
|
- Volverás a tu reproductor local
|
||||||
|
|||||||
22
server.js
22
server.js
@@ -416,6 +416,28 @@ app.prepare().then(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Notificar loop detectado
|
||||||
|
socket.on('peer-loop-detected', (data) => {
|
||||||
|
try {
|
||||||
|
if (!socket.username || !data || !data.to) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetSocket = Array.from(io.sockets.sockets.values())
|
||||||
|
.find(s => s.username === data.to);
|
||||||
|
|
||||||
|
if (targetSocket) {
|
||||||
|
targetSocket.emit('peer-loop-detected', {
|
||||||
|
from: socket.username,
|
||||||
|
message: data.message
|
||||||
|
});
|
||||||
|
console.log(`⚠️ Loop detectado entre ${socket.username} y ${data.to}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al notificar loop:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Solicitar ver stream de usuario
|
// Solicitar ver stream de usuario
|
||||||
socket.on('request-watch', (data) => {
|
socket.on('request-watch', (data) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export default function Home() {
|
|||||||
const [remoteStream, setRemoteStream] = useState(null);
|
const [remoteStream, setRemoteStream] = useState(null);
|
||||||
const [watchingUser, setWatchingUser] = useState(null);
|
const [watchingUser, setWatchingUser] = useState(null);
|
||||||
const [isCapturingStream, setIsCapturingStream] = useState(false);
|
const [isCapturingStream, setIsCapturingStream] = useState(false);
|
||||||
|
const [autoConnectUser, setAutoConnectUser] = useState(null);
|
||||||
|
const [showAutoConnectBanner, setShowAutoConnectBanner] = useState(false);
|
||||||
const videoPlayerRef = useRef(null);
|
const videoPlayerRef = useRef(null);
|
||||||
const p2pManagerRef = useRef(null);
|
const p2pManagerRef = useRef(null);
|
||||||
const [stats, setStats] = useState({
|
const [stats, setStats] = useState({
|
||||||
@@ -127,9 +129,33 @@ export default function Home() {
|
|||||||
} else {
|
} else {
|
||||||
setVideoUrl(exampleVideos[0].url);
|
setVideoUrl(exampleVideos[0].url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detectar parámetro ?watch=username para auto-conectar
|
||||||
|
const watchParam = searchParams.get('watch');
|
||||||
|
if (watchParam) {
|
||||||
|
setAutoConnectUser(watchParam);
|
||||||
|
console.log('🔗 Link compartido detectado. Auto-conectando con:', watchParam);
|
||||||
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
|
|
||||||
|
// Auto-conectar cuando el usuario ingresa al chat (después de registrarse)
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoConnectUser && username && socket && socket.connected) {
|
||||||
|
console.log('🚀 Iniciando auto-conexión con:', autoConnectUser);
|
||||||
|
setShowAutoConnectBanner(true);
|
||||||
|
|
||||||
|
// Esperar 1 segundo para que el usuario vea el banner y el socket esté completamente listo
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
handleWatchUser(autoConnectUser);
|
||||||
|
setShowAutoConnectBanner(false);
|
||||||
|
setAutoConnectUser(null); // Limpiar para no reconectar
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [autoConnectUser, username, socket, handleWatchUser]);
|
||||||
|
|
||||||
const handleVideoStats = useCallback((data) => {
|
const handleVideoStats = useCallback((data) => {
|
||||||
setStats(prev => ({
|
setStats(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -227,6 +253,13 @@ export default function Home() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Protección contra loops: si el usuario objetivo ya me está viendo, no permitir
|
||||||
|
if (peers.includes(targetUser)) {
|
||||||
|
console.error('⚠️ Loop detectado: el usuario ya te está viendo');
|
||||||
|
alert(`⚠️ No se puede crear conexión: ${targetUser} ya está viendo tu stream.\n\nEsto crearía un bucle infinito de video. Espera a que ${targetUser} cierre tu stream primero.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('👁️ Solicitando ver a:', targetUser);
|
console.log('👁️ Solicitando ver a:', targetUser);
|
||||||
console.log('Estado actual:', {
|
console.log('Estado actual:', {
|
||||||
socket: !!socket,
|
socket: !!socket,
|
||||||
@@ -260,6 +293,15 @@ export default function Home() {
|
|||||||
setRemoteStream(null);
|
setRemoteStream(null);
|
||||||
}, [watchingUser]);
|
}, [watchingUser]);
|
||||||
|
|
||||||
|
const handleLoopDetected = useCallback((fromUser) => {
|
||||||
|
console.log('⚠️ Loop detectado, limpiando estado de watchingUser');
|
||||||
|
// Si estamos viendo a ese usuario, cerrar
|
||||||
|
if (watchingUser === fromUser) {
|
||||||
|
setWatchingUser(null);
|
||||||
|
setRemoteStream(null);
|
||||||
|
}
|
||||||
|
}, [watchingUser]);
|
||||||
|
|
||||||
const loadCustomUrl = () => {
|
const loadCustomUrl = () => {
|
||||||
if (customUrl.trim()) {
|
if (customUrl.trim()) {
|
||||||
// Si la URL es externa, usar el proxy
|
// Si la URL es externa, usar el proxy
|
||||||
@@ -325,6 +367,19 @@ export default function Home() {
|
|||||||
{/* Columna Principal - Video */}
|
{/* Columna Principal - Video */}
|
||||||
<div className="lg:col-span-2 space-y-6">
|
<div className="lg:col-span-2 space-y-6">
|
||||||
{/* Banner de stream remoto */}
|
{/* Banner de stream remoto */}
|
||||||
|
{/* Banner de auto-conexión */}
|
||||||
|
{showAutoConnectBanner && (
|
||||||
|
<div className="bg-gradient-to-r from-green-400 to-blue-500 text-white rounded-lg p-4 shadow-lg animate-pulse">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<span className="text-3xl">🔗</span>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold">Conectando automáticamente...</h3>
|
||||||
|
<p className="text-sm">Uniéndose al stream de {autoConnectUser}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{watchingUser && (
|
{watchingUser && (
|
||||||
<div className="bg-purple-100 border-2 border-purple-500 rounded-lg p-4">
|
<div className="bg-purple-100 border-2 border-purple-500 rounded-lg p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -516,6 +571,7 @@ export default function Home() {
|
|||||||
localStream={localStream}
|
localStream={localStream}
|
||||||
onRemoteStream={handleRemoteStream}
|
onRemoteStream={handleRemoteStream}
|
||||||
onNeedStream={captureLocalStream}
|
onNeedStream={captureLocalStream}
|
||||||
|
onLoopDetected={handleLoopDetected}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -526,6 +582,7 @@ export default function Home() {
|
|||||||
onUsernameChange={setUsername}
|
onUsernameChange={setUsername}
|
||||||
onSocketReady={handleSocketReady}
|
onSocketReady={handleSocketReady}
|
||||||
onWatchUser={handleWatchUser}
|
onWatchUser={handleWatchUser}
|
||||||
|
peers={peers}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { io } from 'socket.io-client';
|
|||||||
/**
|
/**
|
||||||
* Componente de Chat en tiempo real con Socket.IO y thumbnails de video
|
* Componente de Chat en tiempo real con Socket.IO y thumbnails de video
|
||||||
*/
|
*/
|
||||||
export default function Chat({ username, onUsernameChange, onSocketReady, onWatchUser }) {
|
export default function Chat({ username, onUsernameChange, onSocketReady, onWatchUser, peers = [] }) {
|
||||||
const [socket, setSocket] = useState(null);
|
const [socket, setSocket] = useState(null);
|
||||||
const [messages, setMessages] = useState([]);
|
const [messages, setMessages] = useState([]);
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
@@ -15,6 +15,7 @@ export default function Chat({ username, onUsernameChange, onSocketReady, onWatc
|
|||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const [tempUsername, setTempUsername] = useState(username || '');
|
const [tempUsername, setTempUsername] = useState(username || '');
|
||||||
const [hoveredUser, setHoveredUser] = useState(null);
|
const [hoveredUser, setHoveredUser] = useState(null);
|
||||||
|
const [showCopiedTooltip, setShowCopiedTooltip] = useState(false);
|
||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
|
|
||||||
// Auto-scroll al final de los mensajes
|
// Auto-scroll al final de los mensajes
|
||||||
@@ -160,6 +161,19 @@ export default function Chat({ username, onUsernameChange, onSocketReady, onWatc
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const shareMyStream = () => {
|
||||||
|
const shareUrl = `${window.location.origin}?watch=${encodeURIComponent(username)}`;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(shareUrl).then(() => {
|
||||||
|
setShowCopiedTooltip(true);
|
||||||
|
setTimeout(() => setShowCopiedTooltip(false), 2000);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Error al copiar:', err);
|
||||||
|
// Fallback: mostrar prompt
|
||||||
|
prompt('Copia este enlace:', shareUrl);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Formulario de username si no está conectado
|
// Formulario de username si no está conectado
|
||||||
if (!username) {
|
if (!username) {
|
||||||
return (
|
return (
|
||||||
@@ -203,7 +217,24 @@ export default function Chat({ username, onUsernameChange, onSocketReady, onWatc
|
|||||||
<span className="text-white text-sm">{isConnected ? 'Conectado' : 'Desconectado'}</span>
|
<span className="text-white text-sm">{isConnected ? 'Conectado' : 'Desconectado'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-white text-sm mt-1">Como: <span className="font-semibold">{username}</span></p>
|
<div className="flex items-center justify-between mt-2">
|
||||||
|
<p className="text-white text-sm">Como: <span className="font-semibold">{username}</span></p>
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
onClick={shareMyStream}
|
||||||
|
className="bg-white/20 hover:bg-white/30 text-white text-xs font-medium px-3 py-1 rounded-full transition-colors flex items-center space-x-1"
|
||||||
|
title="Compartir enlace para que otros vean tu stream"
|
||||||
|
>
|
||||||
|
<span>🔗</span>
|
||||||
|
<span>Compartir</span>
|
||||||
|
</button>
|
||||||
|
{showCopiedTooltip && (
|
||||||
|
<div className="absolute top-full right-0 mt-1 bg-green-600 text-white text-xs px-2 py-1 rounded shadow-lg whitespace-nowrap z-50">
|
||||||
|
✓ Enlace copiado
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Usuarios conectados */}
|
{/* Usuarios conectados */}
|
||||||
@@ -216,6 +247,7 @@ export default function Chat({ username, onUsernameChange, onSocketReady, onWatc
|
|||||||
const isCurrentUser = user === username;
|
const isCurrentUser = user === username;
|
||||||
const hasThumbnail = userThumbnails[user]?.thumbnail;
|
const hasThumbnail = userThumbnails[user]?.thumbnail;
|
||||||
const isHovered = hoveredUser === user;
|
const isHovered = hoveredUser === user;
|
||||||
|
const isWatchingMe = peers.includes(user); // Este usuario me está viendo
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -235,12 +267,17 @@ export default function Chat({ username, onUsernameChange, onSocketReady, onWatc
|
|||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="truncate">{user}</span>
|
<span className="truncate">{user}</span>
|
||||||
{hasThumbnail && !isCurrentUser && (
|
<div className="flex items-center space-x-1">
|
||||||
<span className="ml-1 text-red-500">🔴</span>
|
{isWatchingMe && !isCurrentUser && (
|
||||||
)}
|
<span className="text-purple-600" title="Te está viendo">👁️</span>
|
||||||
{isCurrentUser && (
|
)}
|
||||||
<span className="ml-1">👤</span>
|
{hasThumbnail && !isCurrentUser && (
|
||||||
)}
|
<span className="text-red-500">🔴</span>
|
||||||
|
)}
|
||||||
|
{isCurrentUser && (
|
||||||
|
<span>👤</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ const P2PManager = forwardRef(({
|
|||||||
onPeersUpdate,
|
onPeersUpdate,
|
||||||
localStream = null,
|
localStream = null,
|
||||||
onRemoteStream = null,
|
onRemoteStream = null,
|
||||||
onNeedStream = null
|
onNeedStream = null,
|
||||||
|
onLoopDetected = null
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const [peers, setPeers] = useState([]);
|
const [peers, setPeers] = useState([]);
|
||||||
const [stats, setStats] = useState({
|
const [stats, setStats] = useState({
|
||||||
@@ -64,6 +65,7 @@ const P2PManager = forwardRef(({
|
|||||||
const onPeersUpdateRef = useRef(onPeersUpdate);
|
const onPeersUpdateRef = useRef(onPeersUpdate);
|
||||||
const onRemoteStreamRef = useRef(onRemoteStream);
|
const onRemoteStreamRef = useRef(onRemoteStream);
|
||||||
const onNeedStreamRef = useRef(onNeedStream);
|
const onNeedStreamRef = useRef(onNeedStream);
|
||||||
|
const onLoopDetectedRef = useRef(onLoopDetected);
|
||||||
|
|
||||||
// Actualizar refs cuando cambien las callbacks
|
// Actualizar refs cuando cambien las callbacks
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -71,7 +73,8 @@ const P2PManager = forwardRef(({
|
|||||||
onPeersUpdateRef.current = onPeersUpdate;
|
onPeersUpdateRef.current = onPeersUpdate;
|
||||||
onRemoteStreamRef.current = onRemoteStream;
|
onRemoteStreamRef.current = onRemoteStream;
|
||||||
onNeedStreamRef.current = onNeedStream;
|
onNeedStreamRef.current = onNeedStream;
|
||||||
}, [onPeerStats, onPeersUpdate, onRemoteStream, onNeedStream]);
|
onLoopDetectedRef.current = onLoopDetected;
|
||||||
|
}, [onPeerStats, onPeersUpdate, onRemoteStream, onNeedStream, onLoopDetected]);
|
||||||
|
|
||||||
// Exponer métodos al componente padre
|
// Exponer métodos al componente padre
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
@@ -130,6 +133,17 @@ const P2PManager = forwardRef(({
|
|||||||
socket.on('peer-requested', (data) => {
|
socket.on('peer-requested', (data) => {
|
||||||
if (!data || !data.from) return;
|
if (!data || !data.from) return;
|
||||||
|
|
||||||
|
// PROTECCIÓN CONTRA LOOPS: Si YO estoy viendo a ese usuario, rechazar
|
||||||
|
const amWatchingThem = peers.includes(data.from);
|
||||||
|
if (amWatchingThem) {
|
||||||
|
console.error('⚠️ Loop detectado: estás viendo a', data.from, 'y ahora intenta verte');
|
||||||
|
socket.emit('peer-loop-detected', {
|
||||||
|
to: data.from,
|
||||||
|
message: `No se puede establecer conexión: ya estás viendo a ${username}. Esto crearía un bucle infinito.`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Verificar si ya existe un peer
|
// Verificar si ya existe un peer
|
||||||
const existingPeer = peersRef.current[data.from];
|
const existingPeer = peersRef.current[data.from];
|
||||||
if (existingPeer) {
|
if (existingPeer) {
|
||||||
@@ -194,6 +208,30 @@ const P2PManager = forwardRef(({
|
|||||||
alert(`No se puede conectar: ${data.message}`);
|
alert(`No se puede conectar: ${data.message}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Loop detectado por el otro usuario
|
||||||
|
socket.on('peer-loop-detected', (data) => {
|
||||||
|
console.error('⚠️ Loop detectado por el otro usuario:', data.message);
|
||||||
|
alert(`⚠️ Loop de video detectado\n\n${data.message}`);
|
||||||
|
|
||||||
|
// Si tenemos un peer con ese usuario, cerrarlo
|
||||||
|
if (data.from && peersRef.current[data.from]) {
|
||||||
|
const peer = peersRef.current[data.from];
|
||||||
|
peer.destroy();
|
||||||
|
delete peersRef.current[data.from];
|
||||||
|
delete remoteStreamsRef.current[data.from];
|
||||||
|
setPeers(prev => {
|
||||||
|
const newPeers = prev.filter(p => p !== data.from);
|
||||||
|
if (onPeersUpdateRef.current) onPeersUpdateRef.current(newPeers);
|
||||||
|
return newPeers;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notificar al componente padre
|
||||||
|
if (onLoopDetectedRef.current) {
|
||||||
|
onLoopDetectedRef.current(data.from);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Intervalo para calcular estadísticas
|
// Intervalo para calcular estadísticas
|
||||||
statsInterval.current = setInterval(() => {
|
statsInterval.current = setInterval(() => {
|
||||||
const uploadSpeed = uploadBytes.current / 5;
|
const uploadSpeed = uploadBytes.current / 5;
|
||||||
@@ -226,6 +264,7 @@ const P2PManager = forwardRef(({
|
|||||||
socket.off('peer-requested');
|
socket.off('peer-requested');
|
||||||
socket.off('signal');
|
socket.off('signal');
|
||||||
socket.off('peer-not-found');
|
socket.off('peer-not-found');
|
||||||
|
socket.off('peer-loop-detected');
|
||||||
|
|
||||||
Object.values(peersRef.current).forEach(peer => {
|
Object.values(peersRef.current).forEach(peer => {
|
||||||
if (peer) peer.destroy();
|
if (peer) peer.destroy();
|
||||||
|
|||||||
Referencia en una nueva incidencia
Block a user