@rajeev02/media 0.1.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.
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @rajeev02/media — Download Manager
3
+ * Offline media downloads with quality selection, pause/resume, storage management
4
+ */
5
+ export type DownloadState = "queued" | "downloading" | "paused" | "completed" | "failed" | "cancelled";
6
+ export interface DownloadRequest {
7
+ id: string;
8
+ uri: string;
9
+ title: string;
10
+ qualityId?: string;
11
+ /** Estimated size in bytes */
12
+ estimatedSizeBytes?: number;
13
+ /** Expiry time for downloaded content (DRM) */
14
+ expiresAt?: number;
15
+ metadata?: Record<string, unknown>;
16
+ }
17
+ export interface DownloadProgress {
18
+ id: string;
19
+ state: DownloadState;
20
+ bytesDownloaded: number;
21
+ totalBytes: number;
22
+ percentComplete: number;
23
+ speedBps: number;
24
+ estimatedRemainingMs: number;
25
+ }
26
+ /**
27
+ * Download Manager — tracks offline media downloads
28
+ */
29
+ export declare class DownloadManager {
30
+ private downloads;
31
+ private listeners;
32
+ private maxConcurrent;
33
+ private maxStorageBytes;
34
+ constructor(maxConcurrent?: number, maxStorageBytes?: number);
35
+ /** Queue a download */
36
+ enqueue(request: DownloadRequest): boolean;
37
+ /** Pause a download */
38
+ pause(id: string): boolean;
39
+ /** Resume a download */
40
+ resume(id: string): boolean;
41
+ /** Cancel and remove a download */
42
+ cancel(id: string): boolean;
43
+ /** Update progress (called by native downloader) */
44
+ updateProgress(id: string, bytesDownloaded: number, totalBytes: number): void;
45
+ /** Get all downloads */
46
+ getAll(): (DownloadRequest & {
47
+ state: DownloadState;
48
+ })[];
49
+ /** Get completed downloads */
50
+ getCompleted(): DownloadRequest[];
51
+ /** Get total storage used by completed downloads */
52
+ getTotalStorageUsed(): number;
53
+ /** Get max storage in bytes */
54
+ getMaxStorage(): number;
55
+ /** Delete a completed download */
56
+ deleteDownload(id: string): boolean;
57
+ /** Clear all completed downloads */
58
+ clearCompleted(): number;
59
+ /** Subscribe to progress updates */
60
+ onProgress(listener: (id: string, progress: DownloadProgress) => void): () => void;
61
+ private notify;
62
+ }
63
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/download/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,aAAa,GACrB,QAAQ,GACR,aAAa,GACb,QAAQ,GACR,WAAW,GACX,QAAQ,GACR,WAAW,CAAC;AAEhB,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8BAA8B;IAC9B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,aAAa,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED;;GAEG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,SAAS,CAQH;IACd,OAAO,CAAC,SAAS,CACL;IACZ,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,eAAe,CAAS;gBAG9B,aAAa,GAAE,MAAU,EACzB,eAAe,GAAE,MAA+B;IAMlD,uBAAuB;IACvB,OAAO,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO;IAkB1C,uBAAuB;IACvB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAS1B,wBAAwB;IACxB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAS3B,mCAAmC;IACnC,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAU3B,oDAAoD;IACpD,cAAc,CACZ,EAAE,EAAE,MAAM,EACV,eAAe,EAAE,MAAM,EACvB,UAAU,EAAE,MAAM,GACjB,IAAI;IA+BP,wBAAwB;IACxB,MAAM,IAAI,CAAC,eAAe,GAAG;QAAE,KAAK,EAAE,aAAa,CAAA;KAAE,CAAC,EAAE;IAIxD,8BAA8B;IAC9B,YAAY,IAAI,eAAe,EAAE;IAMjC,oDAAoD;IACpD,mBAAmB,IAAI,MAAM;IAM7B,+BAA+B;IAC/B,aAAa,IAAI,MAAM;IAIvB,kCAAkC;IAClC,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAInC,oCAAoC;IACpC,cAAc,IAAI,MAAM;IAWxB,oCAAoC;IACpC,UAAU,CACR,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB,KAAK,IAAI,GACzD,MAAM,IAAI;IAKb,OAAO,CAAC,MAAM;CAOf"}
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ /**
3
+ * @rajeev02/media — Download Manager
4
+ * Offline media downloads with quality selection, pause/resume, storage management
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.DownloadManager = void 0;
8
+ /**
9
+ * Download Manager — tracks offline media downloads
10
+ */
11
+ class DownloadManager {
12
+ constructor(maxConcurrent = 2, maxStorageBytes = 2 * 1024 * 1024 * 1024) {
13
+ this.downloads = new Map();
14
+ this.listeners = new Set();
15
+ this.maxConcurrent = maxConcurrent;
16
+ this.maxStorageBytes = maxStorageBytes;
17
+ }
18
+ /** Queue a download */
19
+ enqueue(request) {
20
+ if (this.downloads.has(request.id))
21
+ return false;
22
+ if (this.getTotalStorageUsed() + (request.estimatedSizeBytes ?? 0) >
23
+ this.maxStorageBytes)
24
+ return false;
25
+ this.downloads.set(request.id, {
26
+ ...request,
27
+ state: "queued",
28
+ bytesDownloaded: 0,
29
+ totalBytes: request.estimatedSizeBytes ?? 0,
30
+ startedAt: Date.now(),
31
+ });
32
+ return true;
33
+ }
34
+ /** Pause a download */
35
+ pause(id) {
36
+ const dl = this.downloads.get(id);
37
+ if (dl && dl.state === "downloading") {
38
+ dl.state = "paused";
39
+ return true;
40
+ }
41
+ return false;
42
+ }
43
+ /** Resume a download */
44
+ resume(id) {
45
+ const dl = this.downloads.get(id);
46
+ if (dl && dl.state === "paused") {
47
+ dl.state = "queued";
48
+ return true;
49
+ }
50
+ return false;
51
+ }
52
+ /** Cancel and remove a download */
53
+ cancel(id) {
54
+ const dl = this.downloads.get(id);
55
+ if (dl) {
56
+ dl.state = "cancelled";
57
+ this.downloads.delete(id);
58
+ return true;
59
+ }
60
+ return false;
61
+ }
62
+ /** Update progress (called by native downloader) */
63
+ updateProgress(id, bytesDownloaded, totalBytes) {
64
+ const dl = this.downloads.get(id);
65
+ if (!dl)
66
+ return;
67
+ dl.bytesDownloaded = bytesDownloaded;
68
+ dl.totalBytes = totalBytes;
69
+ dl.state = "downloading";
70
+ const elapsed = Date.now() - dl.startedAt;
71
+ const speed = elapsed > 0 ? (bytesDownloaded / elapsed) * 1000 : 0;
72
+ const remaining = speed > 0 ? ((totalBytes - bytesDownloaded) / speed) * 1000 : 0;
73
+ const progress = {
74
+ id,
75
+ state: "downloading",
76
+ bytesDownloaded,
77
+ totalBytes,
78
+ percentComplete: totalBytes > 0 ? (bytesDownloaded / totalBytes) * 100 : 0,
79
+ speedBps: speed,
80
+ estimatedRemainingMs: remaining,
81
+ };
82
+ if (bytesDownloaded >= totalBytes) {
83
+ dl.state = "completed";
84
+ progress.state = "completed";
85
+ progress.percentComplete = 100;
86
+ }
87
+ this.notify(id, progress);
88
+ }
89
+ /** Get all downloads */
90
+ getAll() {
91
+ return Array.from(this.downloads.values());
92
+ }
93
+ /** Get completed downloads */
94
+ getCompleted() {
95
+ return Array.from(this.downloads.values()).filter((d) => d.state === "completed");
96
+ }
97
+ /** Get total storage used by completed downloads */
98
+ getTotalStorageUsed() {
99
+ return Array.from(this.downloads.values())
100
+ .filter((d) => d.state === "completed")
101
+ .reduce((sum, d) => sum + d.totalBytes, 0);
102
+ }
103
+ /** Get max storage in bytes */
104
+ getMaxStorage() {
105
+ return this.maxStorageBytes;
106
+ }
107
+ /** Delete a completed download */
108
+ deleteDownload(id) {
109
+ return this.downloads.delete(id);
110
+ }
111
+ /** Clear all completed downloads */
112
+ clearCompleted() {
113
+ let count = 0;
114
+ for (const [id, dl] of this.downloads) {
115
+ if (dl.state === "completed") {
116
+ this.downloads.delete(id);
117
+ count++;
118
+ }
119
+ }
120
+ return count;
121
+ }
122
+ /** Subscribe to progress updates */
123
+ onProgress(listener) {
124
+ this.listeners.add(listener);
125
+ return () => this.listeners.delete(listener);
126
+ }
127
+ notify(id, progress) {
128
+ for (const l of this.listeners) {
129
+ try {
130
+ l(id, progress);
131
+ }
132
+ catch { }
133
+ }
134
+ }
135
+ }
136
+ exports.DownloadManager = DownloadManager;
137
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/download/index.ts"],"names":[],"mappings":"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"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @rajeev02/media
3
+ * Unified Media Player Engine
4
+ * Adaptive streaming, PiP, offline download, DRM, quality selection
5
+ *
6
+ * @author Rajeev Kumar Joshi
7
+ * @license MIT
8
+ */
9
+ export { MediaPlayerController } from "./player";
10
+ export type { MediaSource, DrmConfig, QualityLevel, PlaybackState, PlayerConfig, PlayerEvent, PlayerState, MediaType, } from "./player";
11
+ export { DownloadManager } from "./download";
12
+ export type { DownloadRequest, DownloadProgress, DownloadState, } from "./download";
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACjD,YAAY,EACV,WAAW,EACX,SAAS,EACT,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,WAAW,EACX,WAAW,EACX,SAAS,GACV,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,YAAY,EACV,eAAe,EACf,gBAAgB,EAChB,aAAa,GACd,MAAM,YAAY,CAAC"}
package/lib/index.js ADDED
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DownloadManager = exports.MediaPlayerController = void 0;
4
+ /**
5
+ * @rajeev02/media
6
+ * Unified Media Player Engine
7
+ * Adaptive streaming, PiP, offline download, DRM, quality selection
8
+ *
9
+ * @author Rajeev Kumar Joshi
10
+ * @license MIT
11
+ */
12
+ var player_1 = require("./player");
13
+ Object.defineProperty(exports, "MediaPlayerController", { enumerable: true, get: function () { return player_1.MediaPlayerController; } });
14
+ var download_1 = require("./download");
15
+ Object.defineProperty(exports, "DownloadManager", { enumerable: true, get: function () { return download_1.DownloadManager; } });
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA;;;;;;;GAOG;AACH,mCAAiD;AAAxC,+GAAA,qBAAqB,OAAA;AAY9B,uCAA6C;AAApC,2GAAA,eAAe,OAAA"}
@@ -0,0 +1,166 @@
1
+ /**
2
+ * @rajeev02/media — Player Core
3
+ * State machine for media playback — controls, events, quality selection, PiP, casting
4
+ */
5
+ export type PlayerState = "idle" | "loading" | "ready" | "playing" | "paused" | "buffering" | "ended" | "error";
6
+ export type MediaType = "video" | "audio" | "live_video" | "live_audio";
7
+ export interface MediaSource {
8
+ uri: string;
9
+ type: MediaType;
10
+ title?: string;
11
+ subtitle?: string;
12
+ thumbnailUri?: string;
13
+ durationMs?: number;
14
+ /** DRM configuration */
15
+ drm?: DrmConfig;
16
+ /** Available quality levels (for adaptive streaming) */
17
+ qualities?: QualityLevel[];
18
+ /** Headers for authenticated streams */
19
+ headers?: Record<string, string>;
20
+ /** Offline download ID (if playing from local storage) */
21
+ offlineId?: string;
22
+ }
23
+ export interface DrmConfig {
24
+ type: "widevine" | "fairplay" | "clearkey";
25
+ licenseServerUrl: string;
26
+ headers?: Record<string, string>;
27
+ certificateUrl?: string;
28
+ }
29
+ export interface QualityLevel {
30
+ id: string;
31
+ label: string;
32
+ width: number;
33
+ height: number;
34
+ bitrate: number;
35
+ codec?: string;
36
+ }
37
+ export interface PlaybackState {
38
+ state: PlayerState;
39
+ currentTimeMs: number;
40
+ durationMs: number;
41
+ bufferedMs: number;
42
+ playbackRate: number;
43
+ volume: number;
44
+ muted: boolean;
45
+ isPiP: boolean;
46
+ isCasting: boolean;
47
+ isFullscreen: boolean;
48
+ currentQuality: QualityLevel | null;
49
+ autoQuality: boolean;
50
+ error?: string;
51
+ }
52
+ export interface PlayerConfig {
53
+ /** Auto-play when source is loaded */
54
+ autoPlay?: boolean;
55
+ /** Loop playback */
56
+ loop?: boolean;
57
+ /** Start at specific position (ms) — for resume */
58
+ startPositionMs?: number;
59
+ /** Max buffer duration (ms). Default: 30000 */
60
+ maxBufferMs?: number;
61
+ /** Min buffer before playback starts (ms). Default: 5000 */
62
+ minBufferMs?: number;
63
+ /** Preferred quality level (or 'auto') */
64
+ preferredQuality?: string;
65
+ /** Enable PiP support */
66
+ enablePiP?: boolean;
67
+ /** Enable casting support (Chromecast/AirPlay) */
68
+ enableCast?: boolean;
69
+ /** Background audio playback */
70
+ backgroundAudio?: boolean;
71
+ }
72
+ export type PlayerEvent = {
73
+ type: "stateChange";
74
+ state: PlayerState;
75
+ } | {
76
+ type: "progress";
77
+ currentTimeMs: number;
78
+ durationMs: number;
79
+ bufferedMs: number;
80
+ } | {
81
+ type: "qualityChange";
82
+ quality: QualityLevel;
83
+ } | {
84
+ type: "error";
85
+ code: string;
86
+ message: string;
87
+ } | {
88
+ type: "ended";
89
+ } | {
90
+ type: "pipChange";
91
+ active: boolean;
92
+ } | {
93
+ type: "castChange";
94
+ active: boolean;
95
+ deviceName?: string;
96
+ } | {
97
+ type: "seekComplete";
98
+ positionMs: number;
99
+ };
100
+ /**
101
+ * Media Player Controller
102
+ * Abstract player that works with any native backend (ExoPlayer, AVPlayer, web <video>)
103
+ */
104
+ export declare class MediaPlayerController {
105
+ private source;
106
+ private config;
107
+ private state;
108
+ private listeners;
109
+ private watchHistory;
110
+ constructor(config?: PlayerConfig);
111
+ /** Load a media source */
112
+ load(source: MediaSource): void;
113
+ /** Called by native layer when media is ready */
114
+ onReady(durationMs: number, qualities?: QualityLevel[]): void;
115
+ /** Play */
116
+ play(): void;
117
+ /** Pause */
118
+ pause(): void;
119
+ /** Seek to position */
120
+ seek(positionMs: number): void;
121
+ /** Skip forward */
122
+ skipForward(ms?: number): void;
123
+ /** Skip backward */
124
+ skipBackward(ms?: number): void;
125
+ /** Set playback rate */
126
+ setRate(rate: number): void;
127
+ /** Set volume (0.0 - 1.0) */
128
+ setVolume(volume: number): void;
129
+ /** Toggle mute */
130
+ toggleMute(): void;
131
+ /** Toggle fullscreen */
132
+ toggleFullscreen(): void;
133
+ /** Enter PiP */
134
+ enterPiP(): void;
135
+ /** Exit PiP */
136
+ exitPiP(): void;
137
+ /** Select quality level */
138
+ setQuality(qualityId: string): void;
139
+ /** Get available quality levels */
140
+ getQualities(): QualityLevel[];
141
+ /** Update progress (called by native layer on timer) */
142
+ onProgress(currentTimeMs: number, bufferedMs: number): void;
143
+ /** Called when playback ends */
144
+ onEnded(): void;
145
+ /** Called on error */
146
+ onError(code: string, message: string): void;
147
+ /** Get current playback state */
148
+ getState(): PlaybackState;
149
+ getSource(): MediaSource | null;
150
+ getConfig(): PlayerConfig;
151
+ /** Get watch progress percentage (0-100) */
152
+ getProgressPercent(): number;
153
+ /** Get resume position for a URI */
154
+ getResumePosition(uri: string): number;
155
+ /** Subscribe to events */
156
+ on(listener: (event: PlayerEvent) => void): () => void;
157
+ /** Destroy player */
158
+ destroy(): void;
159
+ /** Suggest quality based on network bandwidth */
160
+ static suggestQuality(qualities: QualityLevel[], bandwidthKbps: number): QualityLevel | null;
161
+ private saveProgress;
162
+ private setState;
163
+ private emit;
164
+ private createInitialState;
165
+ }
166
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/player/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+ /**
3
+ * @rajeev02/media — Player Core
4
+ * State machine for media playback — controls, events, quality selection, PiP, casting
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.MediaPlayerController = void 0;
8
+ /**
9
+ * Media Player Controller
10
+ * Abstract player that works with any native backend (ExoPlayer, AVPlayer, web <video>)
11
+ */
12
+ class MediaPlayerController {
13
+ constructor(config = {}) {
14
+ this.source = null;
15
+ this.listeners = new Set();
16
+ this.watchHistory = new Map(); // uri → last position
17
+ this.config = {
18
+ autoPlay: false,
19
+ loop: false,
20
+ startPositionMs: 0,
21
+ maxBufferMs: 30000,
22
+ minBufferMs: 5000,
23
+ preferredQuality: "auto",
24
+ enablePiP: true,
25
+ enableCast: false,
26
+ backgroundAudio: false,
27
+ ...config,
28
+ };
29
+ this.state = this.createInitialState();
30
+ }
31
+ /** Load a media source */
32
+ load(source) {
33
+ this.source = source;
34
+ this.setState("loading");
35
+ // Check for resume position
36
+ const savedPos = this.watchHistory.get(source.uri);
37
+ if (savedPos && savedPos > 0 && !this.config.startPositionMs) {
38
+ this.config.startPositionMs = savedPos;
39
+ }
40
+ this.emit({ type: "stateChange", state: "loading" });
41
+ }
42
+ /** Called by native layer when media is ready */
43
+ onReady(durationMs, qualities) {
44
+ this.state.durationMs = durationMs;
45
+ if (qualities && this.source) {
46
+ this.source.qualities = qualities;
47
+ }
48
+ this.setState("ready");
49
+ if (this.config.autoPlay)
50
+ this.play();
51
+ }
52
+ /** Play */
53
+ play() {
54
+ if (this.state.state === "ended" && this.config.loop) {
55
+ this.seek(0);
56
+ }
57
+ this.setState("playing");
58
+ }
59
+ /** Pause */
60
+ pause() {
61
+ this.saveProgress();
62
+ this.setState("paused");
63
+ }
64
+ /** Seek to position */
65
+ seek(positionMs) {
66
+ this.state.currentTimeMs = Math.max(0, Math.min(positionMs, this.state.durationMs));
67
+ this.emit({ type: "seekComplete", positionMs: this.state.currentTimeMs });
68
+ }
69
+ /** Skip forward */
70
+ skipForward(ms = 10000) {
71
+ this.seek(this.state.currentTimeMs + ms);
72
+ }
73
+ /** Skip backward */
74
+ skipBackward(ms = 10000) {
75
+ this.seek(this.state.currentTimeMs - ms);
76
+ }
77
+ /** Set playback rate */
78
+ setRate(rate) {
79
+ this.state.playbackRate = Math.max(0.25, Math.min(4, rate));
80
+ }
81
+ /** Set volume (0.0 - 1.0) */
82
+ setVolume(volume) {
83
+ this.state.volume = Math.max(0, Math.min(1, volume));
84
+ }
85
+ /** Toggle mute */
86
+ toggleMute() {
87
+ this.state.muted = !this.state.muted;
88
+ }
89
+ /** Toggle fullscreen */
90
+ toggleFullscreen() {
91
+ this.state.isFullscreen = !this.state.isFullscreen;
92
+ }
93
+ /** Enter PiP */
94
+ enterPiP() {
95
+ if (this.config.enablePiP) {
96
+ this.state.isPiP = true;
97
+ this.emit({ type: "pipChange", active: true });
98
+ }
99
+ }
100
+ /** Exit PiP */
101
+ exitPiP() {
102
+ this.state.isPiP = false;
103
+ this.emit({ type: "pipChange", active: false });
104
+ }
105
+ /** Select quality level */
106
+ setQuality(qualityId) {
107
+ if (qualityId === "auto") {
108
+ this.state.autoQuality = true;
109
+ this.state.currentQuality = null;
110
+ return;
111
+ }
112
+ const quality = this.source?.qualities?.find((q) => q.id === qualityId);
113
+ if (quality) {
114
+ this.state.autoQuality = false;
115
+ this.state.currentQuality = quality;
116
+ this.emit({ type: "qualityChange", quality });
117
+ }
118
+ }
119
+ /** Get available quality levels */
120
+ getQualities() {
121
+ return this.source?.qualities ?? [];
122
+ }
123
+ /** Update progress (called by native layer on timer) */
124
+ onProgress(currentTimeMs, bufferedMs) {
125
+ this.state.currentTimeMs = currentTimeMs;
126
+ this.state.bufferedMs = bufferedMs;
127
+ this.emit({
128
+ type: "progress",
129
+ currentTimeMs,
130
+ durationMs: this.state.durationMs,
131
+ bufferedMs,
132
+ });
133
+ }
134
+ /** Called when playback ends */
135
+ onEnded() {
136
+ this.setState("ended");
137
+ this.watchHistory.delete(this.source?.uri ?? "");
138
+ this.emit({ type: "ended" });
139
+ if (this.config.loop)
140
+ this.play();
141
+ }
142
+ /** Called on error */
143
+ onError(code, message) {
144
+ this.setState("error");
145
+ this.state.error = message;
146
+ this.emit({ type: "error", code, message });
147
+ }
148
+ /** Get current playback state */
149
+ getState() {
150
+ return { ...this.state };
151
+ }
152
+ getSource() {
153
+ return this.source;
154
+ }
155
+ getConfig() {
156
+ return { ...this.config };
157
+ }
158
+ /** Get watch progress percentage (0-100) */
159
+ getProgressPercent() {
160
+ if (this.state.durationMs === 0)
161
+ return 0;
162
+ return (this.state.currentTimeMs / this.state.durationMs) * 100;
163
+ }
164
+ /** Get resume position for a URI */
165
+ getResumePosition(uri) {
166
+ return this.watchHistory.get(uri) ?? 0;
167
+ }
168
+ /** Subscribe to events */
169
+ on(listener) {
170
+ this.listeners.add(listener);
171
+ return () => this.listeners.delete(listener);
172
+ }
173
+ /** Destroy player */
174
+ destroy() {
175
+ this.saveProgress();
176
+ this.source = null;
177
+ this.setState("idle");
178
+ this.listeners.clear();
179
+ }
180
+ /** Suggest quality based on network bandwidth */
181
+ static suggestQuality(qualities, bandwidthKbps) {
182
+ const bandwidthBps = bandwidthKbps * 1000;
183
+ // Pick highest quality that fits within 80% of available bandwidth
184
+ const suitable = qualities
185
+ .filter((q) => q.bitrate <= bandwidthBps * 0.8)
186
+ .sort((a, b) => b.bitrate - a.bitrate);
187
+ return suitable[0] ?? qualities[qualities.length - 1] ?? null;
188
+ }
189
+ saveProgress() {
190
+ if (this.source && this.state.currentTimeMs > 5000) {
191
+ this.watchHistory.set(this.source.uri, this.state.currentTimeMs);
192
+ }
193
+ }
194
+ setState(state) {
195
+ this.state.state = state;
196
+ this.emit({ type: "stateChange", state });
197
+ }
198
+ emit(event) {
199
+ for (const l of this.listeners) {
200
+ try {
201
+ l(event);
202
+ }
203
+ catch { }
204
+ }
205
+ }
206
+ createInitialState() {
207
+ return {
208
+ state: "idle",
209
+ currentTimeMs: 0,
210
+ durationMs: 0,
211
+ bufferedMs: 0,
212
+ playbackRate: 1,
213
+ volume: 1,
214
+ muted: false,
215
+ isPiP: false,
216
+ isCasting: false,
217
+ isFullscreen: false,
218
+ currentQuality: null,
219
+ autoQuality: true,
220
+ };
221
+ }
222
+ }
223
+ exports.MediaPlayerController = MediaPlayerController;
224
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/player/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@rajeev02/media",
3
+ "version": "0.1.0",
4
+ "description": "Unified Media Player Engine — adaptive streaming, PiP, offline download, DRM, Hotstar/Netflix style",
5
+ "main": "lib/index.js",
6
+ "author": "Rajeev Kumar Joshi <rajeevjoshi91@gmail.com> (https://rajeev02.github.io)",
7
+ "license": "MIT",
8
+ "types": "lib/index.d.ts",
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "clean": "rm -rf lib",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "keywords": [
15
+ "react-native",
16
+ "media-player",
17
+ "audio",
18
+ "video",
19
+ "streaming",
20
+ "pip"
21
+ ],
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/Rajeev02/rajeev-sdk",
25
+ "directory": "packages/media"
26
+ },
27
+ "homepage": "https://github.com/Rajeev02/rajeev-sdk#readme",
28
+ "bugs": {
29
+ "url": "https://github.com/Rajeev02/rajeev-sdk/issues"
30
+ },
31
+ "files": [
32
+ "lib/",
33
+ "src/",
34
+ "README.md"
35
+ ],
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "peerDependencies": {
40
+ "react": ">=18.3.0",
41
+ "react-native": ">=0.84.0"
42
+ },
43
+ "peerDependenciesMeta": {
44
+ "react-native": {
45
+ "optional": true
46
+ }
47
+ },
48
+ "devDependencies": {
49
+ "@types/react": "^19.0.0",
50
+ "typescript": "^5.4.0"
51
+ }
52
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * @rajeev02/media — Download Manager
3
+ * Offline media downloads with quality selection, pause/resume, storage management
4
+ */
5
+
6
+ export type DownloadState =
7
+ | "queued"
8
+ | "downloading"
9
+ | "paused"
10
+ | "completed"
11
+ | "failed"
12
+ | "cancelled";
13
+
14
+ export interface DownloadRequest {
15
+ id: string;
16
+ uri: string;
17
+ title: string;
18
+ qualityId?: string;
19
+ /** Estimated size in bytes */
20
+ estimatedSizeBytes?: number;
21
+ /** Expiry time for downloaded content (DRM) */
22
+ expiresAt?: number;
23
+ metadata?: Record<string, unknown>;
24
+ }
25
+
26
+ export interface DownloadProgress {
27
+ id: string;
28
+ state: DownloadState;
29
+ bytesDownloaded: number;
30
+ totalBytes: number;
31
+ percentComplete: number;
32
+ speedBps: number;
33
+ estimatedRemainingMs: number;
34
+ }
35
+
36
+ /**
37
+ * Download Manager — tracks offline media downloads
38
+ */
39
+ export class DownloadManager {
40
+ private downloads: Map<
41
+ string,
42
+ DownloadRequest & {
43
+ state: DownloadState;
44
+ bytesDownloaded: number;
45
+ totalBytes: number;
46
+ startedAt: number;
47
+ }
48
+ > = new Map();
49
+ private listeners: Set<(id: string, progress: DownloadProgress) => void> =
50
+ new Set();
51
+ private maxConcurrent: number;
52
+ private maxStorageBytes: number;
53
+
54
+ constructor(
55
+ maxConcurrent: number = 2,
56
+ maxStorageBytes: number = 2 * 1024 * 1024 * 1024,
57
+ ) {
58
+ this.maxConcurrent = maxConcurrent;
59
+ this.maxStorageBytes = maxStorageBytes;
60
+ }
61
+
62
+ /** Queue a download */
63
+ enqueue(request: DownloadRequest): boolean {
64
+ if (this.downloads.has(request.id)) return false;
65
+ if (
66
+ this.getTotalStorageUsed() + (request.estimatedSizeBytes ?? 0) >
67
+ this.maxStorageBytes
68
+ )
69
+ return false;
70
+
71
+ this.downloads.set(request.id, {
72
+ ...request,
73
+ state: "queued",
74
+ bytesDownloaded: 0,
75
+ totalBytes: request.estimatedSizeBytes ?? 0,
76
+ startedAt: Date.now(),
77
+ });
78
+ return true;
79
+ }
80
+
81
+ /** Pause a download */
82
+ pause(id: string): boolean {
83
+ const dl = this.downloads.get(id);
84
+ if (dl && dl.state === "downloading") {
85
+ dl.state = "paused";
86
+ return true;
87
+ }
88
+ return false;
89
+ }
90
+
91
+ /** Resume a download */
92
+ resume(id: string): boolean {
93
+ const dl = this.downloads.get(id);
94
+ if (dl && dl.state === "paused") {
95
+ dl.state = "queued";
96
+ return true;
97
+ }
98
+ return false;
99
+ }
100
+
101
+ /** Cancel and remove a download */
102
+ cancel(id: string): boolean {
103
+ const dl = this.downloads.get(id);
104
+ if (dl) {
105
+ dl.state = "cancelled";
106
+ this.downloads.delete(id);
107
+ return true;
108
+ }
109
+ return false;
110
+ }
111
+
112
+ /** Update progress (called by native downloader) */
113
+ updateProgress(
114
+ id: string,
115
+ bytesDownloaded: number,
116
+ totalBytes: number,
117
+ ): void {
118
+ const dl = this.downloads.get(id);
119
+ if (!dl) return;
120
+ dl.bytesDownloaded = bytesDownloaded;
121
+ dl.totalBytes = totalBytes;
122
+ dl.state = "downloading";
123
+
124
+ const elapsed = Date.now() - dl.startedAt;
125
+ const speed = elapsed > 0 ? (bytesDownloaded / elapsed) * 1000 : 0;
126
+ const remaining =
127
+ speed > 0 ? ((totalBytes - bytesDownloaded) / speed) * 1000 : 0;
128
+
129
+ const progress: DownloadProgress = {
130
+ id,
131
+ state: "downloading",
132
+ bytesDownloaded,
133
+ totalBytes,
134
+ percentComplete:
135
+ totalBytes > 0 ? (bytesDownloaded / totalBytes) * 100 : 0,
136
+ speedBps: speed,
137
+ estimatedRemainingMs: remaining,
138
+ };
139
+
140
+ if (bytesDownloaded >= totalBytes) {
141
+ dl.state = "completed";
142
+ progress.state = "completed";
143
+ progress.percentComplete = 100;
144
+ }
145
+ this.notify(id, progress);
146
+ }
147
+
148
+ /** Get all downloads */
149
+ getAll(): (DownloadRequest & { state: DownloadState })[] {
150
+ return Array.from(this.downloads.values());
151
+ }
152
+
153
+ /** Get completed downloads */
154
+ getCompleted(): DownloadRequest[] {
155
+ return Array.from(this.downloads.values()).filter(
156
+ (d) => d.state === "completed",
157
+ );
158
+ }
159
+
160
+ /** Get total storage used by completed downloads */
161
+ getTotalStorageUsed(): number {
162
+ return Array.from(this.downloads.values())
163
+ .filter((d) => d.state === "completed")
164
+ .reduce((sum, d) => sum + d.totalBytes, 0);
165
+ }
166
+
167
+ /** Get max storage in bytes */
168
+ getMaxStorage(): number {
169
+ return this.maxStorageBytes;
170
+ }
171
+
172
+ /** Delete a completed download */
173
+ deleteDownload(id: string): boolean {
174
+ return this.downloads.delete(id);
175
+ }
176
+
177
+ /** Clear all completed downloads */
178
+ clearCompleted(): number {
179
+ let count = 0;
180
+ for (const [id, dl] of this.downloads) {
181
+ if (dl.state === "completed") {
182
+ this.downloads.delete(id);
183
+ count++;
184
+ }
185
+ }
186
+ return count;
187
+ }
188
+
189
+ /** Subscribe to progress updates */
190
+ onProgress(
191
+ listener: (id: string, progress: DownloadProgress) => void,
192
+ ): () => void {
193
+ this.listeners.add(listener);
194
+ return () => this.listeners.delete(listener);
195
+ }
196
+
197
+ private notify(id: string, progress: DownloadProgress): void {
198
+ for (const l of this.listeners) {
199
+ try {
200
+ l(id, progress);
201
+ } catch {}
202
+ }
203
+ }
204
+ }
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @rajeev02/media
3
+ * Unified Media Player Engine
4
+ * Adaptive streaming, PiP, offline download, DRM, quality selection
5
+ *
6
+ * @author Rajeev Kumar Joshi
7
+ * @license MIT
8
+ */
9
+ export { MediaPlayerController } from "./player";
10
+ export type {
11
+ MediaSource,
12
+ DrmConfig,
13
+ QualityLevel,
14
+ PlaybackState,
15
+ PlayerConfig,
16
+ PlayerEvent,
17
+ PlayerState,
18
+ MediaType,
19
+ } from "./player";
20
+
21
+ export { DownloadManager } from "./download";
22
+ export type {
23
+ DownloadRequest,
24
+ DownloadProgress,
25
+ DownloadState,
26
+ } from "./download";
@@ -0,0 +1,349 @@
1
+ /**
2
+ * @rajeev02/media — Player Core
3
+ * State machine for media playback — controls, events, quality selection, PiP, casting
4
+ */
5
+
6
+ export type PlayerState =
7
+ | "idle"
8
+ | "loading"
9
+ | "ready"
10
+ | "playing"
11
+ | "paused"
12
+ | "buffering"
13
+ | "ended"
14
+ | "error";
15
+ export type MediaType = "video" | "audio" | "live_video" | "live_audio";
16
+
17
+ export interface MediaSource {
18
+ uri: string;
19
+ type: MediaType;
20
+ title?: string;
21
+ subtitle?: string;
22
+ thumbnailUri?: string;
23
+ durationMs?: number;
24
+ /** DRM configuration */
25
+ drm?: DrmConfig;
26
+ /** Available quality levels (for adaptive streaming) */
27
+ qualities?: QualityLevel[];
28
+ /** Headers for authenticated streams */
29
+ headers?: Record<string, string>;
30
+ /** Offline download ID (if playing from local storage) */
31
+ offlineId?: string;
32
+ }
33
+
34
+ export interface DrmConfig {
35
+ type: "widevine" | "fairplay" | "clearkey";
36
+ licenseServerUrl: string;
37
+ headers?: Record<string, string>;
38
+ certificateUrl?: string;
39
+ }
40
+
41
+ export interface QualityLevel {
42
+ id: string;
43
+ label: string;
44
+ width: number;
45
+ height: number;
46
+ bitrate: number;
47
+ codec?: string;
48
+ }
49
+
50
+ export interface PlaybackState {
51
+ state: PlayerState;
52
+ currentTimeMs: number;
53
+ durationMs: number;
54
+ bufferedMs: number;
55
+ playbackRate: number;
56
+ volume: number;
57
+ muted: boolean;
58
+ isPiP: boolean;
59
+ isCasting: boolean;
60
+ isFullscreen: boolean;
61
+ currentQuality: QualityLevel | null;
62
+ autoQuality: boolean;
63
+ error?: string;
64
+ }
65
+
66
+ export interface PlayerConfig {
67
+ /** Auto-play when source is loaded */
68
+ autoPlay?: boolean;
69
+ /** Loop playback */
70
+ loop?: boolean;
71
+ /** Start at specific position (ms) — for resume */
72
+ startPositionMs?: number;
73
+ /** Max buffer duration (ms). Default: 30000 */
74
+ maxBufferMs?: number;
75
+ /** Min buffer before playback starts (ms). Default: 5000 */
76
+ minBufferMs?: number;
77
+ /** Preferred quality level (or 'auto') */
78
+ preferredQuality?: string;
79
+ /** Enable PiP support */
80
+ enablePiP?: boolean;
81
+ /** Enable casting support (Chromecast/AirPlay) */
82
+ enableCast?: boolean;
83
+ /** Background audio playback */
84
+ backgroundAudio?: boolean;
85
+ }
86
+
87
+ export type PlayerEvent =
88
+ | { type: "stateChange"; state: PlayerState }
89
+ | {
90
+ type: "progress";
91
+ currentTimeMs: number;
92
+ durationMs: number;
93
+ bufferedMs: number;
94
+ }
95
+ | { type: "qualityChange"; quality: QualityLevel }
96
+ | { type: "error"; code: string; message: string }
97
+ | { type: "ended" }
98
+ | { type: "pipChange"; active: boolean }
99
+ | { type: "castChange"; active: boolean; deviceName?: string }
100
+ | { type: "seekComplete"; positionMs: number };
101
+
102
+ /**
103
+ * Media Player Controller
104
+ * Abstract player that works with any native backend (ExoPlayer, AVPlayer, web <video>)
105
+ */
106
+ export class MediaPlayerController {
107
+ private source: MediaSource | null = null;
108
+ private config: PlayerConfig;
109
+ private state: PlaybackState;
110
+ private listeners: Set<(event: PlayerEvent) => void> = new Set();
111
+ private watchHistory: Map<string, number> = new Map(); // uri → last position
112
+
113
+ constructor(config: PlayerConfig = {}) {
114
+ this.config = {
115
+ autoPlay: false,
116
+ loop: false,
117
+ startPositionMs: 0,
118
+ maxBufferMs: 30000,
119
+ minBufferMs: 5000,
120
+ preferredQuality: "auto",
121
+ enablePiP: true,
122
+ enableCast: false,
123
+ backgroundAudio: false,
124
+ ...config,
125
+ };
126
+ this.state = this.createInitialState();
127
+ }
128
+
129
+ /** Load a media source */
130
+ load(source: MediaSource): void {
131
+ this.source = source;
132
+ this.setState("loading");
133
+ // Check for resume position
134
+ const savedPos = this.watchHistory.get(source.uri);
135
+ if (savedPos && savedPos > 0 && !this.config.startPositionMs) {
136
+ this.config.startPositionMs = savedPos;
137
+ }
138
+ this.emit({ type: "stateChange", state: "loading" });
139
+ }
140
+
141
+ /** Called by native layer when media is ready */
142
+ onReady(durationMs: number, qualities?: QualityLevel[]): void {
143
+ this.state.durationMs = durationMs;
144
+ if (qualities && this.source) {
145
+ this.source.qualities = qualities;
146
+ }
147
+ this.setState("ready");
148
+ if (this.config.autoPlay) this.play();
149
+ }
150
+
151
+ /** Play */
152
+ play(): void {
153
+ if (this.state.state === "ended" && this.config.loop) {
154
+ this.seek(0);
155
+ }
156
+ this.setState("playing");
157
+ }
158
+
159
+ /** Pause */
160
+ pause(): void {
161
+ this.saveProgress();
162
+ this.setState("paused");
163
+ }
164
+
165
+ /** Seek to position */
166
+ seek(positionMs: number): void {
167
+ this.state.currentTimeMs = Math.max(
168
+ 0,
169
+ Math.min(positionMs, this.state.durationMs),
170
+ );
171
+ this.emit({ type: "seekComplete", positionMs: this.state.currentTimeMs });
172
+ }
173
+
174
+ /** Skip forward */
175
+ skipForward(ms: number = 10000): void {
176
+ this.seek(this.state.currentTimeMs + ms);
177
+ }
178
+
179
+ /** Skip backward */
180
+ skipBackward(ms: number = 10000): void {
181
+ this.seek(this.state.currentTimeMs - ms);
182
+ }
183
+
184
+ /** Set playback rate */
185
+ setRate(rate: number): void {
186
+ this.state.playbackRate = Math.max(0.25, Math.min(4, rate));
187
+ }
188
+
189
+ /** Set volume (0.0 - 1.0) */
190
+ setVolume(volume: number): void {
191
+ this.state.volume = Math.max(0, Math.min(1, volume));
192
+ }
193
+
194
+ /** Toggle mute */
195
+ toggleMute(): void {
196
+ this.state.muted = !this.state.muted;
197
+ }
198
+
199
+ /** Toggle fullscreen */
200
+ toggleFullscreen(): void {
201
+ this.state.isFullscreen = !this.state.isFullscreen;
202
+ }
203
+
204
+ /** Enter PiP */
205
+ enterPiP(): void {
206
+ if (this.config.enablePiP) {
207
+ this.state.isPiP = true;
208
+ this.emit({ type: "pipChange", active: true });
209
+ }
210
+ }
211
+
212
+ /** Exit PiP */
213
+ exitPiP(): void {
214
+ this.state.isPiP = false;
215
+ this.emit({ type: "pipChange", active: false });
216
+ }
217
+
218
+ /** Select quality level */
219
+ setQuality(qualityId: string): void {
220
+ if (qualityId === "auto") {
221
+ this.state.autoQuality = true;
222
+ this.state.currentQuality = null;
223
+ return;
224
+ }
225
+ const quality = this.source?.qualities?.find((q) => q.id === qualityId);
226
+ if (quality) {
227
+ this.state.autoQuality = false;
228
+ this.state.currentQuality = quality;
229
+ this.emit({ type: "qualityChange", quality });
230
+ }
231
+ }
232
+
233
+ /** Get available quality levels */
234
+ getQualities(): QualityLevel[] {
235
+ return this.source?.qualities ?? [];
236
+ }
237
+
238
+ /** Update progress (called by native layer on timer) */
239
+ onProgress(currentTimeMs: number, bufferedMs: number): void {
240
+ this.state.currentTimeMs = currentTimeMs;
241
+ this.state.bufferedMs = bufferedMs;
242
+ this.emit({
243
+ type: "progress",
244
+ currentTimeMs,
245
+ durationMs: this.state.durationMs,
246
+ bufferedMs,
247
+ });
248
+ }
249
+
250
+ /** Called when playback ends */
251
+ onEnded(): void {
252
+ this.setState("ended");
253
+ this.watchHistory.delete(this.source?.uri ?? "");
254
+ this.emit({ type: "ended" });
255
+ if (this.config.loop) this.play();
256
+ }
257
+
258
+ /** Called on error */
259
+ onError(code: string, message: string): void {
260
+ this.setState("error");
261
+ this.state.error = message;
262
+ this.emit({ type: "error", code, message });
263
+ }
264
+
265
+ /** Get current playback state */
266
+ getState(): PlaybackState {
267
+ return { ...this.state };
268
+ }
269
+ getSource(): MediaSource | null {
270
+ return this.source;
271
+ }
272
+ getConfig(): PlayerConfig {
273
+ return { ...this.config };
274
+ }
275
+
276
+ /** Get watch progress percentage (0-100) */
277
+ getProgressPercent(): number {
278
+ if (this.state.durationMs === 0) return 0;
279
+ return (this.state.currentTimeMs / this.state.durationMs) * 100;
280
+ }
281
+
282
+ /** Get resume position for a URI */
283
+ getResumePosition(uri: string): number {
284
+ return this.watchHistory.get(uri) ?? 0;
285
+ }
286
+
287
+ /** Subscribe to events */
288
+ on(listener: (event: PlayerEvent) => void): () => void {
289
+ this.listeners.add(listener);
290
+ return () => this.listeners.delete(listener);
291
+ }
292
+
293
+ /** Destroy player */
294
+ destroy(): void {
295
+ this.saveProgress();
296
+ this.source = null;
297
+ this.setState("idle");
298
+ this.listeners.clear();
299
+ }
300
+
301
+ /** Suggest quality based on network bandwidth */
302
+ static suggestQuality(
303
+ qualities: QualityLevel[],
304
+ bandwidthKbps: number,
305
+ ): QualityLevel | null {
306
+ const bandwidthBps = bandwidthKbps * 1000;
307
+ // Pick highest quality that fits within 80% of available bandwidth
308
+ const suitable = qualities
309
+ .filter((q) => q.bitrate <= bandwidthBps * 0.8)
310
+ .sort((a, b) => b.bitrate - a.bitrate);
311
+ return suitable[0] ?? qualities[qualities.length - 1] ?? null;
312
+ }
313
+
314
+ private saveProgress(): void {
315
+ if (this.source && this.state.currentTimeMs > 5000) {
316
+ this.watchHistory.set(this.source.uri, this.state.currentTimeMs);
317
+ }
318
+ }
319
+
320
+ private setState(state: PlayerState): void {
321
+ this.state.state = state;
322
+ this.emit({ type: "stateChange", state });
323
+ }
324
+
325
+ private emit(event: PlayerEvent): void {
326
+ for (const l of this.listeners) {
327
+ try {
328
+ l(event);
329
+ } catch {}
330
+ }
331
+ }
332
+
333
+ private createInitialState(): PlaybackState {
334
+ return {
335
+ state: "idle",
336
+ currentTimeMs: 0,
337
+ durationMs: 0,
338
+ bufferedMs: 0,
339
+ playbackRate: 1,
340
+ volume: 1,
341
+ muted: false,
342
+ isPiP: false,
343
+ isCasting: false,
344
+ isFullscreen: false,
345
+ currentQuality: null,
346
+ autoQuality: true,
347
+ };
348
+ }
349
+ }