@portalshq/capability-queue-broadcast 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -0
- package/dist/client.d.ts +174 -0
- package/dist/client.js +312 -0
- package/dist/generated/api.d.ts +500 -0
- package/dist/generated/api.js +246 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -0
- package/dist/monitoring/metrics.d.ts +90 -0
- package/dist/monitoring/metrics.js +190 -0
- package/dist/streaming/frame-buffer.d.ts +34 -0
- package/dist/streaming/frame-buffer.js +61 -0
- package/dist/streaming/index.d.ts +2 -0
- package/dist/streaming/index.js +2 -0
- package/dist/streaming/rtmp/rtmp-streamer.d.ts +75 -0
- package/dist/streaming/rtmp/rtmp-streamer.js +299 -0
- package/package.json +34 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
export const BodyUploadQueueItemMediaType = {
|
|
2
|
+
video: 'video',
|
|
3
|
+
image: 'image',
|
|
4
|
+
audio: 'audio',
|
|
5
|
+
};
|
|
6
|
+
export const HealthResponseMediaServer = {
|
|
7
|
+
up: 'up',
|
|
8
|
+
down: 'down',
|
|
9
|
+
};
|
|
10
|
+
export const JobResponseMediaType = {
|
|
11
|
+
video: 'video',
|
|
12
|
+
image: 'image',
|
|
13
|
+
audio: 'audio',
|
|
14
|
+
};
|
|
15
|
+
export const JobResponseStatus = {
|
|
16
|
+
ingesting: 'ingesting',
|
|
17
|
+
staged: 'staged',
|
|
18
|
+
queued: 'queued',
|
|
19
|
+
generating: 'generating',
|
|
20
|
+
ready: 'ready',
|
|
21
|
+
playing: 'playing',
|
|
22
|
+
done: 'done',
|
|
23
|
+
failed: 'failed',
|
|
24
|
+
};
|
|
25
|
+
export const QueueItemJsonMediaType = {
|
|
26
|
+
video: 'video',
|
|
27
|
+
image: 'image',
|
|
28
|
+
audio: 'audio',
|
|
29
|
+
};
|
|
30
|
+
export const getGetHealthUrl = () => {
|
|
31
|
+
return `/health`;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* @summary Health
|
|
35
|
+
*/
|
|
36
|
+
export const getHealth = async (options) => {
|
|
37
|
+
const res = await fetch(getGetHealthUrl(), {
|
|
38
|
+
...options,
|
|
39
|
+
method: 'GET'
|
|
40
|
+
});
|
|
41
|
+
const body = [204, 205, 304].includes(res.status) ? null : await res.text();
|
|
42
|
+
const data = body ? JSON.parse(body) : {};
|
|
43
|
+
return { data, status: res.status, headers: res.headers };
|
|
44
|
+
};
|
|
45
|
+
export const getEnqueueLegacyPromptUrl = () => {
|
|
46
|
+
return `/v1/prompts`;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* @summary Submit Prompt Compat
|
|
50
|
+
*/
|
|
51
|
+
export const enqueueLegacyPrompt = async (promptInput, options) => {
|
|
52
|
+
const res = await fetch(getEnqueueLegacyPromptUrl(), {
|
|
53
|
+
...options,
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
56
|
+
body: JSON.stringify(promptInput)
|
|
57
|
+
});
|
|
58
|
+
const body = [204, 205, 304].includes(res.status) ? null : await res.text();
|
|
59
|
+
const data = body ? JSON.parse(body) : {};
|
|
60
|
+
return { data, status: res.status, headers: res.headers };
|
|
61
|
+
};
|
|
62
|
+
export const getGetLegacyPromptUrl = (jobId) => {
|
|
63
|
+
return `/v1/prompts/${jobId}`;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* @summary Prompt Status
|
|
67
|
+
*/
|
|
68
|
+
export const getLegacyPrompt = async (jobId, options) => {
|
|
69
|
+
const res = await fetch(getGetLegacyPromptUrl(jobId), {
|
|
70
|
+
...options,
|
|
71
|
+
method: 'GET'
|
|
72
|
+
});
|
|
73
|
+
const body = [204, 205, 304].includes(res.status) ? null : await res.text();
|
|
74
|
+
const data = body ? JSON.parse(body) : {};
|
|
75
|
+
return { data, status: res.status, headers: res.headers };
|
|
76
|
+
};
|
|
77
|
+
export const getEnqueueQueueItemUrl = () => {
|
|
78
|
+
return `/v1/queue`;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* @summary Queue Json
|
|
82
|
+
*/
|
|
83
|
+
export const enqueueQueueItem = async (queueItemJson, options) => {
|
|
84
|
+
const res = await fetch(getEnqueueQueueItemUrl(), {
|
|
85
|
+
...options,
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
88
|
+
body: JSON.stringify(queueItemJson)
|
|
89
|
+
});
|
|
90
|
+
const body = [204, 205, 304].includes(res.status) ? null : await res.text();
|
|
91
|
+
const data = body ? JSON.parse(body) : {};
|
|
92
|
+
return { data, status: res.status, headers: res.headers };
|
|
93
|
+
};
|
|
94
|
+
export const getUploadQueuePairUrl = () => {
|
|
95
|
+
return `/v1/queue/pairs/upload`;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Atomically receive finished image and audio bytes from a remote producer.
|
|
99
|
+
*
|
|
100
|
+
* The two jobs stay `ingesting` until both files are checksum-verified and
|
|
101
|
+
* committed. `staged=true` persists the pair without making it eligible for
|
|
102
|
+
* playout; the authenticated release endpoint activates it later.
|
|
103
|
+
* @summary Queue Pair Upload
|
|
104
|
+
*/
|
|
105
|
+
export const uploadQueuePair = async (bodyUploadQueuePair, options) => {
|
|
106
|
+
const formData = new FormData();
|
|
107
|
+
formData.append(`audio`, bodyUploadQueuePair.audio);
|
|
108
|
+
formData.append(`audio_sha256`, bodyUploadQueuePair.audio_sha256);
|
|
109
|
+
formData.append(`image`, bodyUploadQueuePair.image);
|
|
110
|
+
formData.append(`image_duration`, bodyUploadQueuePair.image_duration.toString());
|
|
111
|
+
formData.append(`image_sha256`, bodyUploadQueuePair.image_sha256);
|
|
112
|
+
if (bodyUploadQueuePair.staged !== undefined) {
|
|
113
|
+
formData.append(`staged`, bodyUploadQueuePair.staged.toString());
|
|
114
|
+
}
|
|
115
|
+
const res = await fetch(getUploadQueuePairUrl(), {
|
|
116
|
+
...options,
|
|
117
|
+
method: 'POST',
|
|
118
|
+
body: formData
|
|
119
|
+
});
|
|
120
|
+
const body = [204, 205, 304].includes(res.status) ? null : await res.text();
|
|
121
|
+
const data = body ? JSON.parse(body) : {};
|
|
122
|
+
return { data, status: res.status, headers: res.headers };
|
|
123
|
+
};
|
|
124
|
+
export const getGetQueuePairUrl = (pairId) => {
|
|
125
|
+
return `/v1/queue/pairs/${pairId}`;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* @summary Queue Pair Status
|
|
129
|
+
*/
|
|
130
|
+
export const getQueuePair = async (pairId, options) => {
|
|
131
|
+
const res = await fetch(getGetQueuePairUrl(pairId), {
|
|
132
|
+
...options,
|
|
133
|
+
method: 'GET'
|
|
134
|
+
});
|
|
135
|
+
const body = [204, 205, 304].includes(res.status) ? null : await res.text();
|
|
136
|
+
const data = body ? JSON.parse(body) : {};
|
|
137
|
+
return { data, status: res.status, headers: res.headers };
|
|
138
|
+
};
|
|
139
|
+
export const getReleaseQueuePairUrl = (pairId) => {
|
|
140
|
+
return `/v1/queue/pairs/${pairId}/release`;
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* @summary Release Queue Pair
|
|
144
|
+
*/
|
|
145
|
+
export const releaseQueuePair = async (pairId, options) => {
|
|
146
|
+
const res = await fetch(getReleaseQueuePairUrl(pairId), {
|
|
147
|
+
...options,
|
|
148
|
+
method: 'POST'
|
|
149
|
+
});
|
|
150
|
+
const body = [204, 205, 304].includes(res.status) ? null : await res.text();
|
|
151
|
+
const data = body ? JSON.parse(body) : {};
|
|
152
|
+
return { data, status: res.status, headers: res.headers };
|
|
153
|
+
};
|
|
154
|
+
export const getGetQueueSlotUrl = (slotKey) => {
|
|
155
|
+
return `/v1/queue/slots/${slotKey}`;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* @summary Queue Slot Status
|
|
159
|
+
*/
|
|
160
|
+
export const getQueueSlot = async (slotKey, options) => {
|
|
161
|
+
const res = await fetch(getGetQueueSlotUrl(slotKey), {
|
|
162
|
+
...options,
|
|
163
|
+
method: 'GET'
|
|
164
|
+
});
|
|
165
|
+
const body = [204, 205, 304].includes(res.status) ? null : await res.text();
|
|
166
|
+
const data = body ? JSON.parse(body) : {};
|
|
167
|
+
return { data, status: res.status, headers: res.headers };
|
|
168
|
+
};
|
|
169
|
+
export const getReleaseQueueSlotUrl = (slotKey) => {
|
|
170
|
+
return `/v1/queue/slots/${slotKey}/release`;
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* @summary Release Queue Slot
|
|
174
|
+
*/
|
|
175
|
+
export const releaseQueueSlot = async (slotKey, options) => {
|
|
176
|
+
const res = await fetch(getReleaseQueueSlotUrl(slotKey), {
|
|
177
|
+
...options,
|
|
178
|
+
method: 'POST'
|
|
179
|
+
});
|
|
180
|
+
const body = [204, 205, 304].includes(res.status) ? null : await res.text();
|
|
181
|
+
const data = body ? JSON.parse(body) : {};
|
|
182
|
+
return { data, status: res.status, headers: res.headers };
|
|
183
|
+
};
|
|
184
|
+
export const getUploadQueueItemUrl = () => {
|
|
185
|
+
return `/v1/queue/upload`;
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* @summary Queue Upload
|
|
189
|
+
*/
|
|
190
|
+
export const uploadQueueItem = async (bodyUploadQueueItem, options) => {
|
|
191
|
+
const formData = new FormData();
|
|
192
|
+
if (bodyUploadQueueItem.duration !== undefined && bodyUploadQueueItem.duration !== null) {
|
|
193
|
+
formData.append(`duration`, bodyUploadQueueItem.duration.toString());
|
|
194
|
+
}
|
|
195
|
+
formData.append(`file`, bodyUploadQueueItem.file);
|
|
196
|
+
if (bodyUploadQueueItem.media_type !== undefined) {
|
|
197
|
+
formData.append(`media_type`, bodyUploadQueueItem.media_type);
|
|
198
|
+
}
|
|
199
|
+
if (bodyUploadQueueItem.sha256 !== undefined && bodyUploadQueueItem.sha256 !== null) {
|
|
200
|
+
formData.append(`sha256`, bodyUploadQueueItem.sha256);
|
|
201
|
+
}
|
|
202
|
+
if (bodyUploadQueueItem.slot_key !== undefined && bodyUploadQueueItem.slot_key !== null) {
|
|
203
|
+
formData.append(`slot_key`, bodyUploadQueueItem.slot_key);
|
|
204
|
+
}
|
|
205
|
+
if (bodyUploadQueueItem.staged !== undefined) {
|
|
206
|
+
formData.append(`staged`, bodyUploadQueueItem.staged.toString());
|
|
207
|
+
}
|
|
208
|
+
const res = await fetch(getUploadQueueItemUrl(), {
|
|
209
|
+
...options,
|
|
210
|
+
method: 'POST',
|
|
211
|
+
body: formData
|
|
212
|
+
});
|
|
213
|
+
const body = [204, 205, 304].includes(res.status) ? null : await res.text();
|
|
214
|
+
const data = body ? JSON.parse(body) : {};
|
|
215
|
+
return { data, status: res.status, headers: res.headers };
|
|
216
|
+
};
|
|
217
|
+
export const getGetQueueItemUrl = (jobId) => {
|
|
218
|
+
return `/v1/queue/${jobId}`;
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* @summary Queue Status
|
|
222
|
+
*/
|
|
223
|
+
export const getQueueItem = async (jobId, options) => {
|
|
224
|
+
const res = await fetch(getGetQueueItemUrl(jobId), {
|
|
225
|
+
...options,
|
|
226
|
+
method: 'GET'
|
|
227
|
+
});
|
|
228
|
+
const body = [204, 205, 304].includes(res.status) ? null : await res.text();
|
|
229
|
+
const data = body ? JSON.parse(body) : {};
|
|
230
|
+
return { data, status: res.status, headers: res.headers };
|
|
231
|
+
};
|
|
232
|
+
export const getGetStreamInfoUrl = () => {
|
|
233
|
+
return `/v1/stream`;
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* @summary Stream Info
|
|
237
|
+
*/
|
|
238
|
+
export const getStreamInfo = async (options) => {
|
|
239
|
+
const res = await fetch(getGetStreamInfoUrl(), {
|
|
240
|
+
...options,
|
|
241
|
+
method: 'GET'
|
|
242
|
+
});
|
|
243
|
+
const body = [204, 205, 304].includes(res.status) ? null : await res.text();
|
|
244
|
+
const data = body ? JSON.parse(body) : {};
|
|
245
|
+
return { data, status: res.status, headers: res.headers };
|
|
246
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from "./client.js";
|
|
2
|
+
// Low-level OpenAPI client. Prefer QueueBroadcastClient for application code.
|
|
3
|
+
export * from "./generated/api.js";
|
|
4
|
+
// Streaming infrastructure
|
|
5
|
+
export * from "./streaming/index.js";
|
|
6
|
+
// Monitoring infrastructure
|
|
7
|
+
export * from "./monitoring/metrics.js";
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple metrics collection for monitoring streaming infrastructure.
|
|
3
|
+
* Designed for containerized deployment with HTTP polling instead of WebSocket.
|
|
4
|
+
*/
|
|
5
|
+
export interface MetricsCollector {
|
|
6
|
+
/** Get current metrics snapshot */
|
|
7
|
+
getMetrics(): Record<string, unknown>;
|
|
8
|
+
/** Reset all metrics */
|
|
9
|
+
reset(): void;
|
|
10
|
+
}
|
|
11
|
+
export interface MetricSnapshot {
|
|
12
|
+
timestamp: number;
|
|
13
|
+
component: string;
|
|
14
|
+
metrics: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Simple metrics registry that can collect metrics from multiple components.
|
|
18
|
+
* Provides HTTP-friendly metrics output for containerized environments.
|
|
19
|
+
*/
|
|
20
|
+
export declare class MetricsRegistry {
|
|
21
|
+
private collectors;
|
|
22
|
+
private history;
|
|
23
|
+
private latestMetrics;
|
|
24
|
+
private maxHistorySize;
|
|
25
|
+
constructor(maxHistorySize?: number);
|
|
26
|
+
/**
|
|
27
|
+
* Register a metrics collector
|
|
28
|
+
*/
|
|
29
|
+
register(componentName: string, collector: MetricsCollector): void;
|
|
30
|
+
/**
|
|
31
|
+
* Unregister a metrics collector
|
|
32
|
+
*/
|
|
33
|
+
unregister(componentName: string): void;
|
|
34
|
+
/**
|
|
35
|
+
* Collect metrics from all registered components
|
|
36
|
+
*/
|
|
37
|
+
collectMetrics(): Record<string, MetricSnapshot>;
|
|
38
|
+
/**
|
|
39
|
+
* Get latest metrics snapshot
|
|
40
|
+
*/
|
|
41
|
+
getLatestMetrics(): Record<string, MetricSnapshot> | null;
|
|
42
|
+
/**
|
|
43
|
+
* Get metrics history
|
|
44
|
+
*/
|
|
45
|
+
getHistory(durationMs?: number): MetricSnapshot[];
|
|
46
|
+
/**
|
|
47
|
+
* Reset all collectors and history
|
|
48
|
+
*/
|
|
49
|
+
reset(): void;
|
|
50
|
+
/**
|
|
51
|
+
* Export metrics in Prometheus-compatible format
|
|
52
|
+
*/
|
|
53
|
+
exportPrometheusFormat(): string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Simple metrics collector for basic counter and gauge metrics
|
|
57
|
+
*/
|
|
58
|
+
export declare class SimpleMetrics implements MetricsCollector {
|
|
59
|
+
private counters;
|
|
60
|
+
private gauges;
|
|
61
|
+
private labels;
|
|
62
|
+
/**
|
|
63
|
+
* Increment a counter
|
|
64
|
+
*/
|
|
65
|
+
incrementCounter(name: string, value?: number): void;
|
|
66
|
+
/**
|
|
67
|
+
* Set a gauge value
|
|
68
|
+
*/
|
|
69
|
+
setGauge(name: string, value: number): void;
|
|
70
|
+
/**
|
|
71
|
+
* Get counter value
|
|
72
|
+
*/
|
|
73
|
+
getCounter(name: string): number;
|
|
74
|
+
/**
|
|
75
|
+
* Get gauge value
|
|
76
|
+
*/
|
|
77
|
+
getGauge(name: string): number;
|
|
78
|
+
/**
|
|
79
|
+
* Set a label (metadata)
|
|
80
|
+
*/
|
|
81
|
+
setLabel(name: string, value: string): void;
|
|
82
|
+
/**
|
|
83
|
+
* Get all metrics
|
|
84
|
+
*/
|
|
85
|
+
getMetrics(): Record<string, unknown>;
|
|
86
|
+
/**
|
|
87
|
+
* Reset all metrics
|
|
88
|
+
*/
|
|
89
|
+
reset(): void;
|
|
90
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple metrics collection for monitoring streaming infrastructure.
|
|
3
|
+
* Designed for containerized deployment with HTTP polling instead of WebSocket.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Simple metrics registry that can collect metrics from multiple components.
|
|
7
|
+
* Provides HTTP-friendly metrics output for containerized environments.
|
|
8
|
+
*/
|
|
9
|
+
export class MetricsRegistry {
|
|
10
|
+
collectors = new Map();
|
|
11
|
+
history = [];
|
|
12
|
+
latestMetrics = null;
|
|
13
|
+
maxHistorySize;
|
|
14
|
+
constructor(maxHistorySize = 300) {
|
|
15
|
+
this.maxHistorySize = maxHistorySize;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Register a metrics collector
|
|
19
|
+
*/
|
|
20
|
+
register(componentName, collector) {
|
|
21
|
+
this.collectors.set(componentName, collector);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Unregister a metrics collector
|
|
25
|
+
*/
|
|
26
|
+
unregister(componentName) {
|
|
27
|
+
this.collectors.delete(componentName);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Collect metrics from all registered components
|
|
31
|
+
*/
|
|
32
|
+
collectMetrics() {
|
|
33
|
+
const timestamp = Date.now();
|
|
34
|
+
const metrics = {};
|
|
35
|
+
for (const [componentName, collector] of this.collectors.entries()) {
|
|
36
|
+
try {
|
|
37
|
+
const componentMetrics = collector.getMetrics();
|
|
38
|
+
metrics[componentName] = {
|
|
39
|
+
timestamp,
|
|
40
|
+
component: componentName,
|
|
41
|
+
metrics: componentMetrics,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
metrics[componentName] = {
|
|
46
|
+
timestamp,
|
|
47
|
+
component: componentName,
|
|
48
|
+
metrics: {
|
|
49
|
+
error: error instanceof Error ? error.message : String(error),
|
|
50
|
+
status: 'error',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Add to history
|
|
56
|
+
const snapshot = {
|
|
57
|
+
timestamp,
|
|
58
|
+
component: 'registry',
|
|
59
|
+
metrics: {
|
|
60
|
+
totalComponents: this.collectors.size,
|
|
61
|
+
componentStatuses: Object.fromEntries(Object.entries(metrics).map(([name, snapshot]) => [
|
|
62
|
+
name,
|
|
63
|
+
snapshot.metrics.error ? 'error' : 'ok',
|
|
64
|
+
])),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
this.history.push(snapshot);
|
|
68
|
+
if (this.history.length > this.maxHistorySize) {
|
|
69
|
+
this.history.shift();
|
|
70
|
+
}
|
|
71
|
+
this.latestMetrics = metrics;
|
|
72
|
+
return metrics;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get latest metrics snapshot
|
|
76
|
+
*/
|
|
77
|
+
getLatestMetrics() {
|
|
78
|
+
if (this.history.length === 0) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return this.latestMetrics;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get metrics history
|
|
85
|
+
*/
|
|
86
|
+
getHistory(durationMs) {
|
|
87
|
+
if (!durationMs) {
|
|
88
|
+
return [...this.history];
|
|
89
|
+
}
|
|
90
|
+
const cutoff = Date.now() - durationMs;
|
|
91
|
+
return this.history.filter(snapshot => snapshot.timestamp >= cutoff);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Reset all collectors and history
|
|
95
|
+
*/
|
|
96
|
+
reset() {
|
|
97
|
+
for (const collector of this.collectors.values()) {
|
|
98
|
+
try {
|
|
99
|
+
collector.reset();
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
console.error(`Error resetting collector: ${error}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
this.history = [];
|
|
106
|
+
this.latestMetrics = null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Export metrics in Prometheus-compatible format
|
|
110
|
+
*/
|
|
111
|
+
exportPrometheusFormat() {
|
|
112
|
+
const metrics = this.collectMetrics();
|
|
113
|
+
const lines = [];
|
|
114
|
+
for (const [componentName, snapshot] of Object.entries(metrics)) {
|
|
115
|
+
lines.push(`# Component: ${componentName}`);
|
|
116
|
+
lines.push(`# Timestamp: ${new Date(snapshot.timestamp).toISOString()}`);
|
|
117
|
+
for (const [key, value] of Object.entries(snapshot.metrics)) {
|
|
118
|
+
if (typeof value === 'number') {
|
|
119
|
+
lines.push(`${componentName}_${key} ${value}`);
|
|
120
|
+
}
|
|
121
|
+
else if (typeof value === 'boolean') {
|
|
122
|
+
lines.push(`${componentName}_${key} ${value ? 1 : 0}`);
|
|
123
|
+
}
|
|
124
|
+
else if (typeof value === 'string') {
|
|
125
|
+
lines.push(`${componentName}_${key} "${value}"`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
lines.push('');
|
|
129
|
+
}
|
|
130
|
+
return lines.join('\n');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Simple metrics collector for basic counter and gauge metrics
|
|
135
|
+
*/
|
|
136
|
+
export class SimpleMetrics {
|
|
137
|
+
counters = new Map();
|
|
138
|
+
gauges = new Map();
|
|
139
|
+
labels = new Map();
|
|
140
|
+
/**
|
|
141
|
+
* Increment a counter
|
|
142
|
+
*/
|
|
143
|
+
incrementCounter(name, value = 1) {
|
|
144
|
+
const current = this.counters.get(name) || 0;
|
|
145
|
+
this.counters.set(name, current + value);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Set a gauge value
|
|
149
|
+
*/
|
|
150
|
+
setGauge(name, value) {
|
|
151
|
+
this.gauges.set(name, value);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Get counter value
|
|
155
|
+
*/
|
|
156
|
+
getCounter(name) {
|
|
157
|
+
return this.counters.get(name) || 0;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get gauge value
|
|
161
|
+
*/
|
|
162
|
+
getGauge(name) {
|
|
163
|
+
return this.gauges.get(name) || 0;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Set a label (metadata)
|
|
167
|
+
*/
|
|
168
|
+
setLabel(name, value) {
|
|
169
|
+
this.labels.set(name, value);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get all metrics
|
|
173
|
+
*/
|
|
174
|
+
getMetrics() {
|
|
175
|
+
const metrics = {
|
|
176
|
+
...Object.fromEntries(this.counters),
|
|
177
|
+
...Object.fromEntries(this.gauges),
|
|
178
|
+
...Object.fromEntries(this.labels),
|
|
179
|
+
};
|
|
180
|
+
return metrics;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Reset all metrics
|
|
184
|
+
*/
|
|
185
|
+
reset() {
|
|
186
|
+
this.counters.clear();
|
|
187
|
+
this.gauges.clear();
|
|
188
|
+
this.labels.clear();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frame buffer management for real-time video streaming.
|
|
3
|
+
* Extracted and adapted from infinite-tv's RTMP streamer patterns.
|
|
4
|
+
*/
|
|
5
|
+
export interface FrameBufferOptions {
|
|
6
|
+
maxSize?: number;
|
|
7
|
+
}
|
|
8
|
+
/** A bounded FIFO that retains the newest frames when a producer gets ahead. */
|
|
9
|
+
export declare class FrameBuffer<T> {
|
|
10
|
+
private readonly queue;
|
|
11
|
+
private readonly maxSize;
|
|
12
|
+
private framesDropped;
|
|
13
|
+
private framesAddedTotal;
|
|
14
|
+
constructor(options?: FrameBufferOptions);
|
|
15
|
+
/** Adds a frame and returns false when the oldest queued frame was dropped. */
|
|
16
|
+
addFrame(frame: T): boolean;
|
|
17
|
+
addFrameBatch(frames: readonly T[]): number;
|
|
18
|
+
getNextFrame(): T | null;
|
|
19
|
+
getStatus(): {
|
|
20
|
+
queueSize: number;
|
|
21
|
+
maxSize: number;
|
|
22
|
+
framesDropped: number;
|
|
23
|
+
framesAddedTotal: number;
|
|
24
|
+
utilization: number;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Clear the buffer
|
|
28
|
+
*/
|
|
29
|
+
clear(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Reset metrics
|
|
32
|
+
*/
|
|
33
|
+
resetMetrics(): void;
|
|
34
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frame buffer management for real-time video streaming.
|
|
3
|
+
* Extracted and adapted from infinite-tv's RTMP streamer patterns.
|
|
4
|
+
*/
|
|
5
|
+
/** A bounded FIFO that retains the newest frames when a producer gets ahead. */
|
|
6
|
+
export class FrameBuffer {
|
|
7
|
+
queue = [];
|
|
8
|
+
maxSize;
|
|
9
|
+
framesDropped = 0;
|
|
10
|
+
framesAddedTotal = 0;
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.maxSize = options.maxSize ?? 1_000;
|
|
13
|
+
if (!Number.isInteger(this.maxSize) || this.maxSize < 1) {
|
|
14
|
+
throw new TypeError("maxSize must be a positive integer");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Adds a frame and returns false when the oldest queued frame was dropped. */
|
|
18
|
+
addFrame(frame) {
|
|
19
|
+
const droppedFrame = this.queue.length === this.maxSize;
|
|
20
|
+
if (droppedFrame) {
|
|
21
|
+
this.queue.shift();
|
|
22
|
+
this.framesDropped++;
|
|
23
|
+
}
|
|
24
|
+
this.queue.push(frame);
|
|
25
|
+
this.framesAddedTotal++;
|
|
26
|
+
return !droppedFrame;
|
|
27
|
+
}
|
|
28
|
+
addFrameBatch(frames) {
|
|
29
|
+
let processedCount = 0;
|
|
30
|
+
for (const frame of frames) {
|
|
31
|
+
this.addFrame(frame);
|
|
32
|
+
processedCount++;
|
|
33
|
+
}
|
|
34
|
+
return processedCount;
|
|
35
|
+
}
|
|
36
|
+
getNextFrame() {
|
|
37
|
+
return this.queue.shift() ?? null;
|
|
38
|
+
}
|
|
39
|
+
getStatus() {
|
|
40
|
+
return {
|
|
41
|
+
queueSize: this.queue.length,
|
|
42
|
+
maxSize: this.maxSize,
|
|
43
|
+
framesDropped: this.framesDropped,
|
|
44
|
+
framesAddedTotal: this.framesAddedTotal,
|
|
45
|
+
utilization: this.queue.length / this.maxSize,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Clear the buffer
|
|
50
|
+
*/
|
|
51
|
+
clear() {
|
|
52
|
+
this.queue.length = 0;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Reset metrics
|
|
56
|
+
*/
|
|
57
|
+
resetMetrics() {
|
|
58
|
+
this.framesDropped = 0;
|
|
59
|
+
this.framesAddedTotal = 0;
|
|
60
|
+
}
|
|
61
|
+
}
|