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, ycreateLivePromotionPost). 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 elis_banned: truedebanViewernunca se guardaban realmente,updatePeakViewerCountera código muerto (comparación contraundefined), la duración deendLiveStreamsiempre dabaNaN, y la propia llamada al validador dejoinLiveStreamtení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étodosassociate()vacíos a pesar de que cada método de los access-services hacía eager-load de{model: User, as: 'user'}— Sequelize lanzaEagerLoadingErrorpara un alias deincludesin 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 engetLiveStream/joinLiveStreampara transmisionesprivate/followers_only/close_friends) antes de construir encima la capa de GraphQL de abajo.apps/backend/graphql/types/live-stream.type.jsyresolvers/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.jsyresolvers/live-stream-archive.resolver.js— un passthrough delgado y de solo lectura que exponeliveArchive(limit, offset): las propias transmisiones terminadas del usuario, respaldado porlive-stream.manager.js#getUserLiveStreamsacotado astatus: '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 arribaapps/backend/managers/call-managers/speaker-request.manager.jsydata-access-services/call/speaker-request.access-service.js— Corregido — 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 aCalls grupales. Ambas cosas siguen siendo ciertas para este par de archivos (respaldanpendingSpeakerRequests/mySpeakerRequests/cancelSpeakerRequestencall.resolver.js/call.type.js, solo paraCalls) — 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/getMySpeakerStatusconstruidas directamente enlive-stream.manager.js, controladas mediante una columnaspeakerStatusenLiveStreamViewer. Ver la sección Solicitudes para hablar más abajo.apps/backend/services/livekit.service.jsylivekit-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 stubtale.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 hermanosblast/articlebajodata-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 modeloTaleal cual enrutar. Las Historias realmente viven como filas dePostcontype: 'story'(en paralelo a como los Clips sontype: 'clip'), reutilizando la columnaexpires_atde la tabla Post en lugar de una tabla de historias dedicada. Un comentario de docblock desactualizado sobrePost.typeenpost.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 mutationcreateStorya 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 querieshomeStories,userStories,hasActiveStory,storyViewers,storyViewerCounty la mutationcreateStory(input: StoryCreateInput!);StoryCreateInput.audienceelige elPostVisibilityde 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íapostManagerapps/backend/managers/post-managers/post.manager.js—createStory(fijaexpiresAt = now + STORY_DURATION_MS,visibility: validated.audience || 'followers'),getHomeStories,getUserStories,hasActiveStory.PostManagersigue sin tener métodosgetStoryViewers/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 resolversstoryViewers/storyViewerCountdepost.resolver.jsya no llaman apostManageren absoluto. Ahora delegan directamente awatch-history.manager.js#getPostViewers/#getPostViewersCount— la misma implementación real, solo para el dueño, que ya respaldapostViewers/postViewersCountenwatch-history.type.js/watch-history.resolver.js. Esta página antes decía questoryViewers/storyViewerCountlanzabanTypeError: ... is not a functionen tiempo de ejecución por llamar a métodos inexistentes dePostManager— eso ya no es cierto.apps/backend/validators/post.validator.js—validateStoryInputvalidaaudiencecontra la misma lista blanca devalidateVisibility(que ahora incluyeclose_friends), confollowerscomo 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
audiencede vuelta:getPostno tenía ninguna ramaclose_friends, ygetHomeStories/getUserStoriesno aplicaban ningún filtro de visibilidad. Eso ya está corregido.getPost(~línea 643) ganó una ramaclose_friendsjunto a sus verificaciones existentes deprivate/followers/subscribers, controlada poruserCloseFriendAccessService.isCloseFriend(post.userId, viewerId). Los caminos de listado de historias también quedaron controlados:getHomeStoriesfiltra las historiasclose_friendsde 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), ygetUserStorieshace pasar a cada espectador que no es el dueño por un switchpublic/followers/close_friends(isFollowing/isCloseFriend), cerrando por defecto para cualquier otro valor.StoryCreateInput.audienceahora se aplica de verdad en la lectura, no solo se captura y se guarda. (close-friends.manager.js#isCloseFriendya se usaba de verdad para transmisiones en vivo de amigos cercanos enlive-stream.manager.js— ahora también se usa para historias.)
Frontend
apps/frontend-nextjs/src/page-components/settings/StoryLivePage.tsx(en la rutaapps/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,saveStoryToArchiveyshareLocationse guardan todos medianteprivacySettings/updatePrivacySettings(los mismos tiposPrivacySettings/PrivacySettingsInputque usaAccountPrivacyPage.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 rutaapps/frontend-nextjs/src/app/settings/archive) — tiene una pestaña "Live" que consultaliveArchivey 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 enfrontend-nextjs.apps/frontend-nextjs/src/components/stories/StoryRail.tsx— el carrusel horizontal de avatares de historias, renderizado enHomePage.tsx. ConsultahomeStories, agrupa la lista plana de posts en un avatar por usuario, y abreCreateStoryModal(para tu propio anillo, si no tienes una historia activa) oStoryViewer(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 aincrementPostViews(contador agregado) yrecordPostView(registro por espectador, omitido en tu propia historia) una vez por historia, una barra de reacción rápida de 5 emojis que llama alikePost, una caja de comentarios que llama acreateComment, y (solo el dueño, mediante un ícono de ojo) una hoja inferior "quién vio mi historia" respaldada porstoryViewersapps/frontend-nextjs/src/components/stories/CreateStoryModal.tsx— el compositor; llama acreateStorycon un selector deaudience(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/uploaddel backend) — esta página antes decía que el video estaba bloqueado; la lista blanca del backend ya incluía mimetypesvideo/*(verapi/server.js), el selector de archivos de este modal era la única restricción restante — corregidohasActiveStoryestá conectado al anillo de la foto de perfil tanto enProfilePage.tsxcomo enPublicProfilePage.tsxapps/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) abreGoLiveModalapps/frontend-nextjs/src/components/live/GoLiveModal.tsx— el formulario de creación de transmisión (título, descripción, visibilidad, permitir comentarios); llama acreateLiveStreamy navega a/live/{id}al terminar, donde el dueño inicia la transmisión realapps/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:startLiveStream→liveStreamerToken→ se conecta a LiveKit como publicador (cámara + micrófono, con botones para alternar) usandoRoom/createLocalTracksdelivekit-client;endLiveStreamal detener. Espectador:joinLiveStream(devuelve unLiveKitConnectionInfo: token + url + roomName en una sola llamada) → se conecta como suscriptor, adjuntando las pistas de video/audio suscritas;leaveLiveStreamal 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íaliveStreamReactionCounts). Aplica lavisibilityde 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-clientaapps/frontend-nextjs/package.jsonpero no pudo instalarse en el entorno donde se construyó esto (sin acceso a la red hacia el registro de npm desde ese sandbox) — ejecutanpm installenapps/frontend-nextjsantes de compilar/ejecutar; hasta entonces los imports delivekit-clientfallará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 enProfilePage.tsxyPublicProfilePage.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.audiencese 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 ramaclose_friendsdegetPost, más el filtrado de visibilidad engetHomeStories/getUserStories) — ver la corrección arriba - Historias en video — el selector de archivos de
CreateStoryModal.tsxahora aceptavideo/*, igualando la lista blanca del backend que ya existía - "Quién vio mi historia" — corregido en esta sesión:
storyViewers/storyViewerCountahora delegan directamente awatch-history.manager.js#getPostViewers/#getPostViewersCount(solo para el dueño), la misma implementación real que ya respaldapostViewers/postViewersCount— ver la corrección arriba. La hoja "quién vio mi historia" deStoryViewer.tsxya 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éricolikePost/InteractionTypeque 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 unLiveKitConnectionInfo(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 enLiveRoomPage.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 deLiveRoomPage.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 deLiveRoomPage.tsx, con conteos en vivo víaliveStreamReactionCounts -
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 enLiveRoomPage.tsx; las monedas se mueven de espectador → transmisor a través del libro contable de monedas (sendTip), respaldado por la tablalive_stream_gift - Solicitudes para hablar en transmisiones en vivo —
requestLiveSpeaker/liveStreamSpeakerRequests/myLiveSpeakerStatus/respondToLiveSpeakerRequest, conectado de punta a punta:live-stream.resolver.js(implementado directamente enlive-stream.manager.js, independiente delspeaker-request.manager.jsque es solo paraCall) +LiveSpeakerControls.tsx. La aprobación cambiaLiveStreamViewer.speakerStatusaapproved; 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 aprivacySettings/updatePrivacySettings; "Ocultar historia de" está conectado por separado víastoryHiddenUsers/updateStoryHiddenUsers -
promoteLiveStream— conectado de punta a punta:live-stream.resolver.js+ un botón "Promocionar" enLiveRoomPage.tsx; crea o reutiliza unPostde 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 sonPosts en sí mismas) -
liveArchive— conectado de punta a punta:live-stream-archive.resolver.js+ la pestaña Live deArchivePage.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.tsx→createLiveStream, luegostartLiveStreamdesde 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
| Ajuste | Opciones |
|---|---|
| Quién puede ver/unirse a tu transmisión en vivo | Pública / Privada / Solo seguidores / Amigos cercanos — LiveStreamVisibility, aplicado del lado del servidor tanto en getLiveStream como en joinLiveStream |
| Comentarios en vivo | allowComments — habilitado/deshabilitado por transmisión |
| Compartir | allowSharing — habilitado/deshabilitado por transmisión |
| Regalos con monedas | Implementado — 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ón | Propó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ón | Se 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, …) |
liveStreamsChanged | Cambia 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
| Campo | Descripción |
|---|---|
title / description | Metadatos de la transmisión |
streamKey | Clave única para ingesta RTMP |
streamUrl / playbackUrl | URLs de origen y de reproducción HLS |
thumbnailUrl | Miniatura de vista previa |
status | scheduled, live, ended, cancelled |
visibility | public, private, followers_only, close_friends |
scheduledStartTime / actualStartTime / endTime | Tiempos |
durationSeconds | Duración total de la transmisión |
viewerCount / peakViewerCount / totalViews | Estadísticas de audiencia |
likesCount / commentsCount / sharesCount | Estadísticas de interacción |
isRecorded / recordingUrl | Grabación VOD |
allowComments / allowSharing | Controles de la transmisión |
isMonetized | Bandera 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:
| Servicio | Descripción |
|---|---|
live-stream.access-service.js | Sesiones de transmisión en vivo |
live-stream-viewer.access-service.js | Rastreo de espectadores |
live-stream-comment.access-service.js | Comentarios en tiempo real |
live-stream-interaction.access-service.js | Reacciones con emoji |
live-stream-gift.access-service.js | Registros de regalos con monedas (tabla live_stream_gift) |