Files
ToolsPCastApi-TS/src/apis/Channels.ts

264 lines
7.9 KiB
TypeScript

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;
};
type KillChannelResponse = {
status: string;
killedMembers: Member[];
};
type ForkChannelResponse = {
status: string;
members: Member[];
};
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<ChannelAlias, Channel> = new Map();
constructor(pcastHttpRequests: PCastHttpRequests) {
this._httpRequests = pcastHttpRequests;
this.initialize();
}
public async create(name: string, description: string, channelOptions: string[] = []): Promise<Channel> {
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<ChannelResponse>(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<Channel[]> {
const response = await this._httpRequests.request<ChannelsResponse>(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<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ignored = await this.list();
}
public async getChannelInfoByAlias(alias: string): Promise<Channel | undefined> {
return this.get({alias});
}
public async get({alias, channelId}: GetChannelParams): Promise<Channel | undefined> {
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<number> {
const response = await this._httpRequests.request<string>(HttpMethod.GET, `/channel/${encodeURIComponent(channelId)}/publishers/count`);
return parseInt(response, 10);
}
public async getMembers(channelId: string): Promise<Member[]> {
if (!channelId || channelId.trim().length === 0) {
throw new ChannelError('Channel ID cannot be empty', 'INVALID_CHANNEL_ID');
}
const response = await this._httpRequests.request<MembersResponse>(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<Member[]> {
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<{status: string}> {
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<ChannelResponse>(HttpMethod.DELETE, route);
if (response.status !== 'ok') {
throw new ChannelError(`Failed to delete channel [${channelIdToDelete}]`, 'DELETE_FAILED');
}
if (alias && this._channelsByAlias.has(alias)) {
this._channelsByAlias.delete(alias);
}
return response;
}
public async getPublishSourceStreamId(channelId: string, retryCount: number = 3): Promise<string | null> {
const channelMembers = await this.getMembers(channelId);
console.log('channelMembers [%o] retryCount [%d]', channelMembers, retryCount);
if (channelMembers.length === 0) {
if (retryCount > 0) {
return this.getPublishSourceStreamId(channelId, retryCount - 1);
}
return null;
}
const presenter = channelMembers.find(member => member.role === 'Presenter');
if (!presenter) {
if (retryCount > 0) {
return this.getPublishSourceStreamId(channelId, retryCount - 1);
}
return null;
}
const publishSourceStreamIdRegExp = /pcast:\/\/.*\/([^?]*)/;
return presenter.streams[0]?.uri?.match(publishSourceStreamIdRegExp)?.[1] ?? null;
}
public async fork(sourceChannelId: string, destinationChannelId: string): Promise<ForkChannelResponse> {
const response = await this._httpRequests.request<ForkChannelResponse>(
HttpMethod.PUT,
`/channel/${encodeURIComponent(destinationChannelId)}/fork/${encodeURIComponent(sourceChannelId)}`
);
if (response.status !== 'ok') {
console.error('Fork response [%o]', response);
throw new ChannelError(`Failed to fork channel [${sourceChannelId}] to [${destinationChannelId}]`, 'FORK_FAILED');
}
return response;
}
public async killChannel(channelId: string): Promise<KillChannelResponse> {
const response = await this._httpRequests.request<KillChannelResponse>(HttpMethod.PUT, `/channel/${encodeURIComponent(channelId)}/kill`);
if (response.status !== 'ok') {
console.error('Kill response [%o]', response);
throw new ChannelError(`Failed to kill channel [${channelId}]`, 'KILL_FAILED');
}
return response;
}
private async initialize(): Promise<void> {
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);
}
}
}