T-HUB/packages/connector-mock-livestream/src/index.ts
glass beadian aa5e262f3a Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
2026-10-10 11:56:42 +00:00

190 lines
5.8 KiB
TypeScript

/**
* 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<string, MockLive>;
}
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<NormalizedIdentity> {
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<LiveStream> {
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<LiveStream> {
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<Page<LiveStream>> {
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<StreamPlaybackSource[]> {
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<Page<LiveChatMessage>> {
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<LiveChatMessage> {
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<void> {
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;
}