basic functionality...

This commit is contained in:
2025-08-17 10:36:57 -04:00
parent 1c032da3df
commit 6a40cfa568
13 changed files with 227 additions and 106 deletions

View File

@@ -1,34 +1,34 @@
import { Channels, Streams, type ApplicationCredentials, PCastHttpRequests} from "./pcast";
import {Channels, Streams, type ApplicationCredentials, PCastHttpRequests} from './pcast';
export class PCastApi {
private readonly _pcastUri: string;
private readonly _applicationCredentials: ApplicationCredentials;
private readonly _pcastHttpRequests: PCastHttpRequests;
private readonly _pcastUri: string;
private readonly _applicationCredentials: ApplicationCredentials;
private readonly _pcastHttpRequests: PCastHttpRequests;
constructor(pcastUri: string, applicationCredentials: ApplicationCredentials) {
if (pcastUri.split('/').at(-1) !== 'pcast') {
this._pcastUri = `${pcastUri}/pcast`;
} else {
this._pcastUri = pcastUri;
}
this._applicationCredentials = applicationCredentials;
this._pcastHttpRequests = new PCastHttpRequests(this._pcastUri, this._applicationCredentials);
constructor(pcastUri: string, applicationCredentials: ApplicationCredentials) {
if (pcastUri.split('/').at(-1) !== 'pcast') {
this._pcastUri = `${pcastUri}/pcast`;
} else {
this._pcastUri = pcastUri;
}
get pcastUri(): string {
return this._pcastUri;
}
this._applicationCredentials = applicationCredentials;
this._pcastHttpRequests = new PCastHttpRequests(this._pcastUri, this._applicationCredentials);
}
get applicationCredentials(): ApplicationCredentials {
return this._applicationCredentials;
}
get pcastUri(): string {
return this._pcastUri;
}
get channels(): Channels {
return new Channels(this._pcastHttpRequests);
}
get applicationCredentials(): ApplicationCredentials {
return this._applicationCredentials;
}
get streams(): Streams {
return new Streams(this._pcastHttpRequests);
}
}
get channels(): Channels {
return new Channels(this._pcastHttpRequests);
}
get streams(): Streams {
return new Streams(this._pcastHttpRequests);
}
}

View File

@@ -1,5 +1,5 @@
import {PCastApi} from "./PCastApi";
import {PCastApi} from './PCastApi';
export * from './pcast';
export {PCastApi};
export default {PCastApi};
export default {PCastApi};

View File

