@hff-ai/media-processor-client 1.0.0

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 ADDED
@@ -0,0 +1,570 @@
1
+ # @hff-ai/media-processor-client
2
+
3
+ A TypeScript client library for communicating with the HFF Media Processor service. Abstracts away AMQP and REST API details, providing both async/await and event-based APIs.
4
+
5
+ ## Features
6
+
7
+ - **Dual API**: Choose between async/await (`processS3*`) or event-based (`queueS3*`) styles
8
+ - **Clean separation**: Async requests don't emit events, preventing double-handling
9
+ - **Progress callbacks**: Track processing progress with optional callbacks
10
+ - **Auto-reconnect**: Automatic reconnection handling with configurable retry logic
11
+ - **Type-safe**: Full TypeScript support with comprehensive type definitions
12
+ - **Zero-config MIME detection**: Automatically detects MIME types from file extensions
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @hff-ai/media-processor-client
18
+ ```
19
+
20
+ Or if using the package locally within the monorepo:
21
+
22
+ ```bash
23
+ npm install ./packages/media-processor-client
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ### Async/Await (Recommended)
29
+
30
+ ```typescript
31
+ import { media_processor, ProcessingError } from '@hff-ai/media-processor-client';
32
+
33
+ const processor = await media_processor.connect({
34
+ processorUrl: 'https://mp.hff.ai',
35
+ processorAmqp: 'amqp://user:pass@mp.hff.ai',
36
+ token: process.env.MEDIA_PROCESSOR_TOKEN!,
37
+ s3Details: {
38
+ provider: 'idrive',
39
+ endpoint: 'https://s3.us-east-1.idrivee2-27.com',
40
+ accessKey: process.env.S3_ACCESS_KEY!,
41
+ secretAccessKey: process.env.S3_SECRET_KEY!,
42
+ region: 'us-east-1',
43
+ bucketName: 'my-media-bucket',
44
+ },
45
+ system: 'my-application',
46
+ });
47
+
48
+ // Process and await result
49
+ try {
50
+ const result = await processor.processS3Image('uploads/photo.jpg', {
51
+ timeout: 30000,
52
+ onProgress: (p) => console.log(`${p.progress}%`)
53
+ });
54
+ console.log('Done:', result.result.thumbnailS3Key);
55
+ } catch (error) {
56
+ if (error instanceof ProcessingError) {
57
+ console.error(`Failed: ${error.errorCode}`);
58
+ }
59
+ }
60
+ ```
61
+
62
+ ### Event-Based (Fire-and-Forget)
63
+
64
+ **Important:** Set up handlers BEFORE connecting to capture queued messages.
65
+
66
+ ```typescript
67
+ import { media_processor, MediaResultMessage } from '@hff-ai/media-processor-client';
68
+
69
+ // Create without connecting
70
+ const processor = media_processor.create(config);
71
+
72
+ // Set up handlers FIRST
73
+ processor.on('image_completed', (msg: MediaResultMessage) => {
74
+ console.log('Image processed:', msg.result.s3Key);
75
+ });
76
+
77
+ // NOW connect - handlers ready for queued messages
78
+ await processor.connect();
79
+
80
+ // Queue (returns immediately)
81
+ const mediaId = processor.queueS3Image('uploads/photo.jpg');
82
+ ```
83
+
84
+ ## Configuration
85
+
86
+ ### MediaProcessorConfig
87
+
88
+ | Property | Type | Required | Description |
89
+ |----------|------|----------|-------------|
90
+ | `processorUrl` | `string` | Yes | Base URL of the media processor REST API |
91
+ | `processorAmqp` | `string` | Yes | AMQP connection URL for RabbitMQ |
92
+ | `token` | `string` | Yes | JWT authentication token |
93
+ | `s3Details` | `S3Config` | Yes | S3 bucket configuration |
94
+ | `system` | `string` | Yes | System namespace for queue routing |
95
+ | `options` | `ProcessorOptions` | No | Optional processing defaults |
96
+
97
+ ### S3Config
98
+
99
+ | Property | Type | Required | Description |
100
+ |----------|------|----------|-------------|
101
+ | `provider` | `string` | Yes | S3-compatible provider (e.g., 'idrive', 'aws', 'minio') |
102
+ | `endpoint` | `string` | Yes | S3 endpoint URL |
103
+ | `accessKey` | `string` | Yes | S3 access key ID |
104
+ | `secretAccessKey` | `string` | Yes | S3 secret access key |
105
+ | `region` | `string` | Yes | S3 region |
106
+ | `bucketName` | `string` | Yes | Bucket name |
107
+
108
+ ### ProcessorOptions
109
+
110
+ | Property | Type | Default | Description |
111
+ |----------|------|---------|-------------|
112
+ | `maxConcurrentJobs` | `number` | `5` | Maximum concurrent processing jobs |
113
+ | `defaultUserId` | `string` | `'anonymous'` | Default user ID for requests |
114
+ | `autoReconnect` | `boolean` | `true` | Auto-reconnect on connection loss |
115
+ | `reconnectDelay` | `number` | `5000` | Reconnection delay in milliseconds |
116
+ | `maxReconnectAttempts` | `number` | `10` | Maximum reconnection attempts (0 = infinite) |
117
+ | `requestTimeout` | `number` | `30000` | Request timeout in milliseconds |
118
+
119
+ ## API Reference
120
+
121
+ ### Factory Methods
122
+
123
+ #### `media_processor.connect(config)`
124
+
125
+ Create and connect to a MediaProcessor instance.
126
+
127
+ ```typescript
128
+ const processor = await media_processor.connect(config);
129
+ ```
130
+
131
+ #### `media_processor.create(config)`
132
+
133
+ Create a MediaProcessor instance without connecting. Useful for setting up event handlers before connecting.
134
+
135
+ ```typescript
136
+ const processor = media_processor.create(config);
137
+ processor.on('connected', () => console.log('Connected!'));
138
+ await processor.connect();
139
+ ```
140
+
141
+ ### Async Processing Methods (Promise-based)
142
+
143
+ These methods return a Promise that resolves when processing completes.
144
+
145
+ **Note:** Results from async methods are delivered only via the Promise - no events are emitted. This allows safe mixing of both API styles in the same application.
146
+
147
+ #### `processor.processS3Image(s3Key, options?): Promise<MediaResultMessage>`
148
+
149
+ Process an image and await the result.
150
+
151
+ ```typescript
152
+ const result = await processor.processS3Image('uploads/photo.jpg', {
153
+ userId: 'user-123',
154
+ timeout: 30000,
155
+ onProgress: (p) => console.log(`${p.progress}%`),
156
+ });
157
+ console.log('Thumbnail:', result.result.thumbnailS3Key);
158
+ ```
159
+
160
+ #### `processor.processS3Video(s3Key, options?): Promise<MediaResultMessage>`
161
+
162
+ Process a video and await the result.
163
+
164
+ ```typescript
165
+ const result = await processor.processS3Video('uploads/video.mp4', {
166
+ userId: 'user-123',
167
+ targetQualities: ['1080p', '720p', '480p'],
168
+ timeout: 300000, // 5 minutes
169
+ onProgress: (p) => updateProgressBar(p.progress),
170
+ });
171
+ ```
172
+
173
+ #### `processor.processS3Zip(s3Key, options?): Promise<MediaResultMessage>`
174
+
175
+ Process a zip archive and await the result.
176
+
177
+ ```typescript
178
+ const result = await processor.processS3Zip('uploads/photos.zip', {
179
+ userId: 'user-123',
180
+ processImages: true,
181
+ processVideos: true,
182
+ timeout: 120000,
183
+ });
184
+ ```
185
+
186
+ ### Queue Methods (Fire-and-Forget)
187
+
188
+ These methods return immediately. Results are delivered exclusively via events (`*_completed`, `*_error`, `*_progress`).
189
+
190
+ #### `processor.queueS3Image(s3Key, options?): string`
191
+
192
+ Queue an image for processing.
193
+
194
+ ```typescript
195
+ const mediaId = processor.queueS3Image('uploads/photo.jpg', {
196
+ userId: 'user-123',
197
+ });
198
+ // Handle result via processor.on('image_completed', ...)
199
+ ```
200
+
201
+ #### `processor.queueS3Video(s3Key, options?): string`
202
+
203
+ Queue a video for processing.
204
+
205
+ ```typescript
206
+ const mediaId = processor.queueS3Video('uploads/video.mp4', {
207
+ userId: 'user-123',
208
+ targetQualities: ['1080p', '720p'],
209
+ });
210
+ ```
211
+
212
+ #### `processor.queueS3Zip(s3Key, options?): string`
213
+
214
+ Queue a zip archive for processing.
215
+
216
+ ```typescript
217
+ const mediaId = processor.queueS3Zip('uploads/photos.zip', {
218
+ userId: 'user-123',
219
+ });
220
+ ```
221
+
222
+ ### Connection Methods
223
+
224
+ #### `processor.disconnect()`
225
+
226
+ Gracefully close all connections.
227
+
228
+ ```typescript
229
+ await processor.disconnect();
230
+ ```
231
+
232
+ #### `processor.isConnected()`
233
+
234
+ Check connection status.
235
+
236
+ ```typescript
237
+ if (processor.isConnected()) {
238
+ // Safe to submit requests
239
+ }
240
+ ```
241
+
242
+ #### `processor.reconnect()`
243
+
244
+ Manually trigger reconnection.
245
+
246
+ ```typescript
247
+ await processor.reconnect();
248
+ ```
249
+
250
+ #### `processor.updateToken(token)`
251
+
252
+ Update the authentication token (e.g., after token refresh).
253
+
254
+ ```typescript
255
+ processor.updateToken(newToken);
256
+ ```
257
+
258
+ #### `processor.getSystemStatus()`
259
+
260
+ Get current system status and statistics.
261
+
262
+ ```typescript
263
+ const status = await processor.getSystemStatus();
264
+ console.log(`Pending jobs: ${status.pendingJobs}`);
265
+ ```
266
+
267
+ ## Events
268
+
269
+ ### Processing Events
270
+
271
+ | Event | Payload | Description |
272
+ |-------|---------|-------------|
273
+ | `image_progress` | `MediaProgressMessage` | Image processing progress |
274
+ | `image_completed` | `MediaResultMessage` | Image processing completed |
275
+ | `image_error` | `MediaErrorMessage` | Image processing failed |
276
+ | `video_progress` | `MediaProgressMessage` | Video processing progress |
277
+ | `video_completed` | `MediaResultMessage` | Video processing completed |
278
+ | `video_error` | `MediaErrorMessage` | Video processing failed |
279
+ | `zip_progress` | `MediaProgressMessage` | Zip extraction progress |
280
+ | `zip_completed` | `MediaResultMessage` | Zip processing completed |
281
+ | `zip_error` | `MediaErrorMessage` | Zip processing failed |
282
+ | `progress` | `MediaProgressMessage` | Any media type progress |
283
+ | `completed` | `MediaResultMessage` | Any media type completed |
284
+ | `processing_error` | `MediaErrorMessage` | Any media type failed |
285
+
286
+ ### Connection Events
287
+
288
+ | Event | Payload | Description |
289
+ |-------|---------|-------------|
290
+ | `connected` | `void` | Successfully connected |
291
+ | `disconnected` | `{ reason: string }` | Connection lost |
292
+ | `reconnecting` | `{ attempt: number }` | Attempting to reconnect |
293
+ | `error` | `Error` | Connection-level error |
294
+
295
+ ### Warning Events
296
+
297
+ | Event | Payload | Description |
298
+ |-------|---------|-------------|
299
+ | `unhandled_message` | `UnhandledMessagePayload` | Orphaned or unhandled message (will be dropped) |
300
+
301
+ The `unhandled_message` event fires in two scenarios:
302
+
303
+ 1. **Orphaned async messages**: A `processS3*()` request from a previous process that died. These have a `correlationId` but no matching pending promise. They are NOT sent to regular event handlers.
304
+
305
+ 2. **Queue messages with no listeners**: A `queueS3*()` result where no event handler is registered.
306
+
307
+ ```typescript
308
+ processor.on('unhandled_message', (warning) => {
309
+ console.warn(`Dropped: ${warning.message}`);
310
+ // Optionally: save to dead-letter store, alert, etc.
311
+ });
312
+ ```
313
+
314
+ ## Error Handling
315
+
316
+ The library provides typed error classes for different scenarios:
317
+
318
+ ### Error Classes
319
+
320
+ ```typescript
321
+ import {
322
+ MediaProcessorError, // Base class
323
+ ConnectionError, // Connection-related errors
324
+ ConfigurationError, // Configuration errors
325
+ RequestError, // Queue request errors (sync)
326
+ ProcessingError, // Processing failures (async, from process* methods)
327
+ } from '@hff-ai/media-processor-client';
328
+ ```
329
+
330
+ ### Connection Errors
331
+
332
+ | Code | Description |
333
+ |------|-------------|
334
+ | `CONNECTION_FAILED` | Cannot reach the processor |
335
+ | `CONNECTION_LOST` | Connection dropped |
336
+ | `AUTH_FAILED` | Invalid or expired token |
337
+ | `AMQP_ERROR` | RabbitMQ error |
338
+ | `REST_API_ERROR` | REST API error |
339
+ | `RECONNECT_FAILED` | Exceeded reconnection attempts |
340
+
341
+ ### Processing Errors
342
+
343
+ | Code | Description |
344
+ |------|-------------|
345
+ | `S3_ACCESS_DENIED` | Insufficient S3 permissions |
346
+ | `S3_NOT_FOUND` | Source file not found |
347
+ | `S3_UPLOAD_FAILED` | Failed to upload results |
348
+ | `INVALID_FORMAT` | Invalid file format |
349
+ | `UNSUPPORTED_CODEC` | Unsupported video codec |
350
+ | `FILE_TOO_LARGE` | File exceeds size limit |
351
+ | `FILE_CORRUPTED` | Damaged file |
352
+ | `PROCESSING_TIMEOUT` | Processing timed out |
353
+ | `TRANSCODING_FAILED` | Video transcoding failed |
354
+ | `ZIP_EXTRACTION_FAILED` | Zip extraction failed |
355
+ | `ZIP_PASSWORD_PROTECTED` | Password-protected zip |
356
+
357
+ ### ProcessingError Codes (async methods)
358
+
359
+ | Code | Description |
360
+ |------|-------------|
361
+ | `PROCESSING_FAILED` | Server returned an error (check `errorCode` for details) |
362
+ | `PROCESSING_TIMEOUT` | Client-side timeout exceeded |
363
+ | `DISCONNECTED` | Connection lost during processing |
364
+
365
+ ### Error Handling Example
366
+
367
+ ```typescript
368
+ import { ConnectionError, ProcessingError, MediaErrorMessage } from '@hff-ai/media-processor-client';
369
+
370
+ // Async error handling (try/catch)
371
+ try {
372
+ const result = await processor.processS3Image('uploads/photo.jpg', {
373
+ timeout: 30000
374
+ });
375
+ } catch (error) {
376
+ if (error instanceof ProcessingError) {
377
+ console.error(`Processing failed: ${error.errorCode} - ${error.message}`);
378
+ if (error.errorCode === 'S3_NOT_FOUND') {
379
+ // Handle missing file
380
+ }
381
+ }
382
+ }
383
+
384
+ // Event-based error handling (for queue methods)
385
+ processor.on('image_error', (msg: MediaErrorMessage) => {
386
+ switch (msg.errorCode) {
387
+ case 'S3_NOT_FOUND':
388
+ console.error('File not found:', msg.details);
389
+ break;
390
+ case 'INVALID_FORMAT':
391
+ notifyUser('Invalid image format');
392
+ break;
393
+ default:
394
+ logError(msg);
395
+ }
396
+ });
397
+
398
+ // Connection errors
399
+ processor.on('error', (error: ConnectionError) => {
400
+ if (error.code === 'AUTH_FAILED') {
401
+ refreshToken().then(newToken => {
402
+ processor.updateToken(newToken);
403
+ processor.reconnect();
404
+ });
405
+ }
406
+ });
407
+ ```
408
+
409
+ ## Complete Example
410
+
411
+ ```typescript
412
+ import {
413
+ media_processor,
414
+ MediaProcessorConfig,
415
+ MediaResultMessage,
416
+ MediaProgressMessage,
417
+ MediaErrorMessage,
418
+ ConnectionError,
419
+ } from '@hff-ai/media-processor-client';
420
+
421
+ const config: MediaProcessorConfig = {
422
+ processorUrl: process.env.MEDIA_PROCESSOR_URL!,
423
+ processorAmqp: process.env.MEDIA_PROCESSOR_AMQP!,
424
+ token: process.env.MEDIA_PROCESSOR_TOKEN!,
425
+ s3Details: {
426
+ provider: 'idrive',
427
+ endpoint: process.env.S3_ENDPOINT!,
428
+ accessKey: process.env.S3_ACCESS_KEY!,
429
+ secretAccessKey: process.env.S3_SECRET_KEY!,
430
+ region: process.env.S3_REGION || 'us-east-1',
431
+ bucketName: process.env.S3_BUCKET!,
432
+ },
433
+ system: 'my-application',
434
+ options: {
435
+ maxConcurrentJobs: 10,
436
+ autoReconnect: true,
437
+ },
438
+ };
439
+
440
+ async function main() {
441
+ // Create processor with event handlers before connecting
442
+ const processor = media_processor.create(config);
443
+
444
+ // Connection events
445
+ processor.on('connected', () => {
446
+ console.log('Connected to media processor');
447
+ });
448
+
449
+ processor.on('disconnected', ({ reason }) => {
450
+ console.warn('Disconnected:', reason);
451
+ });
452
+
453
+ processor.on('reconnecting', ({ attempt }) => {
454
+ console.log(`Reconnecting (attempt ${attempt})...`);
455
+ });
456
+
457
+ processor.on('error', (error: Error) => {
458
+ console.error('Connection error:', error.message);
459
+ });
460
+
461
+ // Processing events
462
+ processor.on('image_progress', (msg: MediaProgressMessage) => {
463
+ console.log(`Image ${msg.mediaId}: ${msg.progress}% - ${msg.message}`);
464
+ });
465
+
466
+ processor.on('image_completed', (msg: MediaResultMessage) => {
467
+ console.log(`Image completed: ${msg.mediaId}`);
468
+ // Save to database, update UI, etc.
469
+ });
470
+
471
+ processor.on('image_error', (msg: MediaErrorMessage) => {
472
+ console.error(`Image failed: ${msg.mediaId} - ${msg.error}`);
473
+ });
474
+
475
+ processor.on('video_progress', (msg: MediaProgressMessage) => {
476
+ updateProgressBar(msg.mediaId, msg.progress);
477
+ });
478
+
479
+ processor.on('video_completed', (msg: MediaResultMessage) => {
480
+ console.log(`Video completed: ${msg.mediaId}`);
481
+ });
482
+
483
+ // Connect
484
+ try {
485
+ await processor.connect();
486
+ } catch (error) {
487
+ if (error instanceof ConnectionError) {
488
+ console.error(`Failed to connect: ${error.code} - ${error.message}`);
489
+ }
490
+ process.exit(1);
491
+ }
492
+
493
+ // Process some media
494
+ try {
495
+ const imageId = processor.processS3Image('uploads/photo.jpg', {
496
+ userId: 'user-123',
497
+ });
498
+ console.log(`Processing image: ${imageId}`);
499
+
500
+ const videoId = processor.processS3Video('uploads/video.mp4', {
501
+ userId: 'user-123',
502
+ targetQualities: ['1080p', '720p'],
503
+ });
504
+ console.log(`Processing video: ${videoId}`);
505
+ } catch (error) {
506
+ console.error('Failed to submit request:', error);
507
+ }
508
+
509
+ // Graceful shutdown
510
+ process.on('SIGINT', async () => {
511
+ console.log('Shutting down...');
512
+ await processor.disconnect();
513
+ process.exit(0);
514
+ });
515
+ }
516
+
517
+ main();
518
+ ```
519
+
520
+ ## Type Guards
521
+
522
+ The library provides type guards for narrowing message types:
523
+
524
+ ```typescript
525
+ import {
526
+ isProgressMessage,
527
+ isResultMessage,
528
+ isErrorMessage,
529
+ isImageResult,
530
+ isVideoResult,
531
+ isZipResult,
532
+ } from '@hff-ai/media-processor-client';
533
+
534
+ processor.on('completed', (msg) => {
535
+ if (isImageResult(msg.result)) {
536
+ console.log('Image versions:', msg.result.metadata?.imageVersions);
537
+ } else if (isVideoResult(msg.result)) {
538
+ console.log('Duration:', msg.result.duration);
539
+ } else if (isZipResult(msg.result)) {
540
+ console.log('Extracted files:', msg.result.extractedMedia.length);
541
+ }
542
+ });
543
+ ```
544
+
545
+ ## Development
546
+
547
+ ### Building
548
+
549
+ ```bash
550
+ cd packages/media-processor-client
551
+ npm install
552
+ npm run build
553
+ ```
554
+
555
+ ### Testing
556
+
557
+ ```bash
558
+ npm test
559
+ ```
560
+
561
+ ### Linting
562
+
563
+ ```bash
564
+ npm run lint
565
+ npm run lint:fix
566
+ ```
567
+
568
+ ## License
569
+
570
+ MIT