/** * Deterministic livestream mock: create/manage lives, live chat * read/write/moderation, playback sources, and a scripted state transition * hook for Auto Director tests. */ import type { ConnectorCapabilities, ConnectorConnection, LiveChatMessage, LiveStream, NormalizedIdentity, Page, StreamPlaybackSource, } from "@tactical-media-broadcast-hub/core"; import { assertValidManifest, pageOf, type ConnectorManifest, type PlatformConnector, } from "@tactical-media-broadcast-hub/connector-sdk"; const CAPABILITIES: ConnectorCapabilities = { livestreams: true, createLivestream: true, manageLivestream: true, livePlaybackSource: true, liveChatRead: true, liveChatWrite: true, liveChatModeration: true, livestreamAnalytics: true, }; let seq = 0; const nextId = (prefix: string): string => `${prefix}-${(seq += 1)}`; interface MockLive extends LiveStream { chat: LiveChatMessage[]; } export interface MockLivestreamState { lives: Map; } export function createMockLivestreamState(): MockLivestreamState { return { lives: new Map() }; } export function createMockLivestreamConnector( state: MockLivestreamState = createMockLivestreamState(), ): PlatformConnector { const manifest: ConnectorManifest = { id: "mock-livestream", displayName: "Mock Livestream", capabilities: CAPABILITIES, connectionFields: [{ name: "apiToken", label: "API token", required: true, secret: true }], }; return { manifest: () => { assertValidManifest(manifest); return manifest; }, async authorize() { throw new Error("mock-livestream uses token auth in tests only"); }, async getIdentity(connection: ConnectorConnection): Promise { return { provider: manifest.id, remoteId: connection.channelId, handle: "mock@live", displayName: "Mock Live Channel", }; }, async createLiveStream( _connection: ConnectorConnection, request: { channelId: string; title: string; permanent: boolean; saveReplay: boolean }, ): Promise { const remoteId = nextId("live"); const live: MockLive = { id: remoteId, provider: manifest.id, connectionId: "pending", remoteId, title: request.title, state: "created", streamKeyRef: { kind: "credential-ref", id: nextId("keyref") }, rtmpUrl: "rtmp://mock.local/live", playbackUrls: [`https://mock.local/hls/${remoteId}/master.m3u8`], chat: [], }; state.lives.set(remoteId, live); return live; }, async updateLiveStream( _connection: ConnectorConnection, request: { remoteId: string; title?: string }, ): Promise { const live = state.lives.get(request.remoteId); if (live === undefined) throw new Error(`no live ${request.remoteId}`); if (request.title !== undefined) live.title = request.title; return live; }, async getLiveStreams( _connection: ConnectorConnection, request: { cursor?: string; limit: number }, ): Promise> { const all = [...state.lives.values()]; const start = request.cursor ? Number(request.cursor) : 0; const items = all.slice(start, start + request.limit); const next = start + request.limit; return next < all.length ? pageOf(items, String(next)) : pageOf(items); }, async getStreamPlaybackSources( _connection: ConnectorConnection, request: { remoteId: string }, ): Promise { const live = state.lives.get(request.remoteId); if (live === undefined) return []; return [ { remoteId: live.remoteId, url: `https://mock.local/hls/${live.remoteId}/master.m3u8`, kind: "hls", protocol: "https", }, ]; }, async getLiveChat( _connection: ConnectorConnection, request: { liveRemoteId: string; cursor?: string; limit: number }, ): Promise> { const live = state.lives.get(request.liveRemoteId); if (live === undefined) return pageOf([]); const start = request.cursor ? Number(request.cursor) : 0; const items = live.chat.slice(start, start + request.limit); const next = start + request.limit; return next < live.chat.length ? pageOf(items, String(next)) : pageOf(items); }, async sendLiveChatMessage( _connection: ConnectorConnection, request: { liveRemoteId: string; text: string }, ): Promise { const live = state.lives.get(request.liveRemoteId); if (live === undefined) throw new Error(`no live ${request.liveRemoteId}`); const message: LiveChatMessage = { id: nextId("msg"), provider: manifest.id, liveStreamId: live.remoteId, authorHandle: "mock@live", text: request.text, sentAt: new Date(), isModerated: false, }; live.chat.push(message); return message; }, async moderateLiveChat( _connection: ConnectorConnection, request: { liveRemoteId: string; messageRemoteId: string; action: "delete" | "timeout" | "ban" }, ): Promise { const live = state.lives.get(request.liveRemoteId); if (live === undefined) return; for (const message of live.chat) { if (message.id === request.messageRemoteId && request.action === "delete") { message.isModerated = true; } } }, }; } /** Test hook: scripted state transitions for Auto Director fixtures. */ export function transitionMockLive( state: MockLivestreamState, remoteId: string, to: LiveStream["state"], ): void { const live = state.lives.get(remoteId); if (live === undefined) throw new Error(`no live ${remoteId}`); live.state = to; }