@stoked-ui/media 0.1.0-alpha.13.1

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.
Files changed (3) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1193 -0
  3. package/package.json +57 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Call-Em-All
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,1193 @@
1
+ # @stoked-ui/media
2
+
3
+ A comprehensive media management and component library for React with framework-agnostic abstractions, modern file handling APIs, and reactive media UI components.
4
+
5
+ ## Overview
6
+
7
+ The `@stoked-ui/media` package provides production-ready media handling capabilities including:
8
+
9
+ - **Media File Management**: Comprehensive file handling with upload, download, and metadata extraction
10
+ - **React Components**: MediaCard and MediaViewer for displaying and managing media
11
+ - **Modern APIs**: File System Access API integration, Zip compression, and streaming support
12
+ - **Framework-Agnostic Abstractions**: Decouple from routing, authentication, payments, and queue systems
13
+ - **API Client**: Type-safe client for consuming the @stoked-ui/media-api backend
14
+ - **React Hooks**: TanStack Query-based hooks for media CRUD operations
15
+
16
+ ## Table of Contents
17
+
18
+ - [Installation](#installation)
19
+ - [Quick Start](#quick-start)
20
+ - [Core Concepts](#core-concepts)
21
+ - [Components](#components)
22
+ - [MediaCard](#mediacard)
23
+ - [MediaViewer](#mediaviewer)
24
+ - [Media File Management](#media-file-management)
25
+ - [MediaFile Class](#mediafile-class)
26
+ - [WebFile Class](#webfile-class)
27
+ - [File System Access API](#file-system-access-api)
28
+ - [API Client Integration](#api-client-integration)
29
+ - [React Hooks](#react-hooks)
30
+ - [Abstraction Layers](#abstraction-layers)
31
+ - [Advanced Features](#advanced-features)
32
+ - [Metadata Extraction](#metadata-extraction)
33
+ - [Hybrid Metadata Processing](#hybrid-metadata-processing)
34
+ - [Video Sprite Sheet Generation](#video-sprite-sheet-generation)
35
+ - [Performance Optimization](#performance-optimization)
36
+ - [Troubleshooting](#troubleshooting)
37
+ - [FAQ](#faq)
38
+ - [API Reference](#api-reference)
39
+ - [Contributing](#contributing)
40
+ - [License](#license)
41
+
42
+ ## Installation
43
+
44
+ Install the package using your preferred package manager:
45
+
46
+ ```bash
47
+ npm install @stoked-ui/media
48
+ # or
49
+ yarn add @stoked-ui/media
50
+ # or
51
+ pnpm add @stoked-ui/media
52
+ ```
53
+
54
+ ### Peer Dependencies
55
+
56
+ The package requires React 18+ and TanStack Query 5+ as peer dependencies:
57
+
58
+ ```bash
59
+ npm install react@^18.0.0 @tanstack/react-query@^5.0.0
60
+ ```
61
+
62
+ ### Optional Dependencies
63
+
64
+ Some features require additional packages:
65
+
66
+ - **@stoked-ui/common**: Common utilities and components (workspace dependency)
67
+ - **sharp**: For server-side image optimization (optional, for media-api integration)
68
+ - **fluent-ffmpeg**: For video processing (optional, for media-api integration)
69
+
70
+ ## Quick Start
71
+
72
+ ### 1. Basic Setup with API Client (5 minutes)
73
+
74
+ ```tsx
75
+ import { MediaApiProvider, useMediaList } from '@stoked-ui/media';
76
+
77
+ export function App() {
78
+ return (
79
+ <MediaApiProvider config={{
80
+ baseUrl: 'https://api.example.com',
81
+ }}>
82
+ <MediaGallery />
83
+ </MediaApiProvider>
84
+ );
85
+ }
86
+
87
+ function MediaGallery() {
88
+ const { data: mediaList, isLoading } = useMediaList();
89
+
90
+ if (isLoading) return <div>Loading...</div>;
91
+
92
+ return (
93
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px' }}>
94
+ {mediaList?.items.map((item) => (
95
+ <MediaCard key={item._id} item={item} />
96
+ ))}
97
+ </div>
98
+ );
99
+ }
100
+ ```
101
+
102
+ ### 2. MediaCard with Controls
103
+
104
+ ```tsx
105
+ import { MediaCard } from '@stoked-ui/media';
106
+ import { useState } from 'react';
107
+
108
+ export function MyMediaCard() {
109
+ const [modeState, setModeState] = useState({ mode: 'view' });
110
+
111
+ const item = {
112
+ _id: '123',
113
+ title: 'My Video',
114
+ mediaType: 'video',
115
+ file: '/videos/sample.mp4',
116
+ thumbnail: '/thumbnails/sample.jpg',
117
+ duration: 300,
118
+ publicity: 'public',
119
+ };
120
+
121
+ return (
122
+ <MediaCard
123
+ item={item}
124
+ modeState={modeState}
125
+ setModeState={setModeState}
126
+ info={true}
127
+ onViewClick={(item) => console.log('View:', item._id)}
128
+ onDeleteClick={(item) => console.log('Delete:', item._id)}
129
+ />
130
+ );
131
+ }
132
+ ```
133
+
134
+ ### 3. MediaViewer with Queue Navigation
135
+
136
+ ```tsx
137
+ import { MediaViewer } from '@stoked-ui/media';
138
+ import { useState } from 'react';
139
+
140
+ export function MyMediaViewer() {
141
+ const [open, setOpen] = useState(false);
142
+ const [currentIndex, setCurrentIndex] = useState(0);
143
+
144
+ const items = [
145
+ { id: '1', title: 'Video 1', mediaType: 'video' as const, file: '/videos/1.mp4' },
146
+ { id: '2', title: 'Video 2', mediaType: 'video' as const, file: '/videos/2.mp4' },
147
+ ];
148
+
149
+ return (
150
+ <>
151
+ <button onClick={() => setOpen(true)}>Open Viewer</button>
152
+ <MediaViewer
153
+ item={items[currentIndex]}
154
+ mediaItems={items}
155
+ currentIndex={currentIndex}
156
+ open={open}
157
+ onClose={() => setOpen(false)}
158
+ onNavigate={(_, index) => setCurrentIndex(index)}
159
+ enableKeyboardShortcuts
160
+ enableQueue
161
+ />
162
+ </>
163
+ );
164
+ }
165
+ ```
166
+
167
+ ## Core Concepts
168
+
169
+ ### Media Items
170
+
171
+ Media items are objects that represent images, videos, or albums. They contain metadata like title, description, file URL, thumbnail, and optional server-side metadata.
172
+
173
+ ```typescript
174
+ interface ExtendedMediaItem {
175
+ _id?: string;
176
+ title?: string;
177
+ description?: string;
178
+ mediaType?: 'image' | 'video' | 'album';
179
+ file?: string;
180
+ url?: string;
181
+ thumbnail?: string;
182
+ duration?: number;
183
+ views?: number;
184
+ publicity?: 'public' | 'private' | 'paid';
185
+ [key: string]: any;
186
+ }
187
+ ```
188
+
189
+ ### Server vs. Client Metadata
190
+
191
+ The package supports hybrid metadata processing where metadata can come from:
192
+
193
+ 1. **Client-side**: Extracted directly from files using browsers APIs
194
+ 2. **Server-side**: Provided by @stoked-ui/media-api for optimized processing
195
+ 3. **Hybrid**: Cached and updated dynamically
196
+
197
+ See [Hybrid Metadata Processing](#hybrid-metadata-processing) for details.
198
+
199
+ ## Components
200
+
201
+ ### MediaCard
202
+
203
+ An interactive card component for displaying media items in galleries or lists with thumbnail previews, progress tracking, and action controls.
204
+
205
+ **Key Features:**
206
+ - Display images and videos with responsive thumbnails
207
+ - Video progress bar with frame-accurate scrubber sprite support
208
+ - Interactive controls (play, edit, delete, toggle public)
209
+ - Selection mode for batch operations and multi-select
210
+ - Payment integration with price display for paid content
211
+ - Queue management with playback tracking
212
+ - Owner vs. viewer mode detection
213
+ - Grid-responsive design with flexible aspect ratios
214
+ - Lazy loading support for performance
215
+ - MediaClass branding support
216
+
217
+ **Complete Props Reference:**
218
+ ```typescript
219
+ interface MediaCardProps {
220
+ // Required
221
+ item: ExtendedMediaItem; // Media item to display
222
+ modeState: MediaCardModeState; // Current selection mode state
223
+ setModeState: (state) => void; // Update selection mode
224
+
225
+ // Display options
226
+ info?: boolean; // Show info overlay on hover
227
+ minimalMode?: boolean; // Compact display without metadata
228
+ squareMode?: boolean; // Force 1:1 aspect ratio
229
+ displayMode?: MediaCardDisplayMode; // 'owner' or 'viewer'
230
+ isVisible?: boolean; // For lazy loading optimization
231
+
232
+ // Callbacks
233
+ onViewClick?: (item: ExtendedMediaItem) => void;
234
+ onEditClick?: (item: ExtendedMediaItem) => void;
235
+ onDeleteClick?: (item: ExtendedMediaItem) => void;
236
+ onTogglePublic?: (item: ExtendedMediaItem) => void;
237
+ onThumbnailLoaded?: () => void;
238
+ onMediaLoaded?: () => void;
239
+
240
+ // Abstraction layers (framework integration)
241
+ router?: IRouter;
242
+ auth?: IAuth;
243
+ payment?: IPayment;
244
+ queue?: IQueue;
245
+
246
+ // API integration
247
+ apiClient?: MediaApiClient;
248
+ enableServerThumbnails?: boolean; // Use server-generated thumbnails
249
+
250
+ // Styling
251
+ sx?: SxProps<Theme>; // MUI styling
252
+ imageAlt?: string; // Image alt text
253
+ }
254
+ ```
255
+
256
+ **Usage Examples:**
257
+ ```tsx
258
+ // Basic display
259
+ <MediaCard item={media} modeState={modeState} setModeState={setModeState} />
260
+
261
+ // With callbacks
262
+ <MediaCard
263
+ item={media}
264
+ modeState={modeState}
265
+ setModeState={setModeState}
266
+ onViewClick={(item) => navigate(`/media/${item._id}`)}
267
+ onDeleteClick={(item) => deleteMedia(item._id)}
268
+ />
269
+
270
+ // Owner-specific features
271
+ <MediaCard
272
+ item={media}
273
+ displayMode="owner"
274
+ onEditClick={(item) => navigate(`/edit/${item._id}`)}
275
+ onTogglePublic={(item) => updateMedia(item._id, { publicity: togglePublicity(item.publicity) })}
276
+ />
277
+
278
+ // With server thumbnails
279
+ <MediaCard
280
+ item={media}
281
+ enableServerThumbnails
282
+ apiClient={apiClient}
283
+ />
284
+ ```
285
+
286
+ **Documentation:** [MediaCard README](./src/components/MediaCard/README.md)
287
+
288
+ ### MediaViewer
289
+
290
+ A full-screen media viewer component with multiple view modes, playlist navigation, keyboard shortcuts, and adaptive layout.
291
+
292
+ **Key Features:**
293
+ - Multiple view modes (NORMAL, THEATER, FULLSCREEN)
294
+ - Smooth navigation between media items with prev/next controls
295
+ - Queue integration for playlist management with next-up indicators
296
+ - Keyboard shortcuts for accessibility and power users
297
+ - Responsive layout that adapts to viewport and content
298
+ - Preview grid showing upcoming items
299
+ - MediaClass branding with before/after idents
300
+ - Owner controls for editing and deletion
301
+ - API integration for loading full media details
302
+ - Error handling with fallback display
303
+
304
+ **Complete Props Reference:**
305
+ ```typescript
306
+ interface MediaViewerProps {
307
+ // Required
308
+ item: MediaItem; // Current media item
309
+ open: boolean; // Viewer visibility
310
+ onClose: () => void; // Close handler
311
+
312
+ // Navigation
313
+ mediaItems?: MediaItem[]; // Array of all items
314
+ currentIndex?: number; // Current position in array
315
+ onNavigate?: (item: MediaItem, index: number) => void;
316
+
317
+ // Callbacks
318
+ onEdit?: (item: MediaItem) => void;
319
+ onDelete?: (item: MediaItem) => void;
320
+ onMediaLoaded?: (item: MediaItem) => void;
321
+ onMetadataLoaded?: (metadata: Metadata) => void;
322
+
323
+ // Display options
324
+ hideNavbar?: boolean; // Hide toolbar
325
+ showPreviewCards?: boolean; // Show next items preview
326
+ initialMode?: MediaViewerMode; // Starting view mode
327
+ autoplay?: boolean; // Auto-play videos
328
+ initialMuted?: boolean; // Muted on start
329
+
330
+ // Feature toggles
331
+ enableQueue?: boolean; // Enable playlist features
332
+ enableKeyboardShortcuts?: boolean; // Enable keyboard controls
333
+ enableOwnerControls?: boolean; // Show edit/delete buttons
334
+
335
+ // Abstraction layers
336
+ router?: IRouter;
337
+ auth?: IAuth;
338
+ queue?: IQueue;
339
+ keyboard?: IKeyboardShortcuts;
340
+ payment?: IPayment;
341
+
342
+ // API integration
343
+ apiClient?: MediaApiClient;
344
+ enableServerFeatures?: boolean; // Load full details from API
345
+ }
346
+ ```
347
+
348
+ **Usage Examples:**
349
+ ```tsx
350
+ // Basic viewer
351
+ <MediaViewer
352
+ item={currentItem}
353
+ open={isOpen}
354
+ onClose={() => setIsOpen(false)}
355
+ />
356
+
357
+ // With navigation and playlist
358
+ <MediaViewer
359
+ item={items[currentIndex]}
360
+ mediaItems={items}
361
+ currentIndex={currentIndex}
362
+ open={isOpen}
363
+ onClose={() => setIsOpen(false)}
364
+ onNavigate={(item, index) => setCurrentIndex(index)}
365
+ enableQueue
366
+ showPreviewCards
367
+ />
368
+
369
+ // With all features enabled
370
+ <MediaViewer
371
+ item={currentItem}
372
+ mediaItems={allItems}
373
+ currentIndex={currentIndex}
374
+ open={isOpen}
375
+ onClose={() => setIsOpen(false)}
376
+ onNavigate={(item, index) => setCurrentIndex(index)}
377
+ router={useRouter()}
378
+ auth={useAuth()}
379
+ keyboard={useKeyboard()}
380
+ enableQueue
381
+ enableKeyboardShortcuts
382
+ enableOwnerControls
383
+ apiClient={apiClient}
384
+ />
385
+ ```
386
+
387
+ **Keyboard Shortcuts:**
388
+ - `ArrowLeft`: Previous item
389
+ - `ArrowRight`: Next item
390
+ - `f`: Toggle fullscreen
391
+ - `Escape`: Exit fullscreen/close viewer
392
+ - `Space`: Play/pause (video only)
393
+
394
+ **Documentation:** [MediaViewer README](./src/components/MediaViewer/README.md)
395
+
396
+ ## Media File Management
397
+
398
+ ### MediaFile Class
399
+
400
+ The core `MediaFile` class handles file uploads, downloads, and provides a unified interface for working with files.
401
+
402
+ ```typescript
403
+ import { MediaFile, type IMediaFile } from '@stoked-ui/media';
404
+
405
+ // Create from File object
406
+ const mediaFile = new MediaFile(fileInput);
407
+
408
+ // Get file metadata
409
+ console.log(mediaFile.name);
410
+ console.log(mediaFile.size);
411
+ console.log(mediaFile.type);
412
+
413
+ // Check media type
414
+ if (mediaFile.isVideo()) {
415
+ console.log('Video file');
416
+ }
417
+
418
+ // Upload to server
419
+ const response = await mediaFile.upload({
420
+ endpoint: '/api/upload',
421
+ headers: { 'Authorization': 'Bearer token' },
422
+ onProgress: (progress) => console.log(`${progress}%`),
423
+ });
424
+ ```
425
+
426
+ **API Reference:**
427
+ ```typescript
428
+ class MediaFile {
429
+ // Properties
430
+ file: File;
431
+ name: string;
432
+ size: number;
433
+ type: string;
434
+ lastModified: number;
435
+
436
+ // Methods
437
+ isImage(): boolean;
438
+ isVideo(): boolean;
439
+ isAudio(): boolean;
440
+ isDocument(): boolean;
441
+ getMediaType(): MediaType;
442
+
443
+ async upload(options: UploadOptions): Promise<UploadResponse>;
444
+ async download(filename?: string): Promise<void>;
445
+
446
+ // File system operations
447
+ async readAsArrayBuffer(): Promise<ArrayBuffer>;
448
+ async readAsDataURL(): Promise<string>;
449
+ async readAsText(): Promise<string>;
450
+ }
451
+ ```
452
+
453
+ ### WebFile Class
454
+
455
+ The `WebFile` class extends `MediaFile` with web-persistent storage capabilities.
456
+
457
+ ```typescript
458
+ import { WebFile } from '@stoked-ui/media';
459
+
460
+ // Save file to IndexedDB
461
+ const webFile = new WebFile(fileObject);
462
+ const savedId = await webFile.save();
463
+
464
+ // Retrieve file later
465
+ const retrieved = await WebFile.load(savedId);
466
+
467
+ // List all saved files
468
+ const allFiles = await WebFile.listAll();
469
+
470
+ // Delete file from storage
471
+ await webFile.delete();
472
+ ```
473
+
474
+ ### File System Access API
475
+
476
+ Access the modern File System Access API for native file picker and save dialogs:
477
+
478
+ ```typescript
479
+ import { openFileApi, saveFileApi } from '@stoked-ui/media';
480
+
481
+ // Open file picker
482
+ const files = await openFileApi({
483
+ types: [
484
+ {
485
+ description: 'Images',
486
+ accept: { 'image/*': ['.png', '.jpg', '.gif'] }
487
+ },
488
+ {
489
+ description: 'Videos',
490
+ accept: { 'video/*': ['.mp4', '.webm'] }
491
+ }
492
+ ],
493
+ multiple: true
494
+ });
495
+
496
+ // Save file
497
+ const fileHandle = await saveFileApi({
498
+ suggestedName: 'export.json',
499
+ types: [{
500
+ description: 'JSON',
501
+ accept: { 'application/json': ['.json'] }
502
+ }]
503
+ });
504
+ ```
505
+
506
+ **Browser Support:**
507
+ - Chrome/Edge: Yes
508
+ - Firefox: Partial (behind flag)
509
+ - Safari: Limited support
510
+ - Mobile: Limited support
511
+
512
+ Fallback to standard file input if not available.
513
+
514
+ ## API Client Integration
515
+
516
+ ### Creating an API Client
517
+
518
+ ```typescript
519
+ import { createMediaApiClient } from '@stoked-ui/media';
520
+
521
+ const client = createMediaApiClient({
522
+ baseUrl: 'https://api.example.com/v1',
523
+ authToken: 'your-jwt-token',
524
+ timeout: 30000,
525
+ });
526
+
527
+ // Update token dynamically
528
+ client.setAuthToken(newToken);
529
+ ```
530
+
531
+ ### Client Methods
532
+
533
+ ```typescript
534
+ // Get single media item
535
+ const media = await client.getMedia(mediaId);
536
+
537
+ // List media with filters
538
+ const list = await client.listMedia({
539
+ limit: 20,
540
+ offset: 0,
541
+ mediaType: 'video',
542
+ search: 'sunset',
543
+ sortBy: '-createdAt',
544
+ });
545
+
546
+ // Create media entry
547
+ const newMedia = await client.createMedia({
548
+ title: 'My Video',
549
+ description: 'A beautiful sunset',
550
+ mediaType: 'video',
551
+ file: 'https://example.com/sunset.mp4',
552
+ thumbnail: 'https://example.com/sunset.jpg',
553
+ });
554
+
555
+ // Update media
556
+ const updated = await client.updateMedia(mediaId, {
557
+ title: 'Updated Title',
558
+ publicity: 'public',
559
+ });
560
+
561
+ // Delete media
562
+ await client.deleteMedia(mediaId);
563
+ ```
564
+
565
+ ## React Hooks
566
+
567
+ ### useMediaApiProvider
568
+
569
+ Set up the API client context:
570
+
571
+ ```tsx
572
+ import { MediaApiProvider } from '@stoked-ui/media';
573
+
574
+ export function App() {
575
+ return (
576
+ <MediaApiProvider config={{
577
+ baseUrl: 'https://api.example.com',
578
+ authToken: localStorage.getItem('token'),
579
+ }}>
580
+ <YourApp />
581
+ </MediaApiProvider>
582
+ );
583
+ }
584
+ ```
585
+
586
+ ### useMediaList
587
+
588
+ Fetch paginated media with caching:
589
+
590
+ ```tsx
591
+ import { useMediaList } from '@stoked-ui/media';
592
+
593
+ function MediaGallery() {
594
+ const { data, isLoading, error, hasNextPage, fetchNextPage } = useMediaList({
595
+ limit: 20,
596
+ mediaType: 'image',
597
+ });
598
+
599
+ return (
600
+ <div>
601
+ {data?.items.map((item) => (
602
+ <MediaCard key={item._id} item={item} />
603
+ ))}
604
+ {hasNextPage && <button onClick={() => fetchNextPage()}>Load More</button>}
605
+ </div>
606
+ );
607
+ }
608
+ ```
609
+
610
+ ### useMediaItem
611
+
612
+ Fetch a single media item with caching:
613
+
614
+ ```tsx
615
+ const { data: media, isLoading, error } = useMediaItem(mediaId);
616
+ ```
617
+
618
+ ### useMediaUpload
619
+
620
+ Handle file uploads with progress tracking:
621
+
622
+ ```tsx
623
+ const { upload, isUploading, progress, error } = useMediaUpload();
624
+
625
+ const handleUpload = async (file: File) => {
626
+ const result = await upload(file, {
627
+ title: file.name,
628
+ description: 'Auto-uploaded',
629
+ });
630
+ console.log('Uploaded:', result._id);
631
+ };
632
+ ```
633
+
634
+ ### useMediaUpdate
635
+
636
+ Update media metadata:
637
+
638
+ ```tsx
639
+ const { update, isUpdating } = useMediaUpdate();
640
+
641
+ const handleUpdate = async (mediaId: string, updates: Partial<MediaItem>) => {
642
+ await update(mediaId, updates);
643
+ };
644
+ ```
645
+
646
+ ### useMediaDelete
647
+
648
+ Delete media:
649
+
650
+ ```tsx
651
+ const { delete: deleteMedia, isDeleting } = useMediaDelete();
652
+
653
+ const handleDelete = async (mediaId: string) => {
654
+ await deleteMedia(mediaId);
655
+ };
656
+ ```
657
+
658
+ ## Abstraction Layers
659
+
660
+ The package uses framework-agnostic abstraction layers to decouple components from specific implementations. This allows components to work with any routing system, auth provider, payment processor, or queue manager.
661
+
662
+ ### Available Abstractions
663
+
664
+ 1. **Router (`IRouter`)** - Navigation and routing
665
+ 2. **Auth (`IAuth`)** - User authentication and authorization
666
+ 3. **Payment (`IPayment`)** - Payment processing for paid content
667
+ 4. **Queue (`IQueue`)** - Media playback queue management
668
+ 5. **KeyboardShortcuts (`IKeyboardShortcuts`)** - Keyboard event handling
669
+
670
+ ### Using Abstractions
671
+
672
+ Import the abstractions and create implementations for your framework:
673
+
674
+ ```tsx
675
+ import {
676
+ IRouter,
677
+ IAuth,
678
+ IPayment,
679
+ IQueue,
680
+ IKeyboardShortcuts,
681
+ createMockAuth,
682
+ createMockPayment,
683
+ createInMemoryQueue,
684
+ } from '@stoked-ui/media';
685
+
686
+ // Use mock implementations for testing
687
+ const mockAuth = createMockAuth({
688
+ id: 'user-123',
689
+ email: 'user@example.com',
690
+ name: 'John Doe',
691
+ });
692
+
693
+ // Or create custom implementations
694
+ const customRouter: IRouter = {
695
+ navigate: (path: string) => {
696
+ // Custom navigation logic
697
+ },
698
+ currentPath: () => window.location.pathname,
699
+ query: () => new URLSearchParams(window.location.search),
700
+ };
701
+
702
+ // Pass to components
703
+ <MediaCard
704
+ item={mediaItem}
705
+ modeState={modeState}
706
+ setModeState={setModeState}
707
+ router={customRouter}
708
+ auth={mockAuth}
709
+ />
710
+ ```
711
+
712
+ ### No-Op Implementations
713
+
714
+ For optional abstractions, use no-op implementations:
715
+
716
+ ```tsx
717
+ import { noOpRouter, noOpAuth, noOpQueue, noOpPayment, noOpKeyboardShortcuts } from '@stoked-ui/media';
718
+
719
+ <MediaCard
720
+ item={mediaItem}
721
+ modeState={modeState}
722
+ setModeState={setModeState}
723
+ router={noOpRouter}
724
+ auth={noOpAuth}
725
+ />
726
+ ```
727
+
728
+ ## TypeScript Support
729
+
730
+ Full TypeScript support with comprehensive type definitions:
731
+
732
+ ```tsx
733
+ import type {
734
+ MediaCardProps,
735
+ ExtendedMediaItem,
736
+ MediaCardModeState,
737
+ MediaCardDisplayMode,
738
+ MediaViewerProps,
739
+ MediaItem,
740
+ MediaViewerMode,
741
+ } from '@stoked-ui/media';
742
+
743
+ import { MediaCard, MediaViewer } from '@stoked-ui/media';
744
+ ```
745
+
746
+ ## Examples
747
+
748
+ ### Gallery with Owner Controls
749
+
750
+ ```tsx
751
+ import { MediaCard } from '@stoked-ui/media';
752
+ import { useRouter, useAuth } from '@/hooks';
753
+ import { useState } from 'react';
754
+
755
+ export function MediaGallery({ items }) {
756
+ const [modeState, setModeState] = useState({ mode: 'view' });
757
+ const router = useRouter();
758
+ const auth = useAuth();
759
+
760
+ return (
761
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px' }}>
762
+ {items.map((item) => (
763
+ <MediaCard
764
+ key={item._id}
765
+ item={item}
766
+ modeState={modeState}
767
+ setModeState={setModeState}
768
+ router={router}
769
+ auth={auth}
770
+ info={true}
771
+ onViewClick={(item) => router.navigate(`/media/${item._id}`)}
772
+ onEditClick={(item) => router.navigate(`/media/${item._id}/edit`)}
773
+ onDeleteClick={async (item) => {
774
+ if (confirm('Delete this media?')) {
775
+ // API call to delete
776
+ }
777
+ }}
778
+ />
779
+ ))}
780
+ </div>
781
+ );
782
+ }
783
+ ```
784
+
785
+ ### Media Viewer with Queue
786
+
787
+ ```tsx
788
+ import { MediaViewer } from '@stoked-ui/media';
789
+ import { useRouter, useAuth, useQueue, useKeyboardShortcuts } from '@/hooks';
790
+ import { useState } from 'react';
791
+
792
+ export function MediaGalleryViewer({ items, initialIndex }) {
793
+ const [open, setOpen] = useState(false);
794
+ const [currentIndex, setCurrentIndex] = useState(initialIndex || 0);
795
+
796
+ const router = useRouter();
797
+ const auth = useAuth();
798
+ const queue = useQueue();
799
+ const keyboard = useKeyboardShortcuts();
800
+
801
+ const currentItem = items[currentIndex];
802
+
803
+ return (
804
+ <MediaViewer
805
+ item={currentItem}
806
+ mediaItems={items}
807
+ currentIndex={currentIndex}
808
+ open={open}
809
+ onClose={() => setOpen(false)}
810
+ onNavigate={(item, index) => setCurrentIndex(index)}
811
+ router={router}
812
+ auth={auth}
813
+ queue={queue}
814
+ keyboard={keyboard}
815
+ enableQueue={true}
816
+ enableKeyboardShortcuts={true}
817
+ enableOwnerControls={true}
818
+ showPreviewCards={true}
819
+ />
820
+ );
821
+ }
822
+ ```
823
+
824
+ ### Integration with Next.js
825
+
826
+ ```tsx
827
+ // hooks/useMediaAbstractions.ts
828
+ import { useRouter } from 'next/router';
829
+ import { useSession } from 'next-auth/react';
830
+ import type { IRouter, IAuth } from '@stoked-ui/media';
831
+
832
+ export function useMediaRouter(): IRouter {
833
+ const router = useRouter();
834
+ return {
835
+ navigate: (path: string) => router.push(path),
836
+ currentPath: () => router.pathname,
837
+ query: () => new URLSearchParams(),
838
+ };
839
+ }
840
+
841
+ export function useMediaAuth(): IAuth {
842
+ const { data: session } = useSession();
843
+ const user = session?.user;
844
+
845
+ return {
846
+ getCurrentUser: () => user ? {
847
+ id: user.id,
848
+ email: user.email,
849
+ name: user.name,
850
+ } : null,
851
+ isAuthenticated: () => !!session,
852
+ login: () => signIn(),
853
+ logout: () => signOut(),
854
+ hasPermission: (resource, action) => true,
855
+ isOwner: (authorId: string) => user?.id === authorId,
856
+ };
857
+ }
858
+
859
+ // Usage
860
+ import { MediaCard } from '@stoked-ui/media';
861
+ import { useMediaRouter, useMediaAuth } from '@/hooks/useMediaAbstractions';
862
+
863
+ export function MyMediaCard({ item }) {
864
+ const [modeState, setModeState] = useState({ mode: 'view' });
865
+ const router = useMediaRouter();
866
+ const auth = useMediaAuth();
867
+
868
+ return (
869
+ <MediaCard
870
+ item={item}
871
+ modeState={modeState}
872
+ setModeState={setModeState}
873
+ router={router}
874
+ auth={auth}
875
+ />
876
+ );
877
+ }
878
+ ```
879
+
880
+ ## Advanced Features
881
+
882
+ ### Metadata Extraction
883
+
884
+ Extract metadata directly from files in the browser:
885
+
886
+ ```tsx
887
+ import { MediaFile } from '@stoked-ui/media';
888
+
889
+ const file = new MediaFile(inputFile);
890
+
891
+ // For videos
892
+ if (file.isVideo()) {
893
+ const video = document.createElement('video');
894
+ video.src = URL.createObjectURL(file.file);
895
+
896
+ video.onloadedmetadata = () => {
897
+ console.log('Duration:', video.duration);
898
+ console.log('Width:', video.videoWidth);
899
+ console.log('Height:', video.videoHeight);
900
+ };
901
+ }
902
+
903
+ // For images
904
+ if (file.isImage()) {
905
+ const img = new Image();
906
+ img.src = URL.createObjectURL(file.file);
907
+
908
+ img.onload = () => {
909
+ console.log('Width:', img.width);
910
+ console.log('Height:', img.height);
911
+ };
912
+ }
913
+ ```
914
+
915
+ ### Hybrid Metadata Processing
916
+
917
+ The package supports hybrid metadata processing where metadata is fetched from both client and server:
918
+
919
+ ```tsx
920
+ import { useMediaItem, useMediaMetadataCache } from '@stoked-ui/media';
921
+
922
+ function MediaDisplay({ mediaId }) {
923
+ // Fetch from server (with caching)
924
+ const { data: media } = useMediaItem(mediaId);
925
+
926
+ // Use cached metadata or extract from file
927
+ const metadata = useMediaMetadataCache({
928
+ mediaId,
929
+ serverMetadata: media,
930
+ onMetadataExtracted: (extracted) => {
931
+ console.log('Client-extracted metadata:', extracted);
932
+ },
933
+ });
934
+
935
+ return (
936
+ <div>
937
+ <h2>{media?.title}</h2>
938
+ <p>Duration: {metadata?.duration || media?.duration}</p>
939
+ </div>
940
+ );
941
+ }
942
+ ```
943
+
944
+ **Benefits:**
945
+ - **Faster initial load**: Client metadata extracted immediately
946
+ - **Accurate data**: Server metadata used when available
947
+ - **Offline support**: Falls back to client extraction
948
+ - **Progressive enhancement**: Updates as server data loads
949
+
950
+ ### Video Sprite Sheet Generation
951
+
952
+ For better scrubbing experience, video thumbnails are generated as sprite sheets:
953
+
954
+ ```typescript
955
+ interface SpriteConfig {
956
+ totalFrames: number; // Total frames in sprite
957
+ framesPerRow: number; // Frames per row
958
+ frameWidth: number; // Width of each frame
959
+ frameHeight: number; // Height of each frame
960
+ spriteSheetWidth: number; // Total width
961
+ spriteSheetHeight: number; // Total height
962
+ interval: number; // Seconds between frames
963
+ }
964
+ ```
965
+
966
+ The server generates these automatically. Access via:
967
+
968
+ ```tsx
969
+ <MediaCard
970
+ item={{
971
+ ...media,
972
+ scrubberGenerated: true,
973
+ scrubberSprite: 'https://api.example.com/sprites/123.jpg',
974
+ scrubberSpriteConfig: {
975
+ totalFrames: 100,
976
+ framesPerRow: 10,
977
+ frameWidth: 160,
978
+ frameHeight: 90,
979
+ // ...
980
+ }
981
+ }}
982
+ />
983
+ ```
984
+
985
+ ### Performance Optimization
986
+
987
+ **Lazy Loading:**
988
+ ```tsx
989
+ const isVisible = useInView(ref);
990
+
991
+ <MediaCard
992
+ item={media}
993
+ isVisible={isVisible}
994
+ // Only loads thumbnail when visible
995
+ />
996
+ ```
997
+
998
+ **Memoization:**
999
+ ```tsx
1000
+ import { useMemo } from 'react';
1001
+ import { useMediaViewerLayout } from '@stoked-ui/media';
1002
+
1003
+ const layout = useMediaViewerLayout({
1004
+ item,
1005
+ mediaItems,
1006
+ currentIndex,
1007
+ // Memoized and recalculated only when deps change
1008
+ });
1009
+ ```
1010
+
1011
+ **Image Optimization:**
1012
+ - Use server-generated thumbnails when possible
1013
+ - Set appropriate responsive image sizes
1014
+ - Enable AVIF/WebP format support via picture element
1015
+
1016
+ ## Troubleshooting
1017
+
1018
+ ### Common Issues
1019
+
1020
+ **Issue: "MediaApiProvider not found" error**
1021
+
1022
+ Make sure to wrap your app with `MediaApiProvider`:
1023
+
1024
+ ```tsx
1025
+ import { MediaApiProvider } from '@stoked-ui/media';
1026
+
1027
+ export function App() {
1028
+ return (
1029
+ <MediaApiProvider config={{ baseUrl: 'https://api.example.com' }}>
1030
+ <YourApp />
1031
+ </MediaApiProvider>
1032
+ );
1033
+ }
1034
+ ```
1035
+
1036
+ **Issue: Component renders but media doesn't load**
1037
+
1038
+ Check that:
1039
+ 1. The media file URL is accessible (CORS headers correct)
1040
+ 2. The file format is supported by the browser
1041
+ 3. The `mediaType` is set correctly (`'image'` or `'video'`)
1042
+ 4. Network request is successful (check DevTools Network tab)
1043
+
1044
+ **Issue: Thumbnails not showing**
1045
+
1046
+ Solutions:
1047
+ 1. Ensure thumbnail URL is correct
1048
+ 2. Check CORS headers on image server
1049
+ 3. Verify image format is supported (JPEG, PNG, WebP)
1050
+ 4. Use server-side thumbnail generation for consistency
1051
+
1052
+ **Issue: Video won't play**
1053
+
1054
+ Check:
1055
+ 1. Video format is supported (MP4/H.264, WebM/VP9)
1056
+ 2. Video codec is compatible
1057
+ 3. Server supports range requests for scrubbing
1058
+ 4. CORS headers allow video access
1059
+
1060
+ **Issue: Performance degradation with large galleries**
1061
+
1062
+ Solutions:
1063
+ 1. Use `isVisible` prop with virtualization
1064
+ 2. Implement pagination instead of infinite scroll
1065
+ 3. Use server-side thumbnail generation
1066
+ 4. Enable browser caching headers
1067
+
1068
+ ### Debug Mode
1069
+
1070
+ Enable debug logging:
1071
+
1072
+ ```tsx
1073
+ import { MediaApiProvider } from '@stoked-ui/media';
1074
+
1075
+ <MediaApiProvider
1076
+ config={{ baseUrl: 'https://api.example.com' }}
1077
+ debug={true}
1078
+ >
1079
+ <App />
1080
+ </MediaApiProvider>
1081
+ ```
1082
+
1083
+ ## FAQ
1084
+
1085
+ **Q: Does this work without the @stoked-ui/media-api backend?**
1086
+
1087
+ A: Yes! The components work standalone. Just provide URLs for files and thumbnails. The API client is optional for managing media metadata.
1088
+
1089
+ **Q: Can I use this with a different backend?**
1090
+
1091
+ A: Yes! Implement the `MediaApiClient` interface or use the generic client with your own endpoints.
1092
+
1093
+ **Q: How do I handle authentication?**
1094
+
1095
+ A: Use the `IAuth` abstraction:
1096
+
1097
+ ```tsx
1098
+ const auth: IAuth = {
1099
+ getCurrentUser: () => ({ id: 'user-1', email: 'user@example.com' }),
1100
+ isAuthenticated: () => !!localStorage.getItem('token'),
1101
+ login: () => redirectToLogin(),
1102
+ logout: () => clearToken(),
1103
+ hasPermission: (resource, action) => true,
1104
+ isOwner: (authorId) => authorId === currentUserId,
1105
+ };
1106
+
1107
+ <MediaCard item={media} auth={auth} />
1108
+ ```
1109
+
1110
+ **Q: How do I implement payment integration?**
1111
+
1112
+ A: Use the `IPayment` abstraction:
1113
+
1114
+ ```tsx
1115
+ const payment: IPayment = {
1116
+ initiatePayment: async (options) => {
1117
+ // Call your payment processor (Stripe, PayPal, etc.)
1118
+ return { status: 'completed', transactionId: '...' };
1119
+ },
1120
+ verifyPayment: async (transactionId) => true,
1121
+ };
1122
+
1123
+ <MediaCard item={paidMedia} payment={payment} />
1124
+ ```
1125
+
1126
+ **Q: Can I customize the UI?**
1127
+
1128
+ A: Use MUI's `sx` prop for styling:
1129
+
1130
+ ```tsx
1131
+ <MediaCard
1132
+ item={media}
1133
+ sx={{
1134
+ '& .media-card': { backgroundColor: '#f5f5f5' },
1135
+ '& .media-title': { fontSize: '1.25rem' },
1136
+ }}
1137
+ />
1138
+ ```
1139
+
1140
+ **Q: How do I implement queue management?**
1141
+
1142
+ A: Use the `IQueue` abstraction:
1143
+
1144
+ ```tsx
1145
+ const queue: IQueue = {
1146
+ items: mediaList,
1147
+ add: (item) => { /* add to queue */ },
1148
+ remove: (itemId) => { /* remove from queue */ },
1149
+ clear: () => { /* clear queue */ },
1150
+ next: () => currentItem,
1151
+ previous: () => previousItem,
1152
+ };
1153
+
1154
+ <MediaViewer item={current} queue={queue} enableQueue />
1155
+ ```
1156
+
1157
+ ## API Reference
1158
+
1159
+ ### Type Definitions
1160
+
1161
+ **ExtendedMediaItem** - Represents a media file with metadata
1162
+
1163
+ **MediaCardProps** - Props for MediaCard component
1164
+
1165
+ **MediaViewerProps** - Props for MediaViewer component
1166
+
1167
+ **IRouter** - Abstract router interface
1168
+
1169
+ **IAuth** - Abstract auth interface
1170
+
1171
+ **IPayment** - Abstract payment interface
1172
+
1173
+ **IQueue** - Abstract queue interface
1174
+
1175
+ **IKeyboardShortcuts** - Abstract keyboard shortcuts interface
1176
+
1177
+ See [src/components/MediaCard/README.md](./src/components/MediaCard/README.md) and [src/components/MediaViewer/README.md](./src/components/MediaViewer/README.md) for comprehensive documentation.
1178
+
1179
+ ## Storybook
1180
+
1181
+ The package includes comprehensive Storybook stories demonstrating all features. Run:
1182
+
1183
+ ```bash
1184
+ npm run storybook
1185
+ ```
1186
+
1187
+ ## Contributing
1188
+
1189
+ Contributions are welcome! Please follow the [Conventional Commits](https://conventionalcommits.org) specification.
1190
+
1191
+ ## License
1192
+
1193
+ MIT - See LICENSE file for details
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@stoked-ui/media",
3
+ "author": "Brian Stoker",
4
+ "private": false,
5
+ "version": "0.1.0-alpha.13.1",
6
+ "description": "Comprehensive media management and component library for React with framework-agnostic abstractions, modern file handling APIs, and reactive media UI components",
7
+ "keywords": [
8
+ "react",
9
+ "media",
10
+ "video",
11
+ "image",
12
+ "upload",
13
+ "player",
14
+ "gallery",
15
+ "mui",
16
+ "material-ui",
17
+ "components",
18
+ "file-handling",
19
+ "tanstack-query",
20
+ "api-client",
21
+ "metadata",
22
+ "thumbnails"
23
+ ],
24
+ "license": "MIT",
25
+ "main": "./node/index.js",
26
+ "files": [
27
+ "build"
28
+ ],
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/stoked-ui/sui",
32
+ "directory": "packages/sui-media"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public",
36
+ "directory": "build"
37
+ },
38
+ "sideEffects": false,
39
+ "peerDependencies": {
40
+ "@stoked-ui/common": "0.1.3-a.0",
41
+ "@tanstack/react-query": "^5.0.0",
42
+ "react": "^18.0.0",
43
+ "react-dom": "^18.0.0"
44
+ },
45
+ "dependencies": {
46
+ "@emotion/react": "^11.11.4",
47
+ "@emotion/styled": "^11.11.5",
48
+ "@mui/icons-material": "^5.15.20",
49
+ "@mui/lab": "^5.0.0-alpha.170",
50
+ "@mui/material": "^5.15.20",
51
+ "formdata-node": "^6.0.3",
52
+ "jszip": "^3.10.1",
53
+ "@tanstack/react-query": "^5.0.0"
54
+ },
55
+ "module": "./index.js",
56
+ "types": "./index.d.ts"
57
+ }