import {HttpMethod} from '../net/http/HttpMethod'; import type {PCastHttpRequests} from './PCastRequests'; import type {ChannelResponse, ChannelsResponse, MembersResponse} from './IResponse'; export type ChannelId = string; export type Channel = { options: string[]; alias: string; name: string; description: string; type: string; streamKey: string; created: string; lastUpdated: string; channelId: string; }; export type ChannelAlias = string; export type Member = { sessionId: string; screenName: string; role: string; streams: [ { type: string; uri: string; audioState: string; videoState: string; } ]; state: string; lastUpdate: number; }; type GetChannelParams = { alias?: string; channelId?: string; }; export class ChannelError extends Error { constructor( message: string, public readonly code: string ) { super(message); this.name = 'ChannelError'; } } export class Channels { private readonly _httpRequests: PCastHttpRequests; private readonly _channelsByAlias: Map = new Map(); constructor(pcastHttpRequests: PCastHttpRequests, skipInitialization = false) { this._httpRequests = pcastHttpRequests; if (!skipInitialization) { this.initialize(); } } public async create(name: string, description: string, channelOptions: string[] = []): Promise { if (!name || name.trim().length === 0) { throw new ChannelError('Channel name cannot be empty', 'INVALID_NAME'); } if (!description || description.trim().length === 0) { throw new ChannelError('Channel description cannot be empty', 'INVALID_DESCRIPTION'); } if (!Array.isArray(channelOptions)) { throw new ChannelError('Channel options must be an array', 'INVALID_OPTIONS'); } const createChannel = { channel: { name: name.trim(), alias: name.trim(), description: description.trim(), options: channelOptions } }; const route = '/channel'; const requestOptions = {body: JSON.stringify(createChannel)}; const response = await this._httpRequests.request(HttpMethod.PUT, route, requestOptions); if (!response.channel) { throw new ChannelError('Invalid response format - missing channel data', 'INVALID_RESPONSE'); } this._channelsByAlias.set(response.channel.alias, response.channel); return response.channel; } public async list(): Promise { const response = await this._httpRequests.request(HttpMethod.GET, '/channels'); if (!response.channels) { throw new ChannelError('Invalid response format - missing channels data', 'INVALID_RESPONSE'); } this._channelsByAlias.clear(); for (const channel of response.channels) { this._channelsByAlias.set(channel.alias, channel); } return response.channels; } public async refreshCache(): Promise { // eslint-disable-next-line @typescript-eslint/no-unused-vars const ignored = await this.list(); } public async getChannelInfoByAlias(alias: string): Promise { return this.get({alias}); } public async get({alias, channelId}: GetChannelParams): Promise { if (!alias && !channelId) { throw new ChannelError('Either alias or channelId must be provided', 'MISSING_PARAMETER'); } if (alias && this._channelsByAlias.has(alias)) { return this._channelsByAlias.get(alias); } const channelList = await this.list(); return alias ? channelList.find(channel => channel.alias === alias) : channelList.find(channel => channel.channelId === channelId); } public async getPublisherCount(channelId: string): Promise { const response = await this._httpRequests.request(HttpMethod.GET, `/channel/${encodeURIComponent(channelId)}/publishers/count`); return parseInt(response, 10); } public async getMembers(channelId: string): Promise { if (!channelId || channelId.trim().length === 0) { throw new ChannelError('Channel ID cannot be empty', 'INVALID_CHANNEL_ID'); } const response = await this._httpRequests.request(HttpMethod.GET, `/channel/${encodeURIComponent(channelId)}/members`); if (!response.members) { throw new ChannelError(`Invalid response format for channel members: ${channelId}`, 'INVALID_RESPONSE'); } return response.members; } public async getMembersByChannelAlias(alias: string): Promise { const channel = await this.get({alias}); if (!channel) { throw new ChannelError(`Channel not found: ${alias}`, 'CHANNEL_NOT_FOUND'); } return this.getMembers(channel.channelId); } public async delete({channelId, alias}: {channelId?: string; alias?: string}): Promise { if (!channelId && !alias) { throw new ChannelError('Deleting a channel requires either a channelId or alias', 'INVALID_ARGUMENTS'); } const channelIdToDelete = alias ? (await this.get({alias}))?.channelId : channelId; if (!channelIdToDelete) { throw new ChannelError('Unable to find room to delete', 'NOT_FOUND'); } const route = `/channel/${encodeURIComponent(channelIdToDelete)}`; const response = await this._httpRequests.request(HttpMethod.DELETE, route); if (!response.channel) { throw new ChannelError(`Invalid response format for deleted channel [${channelId}]`, 'INVALID_RESPONSE'); } const deletedChannel = response.channel; if (this._channelsByAlias.has(deletedChannel.alias)) { this._channelsByAlias.delete(deletedChannel.alias); } return deletedChannel; } async getPublishSourceStreamId(channelId: string, retryCount: number = 3): Promise { const retryCountRemaining = retryCount || 3; const channelMembers = await this.getMembers(channelId); if (channelMembers.length === 0) { if (retryCountRemaining > 0) { return this.getPublishSourceStreamId(channelId, retryCountRemaining - 1); } return null; } const presenter = channelMembers.find(member => member.role === 'Presenter'); if (!presenter) { if (retryCountRemaining > 0) { return this.getPublishSourceStreamId(channelId, retryCountRemaining - 1); } return null; } const publishSourceStreamIdRegExp = /pcast:\/\/.*\/([^?]*)/; return presenter.streams[0].uri.match(publishSourceStreamIdRegExp)?.[1] ?? null; } // TODO(AZ): Implement this // public async fork(channelId: string): Promise // TODO(AZ): Implement this // public async killChannel(channelId: string): Promise // TODO(AZ): Implement this // public async publishViaUrl(mediaUriToPublish: string, token: string): Promise private async initialize(): Promise { try { const channelsList = await this.list(); if (!channelsList) { console.warn('[Channels] Failed to initialize cache - no channels returned'); return; } for (const channel of channelsList) { this._channelsByAlias.set(channel.alias, channel); } } catch (error) { console.error('[Channels] Failed to initialize cache:', error); } } }