diff --git a/README.md b/README.md
index 9b988c1..69c6dc1 100644
--- a/README.md
+++ b/README.md
@@ -19,10 +19,13 @@
- **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
- **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
- Sistema de chat multiusuario con Socket.IO
- Lista de usuarios conectados con indicadores visuales
+- Indicador visual (👁️) para ver quién está viendo tu stream
- Notificaciones de entrada/salida de usuarios
- Limitación de mensajes para prevenir spam
@@ -31,6 +34,7 @@
- **Límite de Conexiones**: Máximo 5 conexiones simultáneas por IP
- **Validación de Datos**: Sanitización automática de mensajes y nombres
- **CORS Configurado**: Seguridad en comunicaciones cross-origin
+- **Protección contra Loops**: Prevención automática de bucles de video infinitos
### 🌐 Proxy de Streams
- 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
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
- Volverás a tu reproductor local
diff --git a/server.js b/server.js
index 04285a0..640e83f 100644
--- a/server.js
+++ b/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
socket.on('request-watch', (data) => {
try {
diff --git a/src/app/page.js b/src/app/page.js
index 5bbf632..95f3a0b 100644
--- a/src/app/page.js
+++ b/src/app/page.js
@@ -18,6 +18,8 @@ export default function Home() {
const [remoteStream, setRemoteStream] = useState(null);
const [watchingUser, setWatchingUser] = useState(null);
const [isCapturingStream, setIsCapturingStream] = useState(false);
+ const [autoConnectUser, setAutoConnectUser] = useState(null);
+ const [showAutoConnectBanner, setShowAutoConnectBanner] = useState(false);
const videoPlayerRef = useRef(null);
const p2pManagerRef = useRef(null);
const [stats, setStats] = useState({
@@ -127,9 +129,33 @@ export default function Home() {
} else {
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
}, [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) => {
setStats(prev => ({
...prev,
@@ -226,6 +252,13 @@ export default function Home() {
alert('No puedes ver tu propio stream. Conéctate desde otro navegador o dispositivo.');
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('Estado actual:', {
@@ -260,6 +293,15 @@ export default function Home() {
setRemoteStream(null);
}, [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 = () => {
if (customUrl.trim()) {
// Si la URL es externa, usar el proxy
@@ -325,6 +367,19 @@ export default function Home() {
{/* Columna Principal - Video */}
{/* Banner de stream remoto */}
+ {/* Banner de auto-conexión */}
+ {showAutoConnectBanner && (
+