@@ -5,8 +5,8 @@ const httpMethodsThatMustNotHaveBody = [HttpMethod.GET]; // Head, Options
export class HttpRequestError extends Error {
constructor(
message: string,
public readonly status?: number,
message: string,
public readonly status?: number,
public readonly statusText?: string,
public readonly originalError?: unknown
) {
@@ -51,11 +51,7 @@ export class HttpRequests {
const response = await fetch(requestPath, options);
if (!response.ok) {
throw new HttpRequestError(
`HTTP error! status [${response.status}] ${response.statusText}`,
response.status,
response.statusText
);
throw new HttpRequestError(`HTTP error! status [${response.status}] ${response.statusText}`, response.status, response.statusText);
}
const responseContentType = response.headers.get('content-type');
@@ -69,14 +65,14 @@ export class HttpRequests {
if (e instanceof HttpRequestError) {
throw e;
}
if (e instanceof Error) {
if (e.name === 'AbortError') {
throw new HttpRequestError(`Request timeout after ${timeoutDuration}ms`, undefined, undefined, e);
}
throw new HttpRequestError(`Request failed: ${e.message}`, undefined, undefined, e);
}
throw new HttpRequestError(`Unknown request error: ${String(e)}`, undefined, undefined, e);
} finally {
globalThis.clearTimeout(requestTimeoutId);

View File

@@ -21,7 +21,10 @@ type GetChannelParams = {
};
export class ChannelError extends Error {
constructor(message: string, public readonly code: string) {
constructor(
message: string,
public readonly code: string
) {
super(message);
this.name = 'ChannelError';
}
@@ -50,7 +53,7 @@ export class Channels {
for (let channel of channelsList) {
this._channelsByAlias.set(channel.alias, channel);
}
this._initialized = true;
console.log('[Channels] [initialize] Cache populated successfully');
} catch (error) {
@@ -70,14 +73,14 @@ export class Channels {
throw new ChannelError('Channel options must be an array', 'INVALID_OPTIONS');
}
const createChannelRequestBody = {
channel: {
name: name.trim(),
alias: name.trim(),
description: description.trim(),
options: channelOptions
}
};
const createChannelRequestBody = {
channel: {
name: name.trim(),
alias: name.trim(),
description: description.trim(),
options: channelOptions
}
};
const response = await this._httpRequests.request<ChannelResponse>(HttpMethod.PUT, '/channel', {
body: JSON.stringify(createChannelRequestBody)
@@ -117,7 +120,7 @@ export class Channels {
}
const response = await this._httpRequests.request<ChannelResponse>(HttpMethod.GET, `/channel/${encodeURIComponent(alias)}`);
if (!response.channel) {
throw new ChannelError(`Invalid response format for channel: ${alias}`, 'INVALID_RESPONSE');
}
@@ -129,7 +132,7 @@ export class Channels {
if (channelId) {
const response = await this._httpRequests.request<ChannelResponse>(HttpMethod.GET, `/channel/${encodeURIComponent(channelId)}`);
if (!response.channel) {
throw new ChannelError(`Invalid response format for channel ID: ${channelId}`, 'INVALID_RESPONSE');
}
@@ -144,7 +147,7 @@ export class Channels {
}
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');
}
@@ -158,7 +161,7 @@ export class Channels {
}
const response = await this._httpRequests.request<ChannelResponse>(HttpMethod.DELETE, `/channel/${encodeURIComponent(channelId)}`);
if (!response.channel) {
throw new ChannelError(`Invalid response format for deleted channel: ${channelId}`, 'INVALID_RESPONSE');
}

View File

@@ -1,4 +1,4 @@
import type { Channel } from './Channels';
import type {Channel} from './Channels';
export default interface IResponse<Key extends string, T> {
status: string;

View File

@@ -1,26 +1,25 @@
import { HttpRequests} from "../net/http/HttpRequests";
import {HttpRequests} from '../net/http/HttpRequests';
export type ApplicationCredentials = {
id: string;
secret: string;
}
id: string;
secret: string;
};
export class PCastHttpRequests extends HttpRequests {
private readonly _tenancy: string;
constructor(baseUri: string, applicationCredentials: ApplicationCredentials, options: {requestTimeoutDuration?: number} = {}) {
const baseHeaders = new Headers({
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Basic ${btoa(`${applicationCredentials.id}:${applicationCredentials.secret}`)}`
});
super(baseUri, baseHeaders, options);
private readonly _tenancy: string;
this._tenancy = applicationCredentials.id;
}
constructor(baseUri: string, applicationCredentials: ApplicationCredentials, options: {requestTimeoutDuration?: number} = {}) {
const baseHeaders = new Headers({
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Basic ${btoa(`${applicationCredentials.id}:${applicationCredentials.secret}`)}`
});
super(baseUri, baseHeaders, options);
get tenancy(): string {
return this._tenancy;
}
}
this._tenancy = applicationCredentials.id;
}
get tenancy(): string {
return this._tenancy;
}
}

132
src/pcast/Reporting.ts Normal file
View File

@@ -0,0 +1,132 @@
import assertUnreachable from '../lang/assertUnreachable';
import {HttpMethod} from '../net/http/HttpMethod';
import type IResponse from './IResponse';
import type { PCastHttpRequests } from './PCastRequests';
export enum ReportKind {
Publishing = 0,
Viewing = 1
}
export type ReportKindType = 'Publishing' | 'Viewing';
export class ReportKindMapping {
public static convertReportKindTypeToReportKind(reportKindType: ReportKindType): ReportKind {
switch (reportKindType) {
case 'Publishing':
return ReportKind.Publishing;
case 'Viewing':
return ReportKind.Viewing;
default:
assertUnreachable(reportKindType);
}
}
public static convertReportKindToReportKindType(reportKind: ReportKind): ReportKindType {
switch (reportKind) {
case ReportKind.Publishing:
return 'Publishing';
case ReportKind.Viewing:
return 'Viewing';
default:
assertUnreachable(reportKind);
}
}
}
export type PublishingReportOptions = {
applicationIds?: string[];
streamIds?: string[];
channelIds?: string[];
channelAliases?: string[];
roomIds?: string[];
roomAliases?:string[];
tags?: string[];
start: string;
end: string;
};
export enum ViewingReportKind {
RealTime = 0,
HLS = 1,
DASH = 2
}
export type ViewingReportKindType = 'RealTime' | 'HLS' | 'DASH';
export type ViewingReportOptions = {
kind: ViewingReportKind,
applicationIds?: string[];
streamIds?: string[];
sessionIds?: string[];
originStreamIds?: string[];
originTags?: string[];
channelIds?: string[];
channelAliases?: string[];
roomIds?: string[];
roomAliases?:string[];
tags?: string[];
start: string;
end: string;
};
export class Reporting {
private readonly _httpRequests: PCastHttpRequests;
constructor(httpRequests: PCastHttpRequests) {
this._httpRequests = httpRequests;
}
public async generateReport<ReportOptions extends PublishingReportOptions | ViewingReportOptions>(kind: ReportKind, options: ReportOptions): Promise<string> {
console.log('[Reporting] generateReport [%o]', ReportKindMapping.convertReportKindToReportKindType(kind));
if (kind === ReportKind.Publishing) {
return this.requestPublishingReport(options as PublishingReportOptions);
}
if (kind === ReportKind.Viewing) {
return this.requestViewingReport(options as ViewingReportOptions);
}
throw new Error(`[Reporting] Unsupported report kind: ${kind}`);
}
public async requestPublishingReport(options: PublishingReportOptions): Promise<string> {
if (!(options.start || options.end)) {
throw new Error('[Reporting] [requestPublishingReport] requires a start and end Date');
}
const publishingReportOptions = {
...options
};
const requestPublishingOptions = {
body: JSON.stringify({publishingReport: publishingReportOptions})
};
const response = await this._httpRequests.request<IResponse<'publishingReport', string>>(HttpMethod.PUT, '/pcast/reporting/publishing', requestPublishingOptions);
return response;
}
private async requestViewingReport(options: ViewingReportOptions): Promise<string> {
const viewingReportOptions = {
...options
};
const requestViewingOptions = {
body: JSON.stringify({viewingReport: viewingReportOptions})
};
const response = await this._httpRequests.request<IResponse<'viewingReport', string>>(HttpMethod.PUT, '/pcast/reporting/viewing', requestViewingOptions);
return response;
}
}

View File

@@ -12,7 +12,7 @@ export class Streams {
public async publishUri(mediaUri: string, token: string) {
const mediaType = mediaUri.split('.')?[mediaUri.at(-1)];
const mediaType = mediaUri.split('.')?.at(-1);
if (!mediaType) {
throw new Error('Invalid media URI no media type found');

View File

@@ -1,3 +1,3 @@
export * from './Channels';
export * from './Stream';
export * from './PCastRequests';
export * from './PCastRequests';