Saltar al contenido principal

Historias y En Vivo — Referencia técnica

← Volver a Historias y En Vivo

Dónde vive esto

Backend

  • apps/backend/managers/live-stream-managers/live-stream.manager.js — implementa la lógica de negocio de cada operación de transmisión en vivo (createLiveStream, startLiveStream, endLiveStream, cancelLiveStream, deleteLiveStream, joinLiveStream, leaveLiveStream, banViewerFromLive/unbanViewerFromLive, sendLiveComment/deleteLiveComment/pinLiveComment/unpinLiveComment, reactToLiveStream/removeReaction, getCurrentlyLive, getTrendingLiveStreams, getScheduledStreams, getUserLiveStreams, getLiveStreamStats, la emisión de tokens de LiveKit, además de lo añadido más recientemente: sendGift/getGiftHistory/getLiveGiftCatalog, requestToSpeak/respondToSpeakerRequest/getSpeakerRequests/getMySpeakerStatus, y createLivePromotionPost). Corregido — ahora está completamente conectado a GraphQL. Una versión anterior de esta página decía que la mitad de "transmisión" de este manager no tenía resolver; eso era cierto en su momento pero ya no. Auditar este manager antes de conectarlo también reveló un bug sistémico: casi todas las llamadas .create()/.update()/.increment() y las lecturas de propiedades de instancia en este manager, sus 4 access-services y los 4 modelos Sequelize subyacentes usaban claves snake_case (user_id, live_stream_id, is_currently_watching, peak_viewer_count, ...) contra modelos cuyos atributos reales son camelCase — Sequelize descarta silenciosamente las claves no reconocidas en las escrituras en lugar de lanzar un error, así que escrituras como el is_banned: true de banViewer nunca se guardaban realmente, updatePeakViewerCount era código muerto (comparación contra undefined), la duración de endLiveStream siempre daba NaN, y la propia llamada al validador de joinLiveStream tenía las claves mal escritas, así que siempre lanzaba error. Además, los 4 modelos Sequelize de la familia LiveStream (LiveStream, LiveStreamComment, LiveStreamViewer, LiveStreamInteraction) tenían métodos associate() vacíos a pesar de que cada método de los access-services hacía eager-load de {model: User, as: 'user'} — Sequelize lanza EagerLoadingError para un alias de include sin asociación definida, así que prácticamente toda lectura de esta funcionalidad habría fallado en tiempo de ejecución. Todo esto se corrigió (camelCase en todas partes, asociaciones agregadas, además de un hueco de aplicación de visibilidad faltante en getLiveStream/joinLiveStream para transmisiones private/followers_only/close_friends) antes de construir encima la capa de GraphQL de abajo.
  • apps/backend/graphql/types/live-stream.type.js y resolvers/live-stream.resolver.js — el API completo de GraphQL de transmisión: cada query/mutation listada en API de GraphQL más abajo.
  • apps/backend/graphql/types/live-stream-archive.type.js y resolvers/live-stream-archive.resolver.js — un passthrough delgado y de solo lectura que expone liveArchive(limit, offset): las propias transmisiones terminadas del usuario, respaldado por live-stream.manager.js#getUserLiveStreams acotado a status: 'ended'.
  • apps/backend/data-access-services/live-stream/live-stream.access-service.js, live-stream-viewer.access-service.js, live-stream-comment.access-service.js, live-stream-interaction.access-service.js — capa de base de datos usada por el manager de arriba
  • apps/backend/managers/call-managers/speaker-request.manager.js y data-access-services/call/speaker-request.access-service.jsCorregido — se movió y ya no es toda la historia. Esta página antes citaba estos archivos en sus rutas anteriores (managers/speaker-request.manager.js, data-access-services/speaker-request.access-service.js) y decía que el flujo de "solicitar hablar" solo estaba conectado a Calls grupales. Ambas cosas siguen siendo ciertas para este par de archivos (respaldan pendingSpeakerRequests/mySpeakerRequests/cancelSpeakerRequest en call.resolver.js/call.type.js, solo para Calls) — pero las transmisiones en vivo ahora tienen su propia implementación de solicitud para hablar, separada, que no pasa en absoluto por este manager: requestToSpeak/respondToSpeakerRequest/getSpeakerRequests/getMySpeakerStatus construidas directamente en live-stream.manager.js, controladas mediante una columna speakerStatus en LiveStreamViewer. Ver la sección Solicitudes para hablar más abajo.
  • apps/backend/services/livekit.service.js y livekit-monitor.service.js — aprovisionamiento de salas de LiveKit, tokens y monitoreo de transmisiones
  • Corregido — las Historias sí están implementadas, pero no vía Tale. Esta página antes afirmaba que Historias no tenía ninguna implementación en el backend, citando un stub tale.access-service.js ("No DB model exists yet; returns safe empty values") como el único código relacionado con historias. Ese stub (junto con sus hermanos blast/article bajo data-access-services/admin/) fue eliminado por completo desde entonces — era código muerto mantenido únicamente para que los flujos de moderación de administradores no fallaran cuando una solicitud de reporte/eliminación nombraba uno de estos tipos de contenido que nunca se lanzaron; nunca existió un modelo Tale al cual enrutar. Las Historias realmente viven como filas de Post con type: 'story' (en paralelo a como los Clips son type: 'clip'), reutilizando la columna expires_at de la tabla Post en lugar de una tabla de historias dedicada. Un comentario de docblock desactualizado sobre Post.type en post.type.js ("'story' is reserved for the upcoming Stories feature and isn't creatable yet") está en sí mismo equivocado y es contradicho por la mutation createStory a apenas un par de docenas de líneas más abajo en el mismo archivo — no confíes en ese comentario.
  • apps/backend/graphql/types/post.type.js — las queries homeStories, userStories, hasActiveStory, storyViewers, storyViewerCount y la mutation createStory(input: StoryCreateInput!); StoryCreateInput.audience elige el PostVisibility de la historia (esta página antes decía que no existía ningún campo de audiencia — corregido)
  • apps/backend/graphql/resolvers/post.resolver.js — resuelve las operaciones de arriba vía postManager
  • apps/backend/managers/post-managers/post.manager.jscreateStory (fija expiresAt = now + STORY_DURATION_MS, visibility: validated.audience || 'followers'), getHomeStories, getUserStories, hasActiveStory. PostManager sigue sin tener métodos getStoryViewers/getStoryViewerCount — y eso ahora no importa, porque corregido en esta sesión, la solución se aplicó en el resolver en lugar del manager: los resolvers storyViewers/storyViewerCount de post.resolver.js ya no llaman a postManager en absoluto. Ahora delegan directamente a watch-history.manager.js#getPostViewers/#getPostViewersCount — la misma implementación real, solo para el dueño, que ya respalda postViewers/postViewersCount en watch-history.type.js / watch-history.resolver.js. Esta página antes decía que storyViewers/storyViewerCount lanzaban TypeError: ... is not a function en tiempo de ejecución por llamar a métodos inexistentes de PostManager — eso ya no es cierto.
  • apps/backend/validators/post.validator.jsvalidateStoryInput valida audience contra la misma lista blanca de validateVisibility (que ahora incluye close_friends), con followers como valor predeterminado si se omite
  • Corregido en esta sesión — ahora se aplica. Esta página antes decía que el camino de lectura de historias nunca verificaba audience de vuelta: getPost no tenía ninguna rama close_friends, y getHomeStories/getUserStories no aplicaban ningún filtro de visibilidad. Eso ya está corregido. getPost (~línea 643) ganó una rama close_friends junto a sus verificaciones existentes de private/followers/subscribers, controlada por userCloseFriendAccessService.isCloseFriend(post.userId, viewerId). Los caminos de listado de historias también quedaron controlados: getHomeStories filtra las historias close_friends de cualquier usuario seguido en cuya lista de amigos cercanos no esté el espectador (resuelto una vez por cada dueño distinto, no por cada historia, para evitar una consulta por historia), y getUserStories hace pasar a cada espectador que no es el dueño por un switch public/followers/close_friends (isFollowing/isCloseFriend), cerrando por defecto para cualquier otro valor. StoryCreateInput.audience ahora se aplica de verdad en la lectura, no solo se captura y se guarda. (close-friends.manager.js#isCloseFriend ya se usaba de verdad para transmisiones en vivo de amigos cercanos en live-stream.manager.js — ahora también se usa para historias.)

Frontend

  • apps/frontend-nextjs/src/page-components/settings/StoryLivePage.tsx (en la ruta apps/frontend-nextjs/src/app/settings/story-live) — una pantalla de controles de configuración (audiencia predeterminada de historias / compartir historias / guardar en archivo / ubicación), no la experiencia real de historias ni de transmisión en vivo. Corregido — estos controles sí están conectados, no son marcadores estáticos. Esta página antes decía que los controles de audiencia/alternadores no estaban conectados a ninguna operación de GraphQL; sí lo están — defaultStoryAudience, allowStorySharing, saveStoryToArchive y shareLocation se guardan todos mediante privacySettings/updatePrivacySettings (los mismos tipos PrivacySettings/PrivacySettingsInput que usa AccountPrivacyPage.tsx). La sección "Ocultar historia de" es una funcionalidad separada, también real y conectada — ver más abajo.
  • apps/frontend-nextjs/src/page-components/settings/ArchivePage.tsx (en la ruta apps/frontend-nextjs/src/app/settings/archive) — tiene una pestaña "Live" que consulta liveArchive y lista las transmisiones terminadas propias del usuario (miniatura, duración, conteos de vistas/me gusta/comentarios). Esta es la única pantalla de transmisión en vivo confirmada en frontend-nextjs.
  • apps/frontend-nextjs/src/components/stories/StoryRail.tsx — el carrusel horizontal de avatares de historias, renderizado en HomePage.tsx. Consulta homeStories, agrupa la lista plana de posts en un avatar por usuario, y abre CreateStoryModal (para tu propio anillo, si no tienes una historia activa) o StoryViewer (en caso contrario)
  • apps/frontend-nextjs/src/components/stories/StoryViewer.tsx — reproducción de historias a pantalla completa: barras de progreso por historia, navegación por toque izquierda/derecha y con flechas del teclado, avance automático (5s para imágenes, al terminar el video), llama a incrementPostViews (contador agregado) y recordPostView (registro por espectador, omitido en tu propia historia) una vez por historia, una barra de reacción rápida de 5 emojis que llama a likePost, una caja de comentarios que llama a createComment, y (solo el dueño, mediante un ícono de ojo) una hoja inferior "quién vio mi historia" respaldada por storyViewers
  • apps/frontend-nextjs/src/components/stories/CreateStoryModal.tsx — el compositor; llama a createStory con un selector de audience (Todos/Seguidores/Amigos cercanos). Acepta tanto imágenes como video (accept="image/*,video/mp4,video/quicktime,video/x-msvideo,video/webm", igualando la lista blanca de /upload del backend) — esta página antes decía que el video estaba bloqueado; la lista blanca del backend ya incluía mimetypes video/* (ver api/server.js), el selector de archivos de este modal era la única restricción restante — corregido
  • hasActiveStory está conectado al anillo de la foto de perfil tanto en ProfilePage.tsx como en PublicProfilePage.tsx
  • apps/frontend-nextjs/src/page-components/LiveDiscoveryPage.tsx (ruta /live) — cuadrículas de en vivo ahora / tendencias / programadas (currentlyLive, trendingLiveStreams, scheduledLiveStreams), cada tarjeta enlaza a /live/[id]; un botón "Transmitir en vivo" (solo usuarios autenticados) abre GoLiveModal
  • apps/frontend-nextjs/src/components/live/GoLiveModal.tsx — el formulario de creación de transmisión (título, descripción, visibilidad, permitir comentarios); llama a createLiveStream y navega a /live/{id} al terminar, donde el dueño inicia la transmisión real
  • apps/frontend-nextjs/src/page-components/LiveRoomPage.tsx (ruta /live/[id]) — la sala en sí, con estados distintos para dueño/espectador/programada/terminada. Dueño: startLiveStreamliveStreamerToken → se conecta a LiveKit como publicador (cámara + micrófono, con botones para alternar) usando Room/createLocalTracks de livekit-client; endLiveStream al detener. Espectador: joinLiveStream (devuelve un LiveKitConnectionInfo: token + url + roomName en una sola llamada) → se conecta como suscriptor, adjuntando las pistas de video/audio suscritas; leaveLiveStream al desmontar/salir de la página. Ambos: un feed de comentarios en vivo (liveStreamComments, sendLiveComment, fijar/desfijar y eliminar para el anfitrión), y una barra de reacción de 6 emojis (reactToLiveStream, conteos en vivo vía liveStreamReactionCounts). Aplica la visibility de la transmisión del lado del servidor (ver la corrección del manager arriba) — una transmisión privada/solo seguidores/amigos cercanos no se puede unir solo con conocer su ID.
  • apps/frontend-nextjs/src/components/Navigation.tsx — una entrada "Live" en el menú "Más", que enlaza a /live
  • Nota de dependencia: se agregó livekit-client a apps/frontend-nextjs/package.json pero no pudo instalarse en el entorno donde se construyó esto (sin acceso a la red hacia el registro de npm desde ese sandbox) — ejecuta npm install en apps/frontend-nextjs antes de compilar/ejecutar; hasta entonces los imports de livekit-client fallarán al resolverse.

Privacidad de historias — usuarios ocultos

query StoryHiddenUsers { storyHiddenUsers { id username profilePicture } }
mutation UpdateStoryHiddenUsers($userIds: [ID!]!) { updateStoryHiddenUsers(userIds: $userIds) { id username } }

Respaldado por graphql/types/extended-privacy-settings.type.js + privacy-settings.manager.js (getStoryHiddenUsers/updateStoryHiddenUsers), conectado de punta a punta. updateStoryHiddenUsers reemplaza la lista completa de exclusión en una sola llamada; el backend filtra el propio ID de quien llama del input para que un usuario nunca pueda ocultarse su historia a sí mismo. Este es un control de privacidad real y funcional sobre la funcionalidad real de Historias documentada más abajo (antes esta página lo describía como un control sobre una preferencia para una funcionalidad que todavía no existía — corregido).

API de GraphQL de Historias

mutation CreateStory($input: StoryCreateInput!) {
createStory(input: $input) { id createdAt user { username } media { mediaUrl mediaType } }
}

# Historias activas (no expiradas) propias + de usuarios seguidos — carrusel de inicio
query HomeStories { homeStories { id createdAt user { id username profilePicture } } }

# Las historias activas de un solo usuario, de la más antigua a la más nueva — la lista de historias por usuario del visor
query UserStories($userId: ID!) {
userStories(userId: $userId) { id text createdAt media { mediaUrl mediaType } }
}

# Indicador del anillo en la foto de perfil
query HasActiveStory($userId: ID!) { hasActiveStory(userId: $userId) }

# Se marca como vista y se comenta igual que un post normal
mutation IncrementStoryViews($postId: ID!) { incrementPostViews(postId: $postId) { id } }
mutation CommentOnStory($postId: ID!, $text: String!) {
createComment(input: { postId: $postId, text: $text }) { id }
}

StoryCreateInput.audience acepta public, followers o close_friends (cualquier otro valor de PostVisibility es técnicamente aceptado por el validador compartido pero no tiene mucho sentido para una historia) y usa followers como valor predeterminado si se omite, igualando el comportamiento fijo anterior. La lista de Amigos cercanos (Configuración → Amigos cercanos) ahora se puede conectar como audiencia de una historia.

Aplicado en la lectura. audience se valida, se guarda y — corregido en esta sesión — ahora también se vuelve a verificar en cada camino de lectura. Ver la corrección "Corregido en esta sesión — ahora se aplica" bajo Dónde vive esto arriba.

mutation RecordStoryView($postId: ID!) { recordPostView(postId: $postId) }
mutation ReactToStory($postId: ID!, $interactionType: InteractionType) {
likePost(postId: $postId, interactionType: $interactionType) { id reactionType }
}

# Solo el dueño - lanza error para cualquier otra persona
query StoryViewers($postId: ID!, $limit: Int) {
storyViewers(postId: $postId, limit: $limit) { viewedAt viewer { id username profilePicture } }
}
query StoryViewerCount($postId: ID!) { storyViewerCount(postId: $postId) }

Corregido en esta sesión — ya funciona. Ambos resolvers delegan directamente a watch-history.manager.js#getPostViewers/#getPostViewersCount — ver la corrección bajo Dónde vive esto arriba. La hoja "quién vio mi historia" de StoryViewer.tsx llama a storyViewers, que ahora devuelve datos reales en lugar de fallar.

Checklist de implementación técnica

  • createStory — conectado de punta a punta: post.resolver.js + CreateStoryModal.tsx (solo imágenes — ver la nota de Frontend arriba)
  • homeStories / StoryRail.tsx — conectado de punta a punta
  • userStories / hasActiveStory / StoryViewer.tsx — conectado de punta a punta, incluyendo el anillo de la foto de perfil en ProfilePage.tsx y PublicProfilePage.tsx
  • Conteo de vistas de historias (incrementPostViews) — conectado, solo conteo agregado
  • Comentar en una historia (createComment) — conectado, reutiliza la mutation de comentarios de posts normal
  • Selección de audiencia por historia (Todos / Seguidores / Amigos cercanos) — StoryCreateInput.audience se captura y se guarda al crear la historia; corregido en esta sesión: ahora también se aplica en cada camino de lectura (la nueva rama close_friends de getPost, más el filtrado de visibilidad en getHomeStories/getUserStories) — ver la corrección arriba
  • Historias en video — el selector de archivos de CreateStoryModal.tsx ahora acepta video/*, igualando la lista blanca del backend que ya existía
  • "Quién vio mi historia" — corregido en esta sesión: storyViewers/storyViewerCount ahora delegan directamente a watch-history.manager.js#getPostViewers/#getPostViewersCount (solo para el dueño), la misma implementación real que ya respalda postViewers/postViewersCount — ver la corrección arriba. La hoja "quién vio mi historia" de StoryViewer.tsx ya funciona de punta a punta.
  • Reaccionar a una historia con un emoji — una barra de reacción rápida de 5 emojis en StoryViewer.tsx, reutilizando el sistema genérico likePost/InteractionType que ya tenían las publicaciones normales
  • createLiveStream / startLiveStream / endLiveStream / cancelLiveStream / deleteLiveStream — conectado de punta a punta: live-stream.resolver.js + GoLiveModal.tsx + LiveRoomPage.tsx (controles del dueño)
  • joinLiveStream / leaveLiveStream — conectado de punta a punta: LiveRoomPage.tsx (espectador), devuelve un LiveKitConnectionInfo (token + url + roomName); visibilidad aplicada del lado del servidor
  • banLiveViewer / unbanLiveViewer — resolver + manager conectados (el bug de persistencia a nivel de manager está corregido — ver la corrección de backend arriba); aún no hay interfaz de moderación dedicada en LiveRoomPage.tsx, se puede llamar solo vía GraphQL
  • sendLiveComment / pinLiveComment / unpinLiveComment / deleteLiveComment — conectado de punta a punta: live-stream.resolver.js + la barra lateral de comentarios de LiveRoomPage.tsx (fijar/desfijar restringido al anfitrión; eliminar permitido para el propio autor del comentario o el anfitrión)
  • reactToLiveStream / removeLiveStreamReaction — conectado de punta a punta: live-stream.resolver.js + la barra de reacciones de LiveRoomPage.tsx, con conteos en vivo vía liveStreamReactionCounts
  • currentlyLive / trendingLiveStreams / scheduledLiveStreams — conectado de punta a punta: live-stream.resolver.js + LiveDiscoveryPage.tsx (/live)
  • Regalos con monedas en vivo — conectado de punta a punta: live-stream-gift.type.js/live-stream-gift.resolver.js (sendLiveGift, liveGiftCatalog, liveStreamGifts) + un selector de regalos en LiveRoomPage.tsx; las monedas se mueven de espectador → transmisor a través del libro contable de monedas (sendTip), respaldado por la tabla live_stream_gift
  • Solicitudes para hablar en transmisiones en vivo — requestLiveSpeaker/liveStreamSpeakerRequests/myLiveSpeakerStatus/respondToLiveSpeakerRequest, conectado de punta a punta: live-stream.resolver.js (implementado directamente en live-stream.manager.js, independiente del speaker-request.manager.js que es solo para Call) + LiveSpeakerControls.tsx. La aprobación cambia LiveStreamViewer.speakerStatus a approved; el espectador debe volver a unirse para recibir realmente un token de publicación de LiveKit (no hay reconexión automática)
  • StoryLivePage.tsx (/settings/story-live) — el selector de audiencia predeterminada y los tres alternadores (compartir historias / guardar en archivo / ubicación) están conectados a privacySettings/updatePrivacySettings; "Ocultar historia de" está conectado por separado vía storyHiddenUsers/updateStoryHiddenUsers
  • promoteLiveStream — conectado de punta a punta: live-stream.resolver.js + un botón "Promocionar" en LiveRoomPage.tsx; crea o reutiliza un Post de anuncio complementario que enlaza de vuelta a la transmisión, el cual luego pasa por el flujo normal de promoción de posts (las transmisiones en vivo no son Posts en sí mismas)
  • liveArchive — conectado de punta a punta: live-stream-archive.resolver.js + la pestaña Live de ArchivePage.tsx

Configuración

Las preferencias de Historias y En Vivo se administran en Configuración → Historias y En Vivo (/settings/story-live).

Historias

Funciones

  • Subir una historia de foto o video que expira después de 24 horas
  • Elegir una audiencia por historia: Todos, Seguidores o Amigos cercanos
  • Comentar en una historia (reutiliza la caja de comentarios de posts normal)
  • Reaccionar con uno de 5 emojis rápidos (reutiliza el sistema de reacciones de publicaciones normales)
  • El dueño puede ver un conteo agregado de vistas, además de una lista "quién vio mi historia" (storyViewers) — ambas conectadas de punta a punta, solo para el dueño, desde la corrección de esta sesión — ver el Checklist de implementación técnica arriba

Visor de historias

Una superposición a pantalla completa (StoryViewer.tsx) con navegación por toque para avanzar, barras de progreso por historia, y avance automático (5s por imagen, o al terminar el video).

Historias para Amigos cercanos

La lista de Amigos cercanos (Configuración → Amigos cercanos, agregar/quitar personas) ahora se puede elegir como audiencia de una historia mediante StoryCreateInput.audience: close_friends. Como se señaló arriba, esta selección ahora se aplica cuando se ve la historia — corregido en esta sesión — solo el dueño y las personas en su lista de amigos cercanos pueden verla.

En Vivo

Funciones

  • Iniciar una transmisión de video en vivo (GoLiveModal.tsxcreateLiveStream, luego startLiveStream desde la página de la sala)
  • Los espectadores se unen y ven vía LiveKit WebRTC, y envían comentarios en tiempo real y reacciones con emojis
  • El anfitrión ve un conteo de espectadores en vivo, el feed de comentarios y los totales de reacciones; puede fijar/eliminar comentarios y alternar su propio micrófono/cámara
  • La transmisión termina cuando el anfitrión llama a endLiveStream (o se desconecta — del lado de LiveKit, aún no respaldado por un cierre automático del lado del servidor al desconectarse)

Reacciones en vivo

Los espectadores pueden enviar una de 6 reacciones con emoji (👍❤️😂😮😢😡) vía reactToLiveStream; llamarla de nuevo con un tipo distinto simplemente cambia la reacción existente de quien llama, y removeLiveStreamReaction la elimina. liveStreamReactionCounts devuelve totales en vivo por tipo.

Regalos con monedas en vivo

Los espectadores envían regalos con precio en monedas desde un selector en la sala en vivo: liveGiftCatalog lista los regalos disponibles y sus precios, sendLiveGift transfiere monedas de espectador → transmisor (precio autoritativo del servidor, con verificación de saldo a través del libro contable de monedas), y liveStreamGifts lista los regalos recibidos de una transmisión. Respaldado por la tabla live_stream_gift. Ver Historias y En Vivo y el archivo separado live-stream-gift.type.js.

Permisos

AjusteOpciones
Quién puede ver/unirse a tu transmisión en vivoPública / Privada / Solo seguidores / Amigos cercanos — LiveStreamVisibility, aplicado del lado del servidor tanto en getLiveStream como en joinLiveStream
Comentarios en vivoallowComments — habilitado/deshabilitado por transmisión
CompartirallowSharing — habilitado/deshabilitado por transmisión
Regalos con monedasImplementado — sendLiveGift / liveGiftCatalog / liveStreamGifts (precio del servidor, respaldado por el libro contable de monedas)

Solicitudes para hablar (sumar a un espectador)

Un espectador puede levantar la mano para co-presentar; el transmisor aprueba y el espectador aprobado recibe un token de publicación de LiveKit.

OperaciónPropósito
requestLiveSpeaker(liveStreamId)El espectador pide hablar — fija speakerStatus en requested
liveStreamSpeakerRequests(liveStreamId, limit, offset)El anfitrión lista las manos levantadas pendientes (requested)
myLiveSpeakerStatus(liveStreamId)El propio estado de orador de quien llama en esta transmisión (none / requested / approved)
respondToLiveSpeakerRequest(liveStreamId, userId, approve)El anfitrión aprueba/rechaza; la aprobación fija speakerStatus en approved, mejorando el próximo token de publicación del espectador

Esta es una implementación separada, exclusiva de transmisiones en vivo, en live-stream.manager.js — no comparte código con el flujo de solicitud para hablar de las llamadas grupales (Call) (pendingSpeakerRequests/mySpeakerRequests/cancelSpeakerRequest en call.resolver.js/call.type.js). A diferencia de ese flujo, aquí no existe un equivalente a cancelSpeakerRequest — un espectador no puede retirar una solicitud de transmisión en vivo pendiente una vez enviada.

Ayudantes de espectador y sesión

  • liveStreamViewers(liveStreamId, limit, offset) — el listado de espectadores actuales/pasados.
  • liveStreamerToken(liveStreamId) — token de publicación de LiveKit para el anfitrión; guestLiveViewerToken(liveStreamId) — token de suscripción para un espectador (incl. invitados sin sesión iniciada donde esté permitido).
  • saveLiveRecording(liveStreamId, recordingUrl) — adjunta una URL de grabación a una transmisión terminada (alimenta el Archivo de En Vivo).

Suscripciones en tiempo real

Las interfaces de En Vivo se mantienen al día mediante suscripciones graphql-ws en lugar de polling:

SuscripciónSe dispara cuando
liveStreamCommentAdded(liveStreamId)Se publica un nuevo comentario en vivo
liveStreamReactionsUpdated(liveStreamId)Cambian los totales de reacciones
liveStreamUpdated(liveStreamId)Cambia el estado de la transmisión (conteo de espectadores, status, …)
liveStreamsChangedCambia el conjunto de transmisiones actualmente en vivo (para el carrusel de descubrimiento /live)
liveSpeakerRequestsChanged(liveStreamId)Se levanta o retira una solicitud para hablar (interfaz del anfitrión)
liveSpeakerStatusChanged(liveStreamId)Cambia el propio estado de orador de quien llama (aprobado/rechazado)

Privacidad

Tanto Historias como En Vivo respetan el ajuste de cuenta privada. Si tu cuenta es privada, solo los seguidores aprobados pueden ver tu contenido, sin importar los ajustes individuales de cada historia.

Modelo de datos de transmisión en vivo

CampoDescripción
title / descriptionMetadatos de la transmisión
streamKeyClave única para ingesta RTMP
streamUrl / playbackUrlURLs de origen y de reproducción HLS
thumbnailUrlMiniatura de vista previa
statusscheduled, live, ended, cancelled
visibilitypublic, private, followers_only, close_friends
scheduledStartTime / actualStartTime / endTimeTiempos
durationSecondsDuración total de la transmisión
viewerCount / peakViewerCount / totalViewsEstadísticas de audiencia
likesCount / commentsCount / sharesCountEstadísticas de interacción
isRecorded / recordingUrlGrabación VOD
allowComments / allowSharingControles de la transmisión
isMonetizedBandera en el modelo/esquema; no está conectada a la funcionalidad de regalos con monedas (sendLiveGift funciona sin importar esta bandera)

API de GraphQL

Definido en graphql/types/live-stream.type.js, resuelto en graphql/resolvers/live-stream.resolver.js.

createLiveStream aprovisiona una sala de LiveKit y devuelve un streamKey/playbackUrl, sin salir al aire todavía. Llama a startLiveStream cuando el anfitrión esté listo — este es el momento en que los espectadores pueden unirse. updateLiveStream edita metadatos (título/descripción/miniatura/visibilidad/allowComments/allowSharing) de una transmisión que no ha terminado.

endLiveStream termina la transmisión y devuelve la durationSeconds final. cancelLiveStream elimina una transmisión aún programada antes de que comience. deleteLiveStream elimina un registro de transmisión por completo. promoteLiveStream (solo dueño) crea — o reutiliza, si ya se creó — un Post de anuncio complementario que enlaza de vuelta a /live/{id}, ya que un LiveStream no es un Post en sí mismo y no se puede pasar directamente por el flujo normal de promoción de posts/anuncios; el Post devuelto luego se puede promocionar como cualquier otro post. Conectado a un botón "Promocionar" en LiveRoomPage.tsx.

joinLiveStream registra a quien llama como espectador y devuelve su información de conexión a LiveKit (token, url, roomName) en una sola llamada — el resolver combina internamente liveStreamManager.joinLiveStream y .getViewerToken. leaveLiveStream desconecta al espectador. banLiveViewer / unbanLiveViewer (solo dueño) moderan a la audiencia. liveStreamerToken es el equivalente solo para el dueño, usado para previsualizar/publicar cámara+micrófono.

sendLiveComment publica un comentario en tiempo real. pinLiveComment / unpinLiveComment (solo dueño) destacan/quitan el destaque de un comentario en la parte superior del feed. deleteLiveComment elimina un comentario (el propio autor del comentario, o el anfitrión de la transmisión — verificado en live-stream.manager.js#deleteLiveComment; no hay aquí ningún acceso especial separado para administradores del sitio). reactToLiveStream fija la reacción con emoji de quien llama (like/love/wow/haha/sad/angry — llamarla de nuevo con un tipo distinto simplemente la cambia); removeLiveStreamReaction la elimina; liveStreamReactionCounts devuelve totales en vivo por tipo.

currentlyLive devuelve las transmisiones en vivo en este momento, ordenadas por viewerCount. trendingLiveStreams aplica un ranking ponderado por actualidad sobre una ventana timeframe (horas, 24 por defecto). scheduledLiveStreams muestra las próximas transmisiones. userLiveStreams devuelve el historial de transmisiones de un usuario. liveStreamStats devuelve métricas agregadas + estadísticas de espectadores + conteos de reacciones de una transmisión.

# Crear y programar una transmisión — aprovisiona la sala de LiveKit, no sale al aire todavía
mutation CreateLiveStream($input: LiveStreamCreateInput!) {
createLiveStream(input: $input) { id title playbackUrl status scheduledStartTime }
}

# Salir al aire — los espectadores pueden unirse a partir de aquí
mutation StartLiveStream($id: ID!) { startLiveStream(id: $id) { id status actualStartTime } }

# Terminar la transmisión — registra la duración final
mutation EndLiveStream($id: ID!) { endLiveStream(id: $id) { id status durationSeconds } }

mutation CancelLiveStream($id: ID!) { cancelLiveStream(id: $id) { id status } }
mutation DeleteLiveStream($id: ID!) { deleteLiveStream(id: $id) }

# El espectador se une — devuelve todo lo que livekit-client necesita para conectarse
mutation JoinLiveStream($id: ID!) {
joinLiveStream(id: $id) { token url roomName }
}
mutation LeaveLiveStream($id: ID!) { leaveLiveStream(id: $id) }

mutation BanLiveViewer($liveStreamId: ID!, $userId: ID!) { banLiveViewer(liveStreamId: $liveStreamId, userId: $userId) }
mutation UnbanLiveViewer($liveStreamId: ID!, $userId: ID!) { unbanLiveViewer(liveStreamId: $liveStreamId, userId: $userId) }

mutation SendLiveComment($liveStreamId: ID!, $commentText: String!) {
sendLiveComment(liveStreamId: $liveStreamId, commentText: $commentText) { id commentText createdAt user { username } }
}
mutation PinLiveComment($commentId: ID!) { pinLiveComment(commentId: $commentId) { id isPinned } }
mutation UnpinLiveComment($commentId: ID!) { unpinLiveComment(commentId: $commentId) { id isPinned } }
mutation DeleteLiveComment($commentId: ID!) { deleteLiveComment(commentId: $commentId) }

# interactionType: like | love | wow | haha | sad | angry
mutation ReactToLiveStream($liveStreamId: ID!, $interactionType: String!) {
reactToLiveStream(liveStreamId: $liveStreamId, interactionType: $interactionType) { id interactionType }
}
mutation RemoveLiveStreamReaction($liveStreamId: ID!) { removeLiveStreamReaction(liveStreamId: $liveStreamId) }

# Descubrimiento — transmitiendo en este momento, ordenado por conteo de espectadores
query CurrentlyLive($limit: Int) { currentlyLive(limit: $limit) { id title viewerCount thumbnailUrl user { username profilePicture isVerified } } }

# Tendencia — ranking ponderado por actualidad
query TrendingLiveStreams($limit: Int) { trendingLiveStreams(limit: $limit) { id title viewerCount likesCount } }

# Próximas transmisiones programadas
query ScheduledLiveStreams($limit: Int) { scheduledLiveStreams(limit: $limit) { id title scheduledStartTime user { username } } }

# El historial de transmisiones de un usuario
query UserLiveStreams($userId: ID!) { userLiveStreams(userId: $userId) { id title status durationSeconds totalViews } }

# Métricas en vivo de una transmisión activa
query LiveStreamStats($id: ID!) {
liveStreamStats(liveStreamId: $id) {
viewerCount peakViewerCount totalViews likesCount commentsCount
viewerStats { currentViewers totalViewers avgWatchTimeSeconds }
reactionCounts { like love wow haha sad angry total }
}
}

# Solo dueño — token de publicación para previsualizar cámara / salir al aire
query LiveStreamerToken($id: ID!) { liveStreamerToken(liveStreamId: $id) { token url roomName } }

Archivo de En Vivo

query LiveArchive($limit: Int, $offset: Int) {
liveArchive(limit: $limit, offset: $offset) {
id
title
thumbnailUrl
status
durationSeconds
viewerCount
peakViewerCount
totalViews
likesCount
commentsCount
isRecorded
recordingUrl
createdAt
}
}

liveArchive está acotado del lado del servidor a las propias transmisiones con status: 'ended' de quien llama — no tiene ningún argumento userId, a diferencia de userLiveStreams en el API de En Vivo más amplio de arriba. Respalda la pestaña "Live" en Configuración → Archivo, junto a las pestañas de archivo de Posts/Historias/Instants ya existentes.

Infraestructura

Las transmisiones en vivo funcionan con LiveKit (SFU WebRTC). Cuando se crea una transmisión, el backend aprovisiona una sala de LiveKit (hasta 10,000 participantes). Anfitriones y espectadores reciben tokens de acceso JWT firmados que el frontend usa para conectarse directamente a LiveKit.

Modelos de datos del backend

La actividad de transmisiones en vivo se rastrea a través de varios servicios de acceso a datos:

ServicioDescripción
live-stream.access-service.jsSesiones de transmisión en vivo
live-stream-viewer.access-service.jsRastreo de espectadores
live-stream-comment.access-service.jsComentarios en tiempo real
live-stream-interaction.access-service.jsReacciones con emoji
live-stream-gift.access-service.jsRegistros de regalos con monedas (tabla live_stream_gift)