@mediafox/core 1.2.8 → 1.2.10
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/dist/compositor/compositor.d.ts.map +1 -1
- package/dist/compositor-worker.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +4 -3
- package/src/compositor/audio-manager.ts +411 -0
- package/src/compositor/compositor-worker.ts +158 -0
- package/src/compositor/compositor.ts +931 -0
- package/src/compositor/index.ts +19 -0
- package/src/compositor/source-pool.ts +450 -0
- package/src/compositor/types.ts +103 -0
- package/src/compositor/worker-client.ts +139 -0
- package/src/compositor/worker-types.ts +67 -0
- package/src/core/player-core.ts +273 -0
- package/src/core/state-facade.ts +98 -0
- package/src/core/track-switcher.ts +127 -0
- package/src/events/emitter.ts +137 -0
- package/src/events/types.ts +24 -0
- package/src/index.ts +124 -0
- package/src/mediafox.ts +642 -0
- package/src/playback/audio.ts +361 -0
- package/src/playback/controller.ts +446 -0
- package/src/playback/renderer.ts +1176 -0
- package/src/playback/renderers/canvas2d.ts +128 -0
- package/src/playback/renderers/factory.ts +172 -0
- package/src/playback/renderers/index.ts +5 -0
- package/src/playback/renderers/types.ts +57 -0
- package/src/playback/renderers/webgl.ts +373 -0
- package/src/playback/renderers/webgpu.ts +395 -0
- package/src/playlist/manager.ts +268 -0
- package/src/plugins/context.ts +93 -0
- package/src/plugins/index.ts +15 -0
- package/src/plugins/manager.ts +482 -0
- package/src/plugins/types.ts +243 -0
- package/src/sources/manager.ts +285 -0
- package/src/sources/source.ts +84 -0
- package/src/sources/types.ts +17 -0
- package/src/state/store.ts +389 -0
- package/src/state/types.ts +18 -0
- package/src/tracks/manager.ts +421 -0
- package/src/tracks/types.ts +30 -0
- package/src/types/jassub.d.ts +1 -0
- package/src/types.ts +235 -0
- package/src/utils/async-lock.ts +26 -0
- package/src/utils/dispose.ts +28 -0
- package/src/utils/equal.ts +33 -0
- package/src/utils/errors.ts +74 -0
- package/src/utils/logger.ts +50 -0
- package/src/utils/time.ts +157 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safely calls return() on an AsyncGenerator if present, swallowing errors.
|
|
3
|
+
* Used to ensure iterators are properly cleaned up without leaking promises.
|
|
4
|
+
*/
|
|
5
|
+
export function safeAsyncReturn<T, TReturn = unknown, TNext = unknown>(
|
|
6
|
+
iterator: AsyncGenerator<T, TReturn, TNext> | null | undefined
|
|
7
|
+
): void {
|
|
8
|
+
try {
|
|
9
|
+
// Intentionally do not await inside non-async dispose paths
|
|
10
|
+
// Some TS lib dom typings require a value argument
|
|
11
|
+
void iterator?.return?.(undefined as unknown as TReturn);
|
|
12
|
+
} catch {
|
|
13
|
+
// Ignore iterator return errors during cleanup
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Clear interval/RAF ids safely.
|
|
19
|
+
*/
|
|
20
|
+
export function safeClearInterval(id: number | null): null {
|
|
21
|
+
if (id !== null) clearInterval(id);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function safeCancelAnimationFrame(id: number | null): null {
|
|
26
|
+
if (id !== null) cancelAnimationFrame(id);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Lightweight deep equality for simple JSON-like structures used in state
|
|
2
|
+
export function isDeepEqual(a: unknown, b: unknown): boolean {
|
|
3
|
+
if (Object.is(a, b)) return true;
|
|
4
|
+
if (typeof a !== typeof b) return false;
|
|
5
|
+
if (a === null || b === null) return a === b;
|
|
6
|
+
|
|
7
|
+
// Arrays
|
|
8
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
9
|
+
if (a.length !== b.length) return false;
|
|
10
|
+
for (let i = 0; i < a.length; i++) {
|
|
11
|
+
if (!isDeepEqual(a[i], b[i])) return false;
|
|
12
|
+
}
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Objects
|
|
17
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
18
|
+
const ak = Object.keys(a as Record<string, unknown>);
|
|
19
|
+
const bk = Object.keys(b as Record<string, unknown>);
|
|
20
|
+
if (ak.length !== bk.length) return false;
|
|
21
|
+
for (const k of ak) {
|
|
22
|
+
if (!Object.hasOwn(b, k)) return false;
|
|
23
|
+
if (!isDeepEqual((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k])) return false;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
32
|
+
return typeof v === 'object' && v !== null && (v as object).constructor === Object;
|
|
33
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export enum ErrorCode {
|
|
2
|
+
MEDIA_NOT_SUPPORTED = 'MEDIA_NOT_SUPPORTED',
|
|
3
|
+
MEDIA_LOAD_FAILED = 'MEDIA_LOAD_FAILED',
|
|
4
|
+
DECODE_ERROR = 'DECODE_ERROR',
|
|
5
|
+
NETWORK_ERROR = 'NETWORK_ERROR',
|
|
6
|
+
PERMISSION_DENIED = 'PERMISSION_DENIED',
|
|
7
|
+
PLAYBACK_ERROR = 'PLAYBACK_ERROR',
|
|
8
|
+
TRACK_NOT_FOUND = 'TRACK_NOT_FOUND',
|
|
9
|
+
INVALID_STATE = 'INVALID_STATE',
|
|
10
|
+
UNKNOWN_ERROR = 'UNKNOWN_ERROR',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class MediaFoxError extends Error {
|
|
14
|
+
code: ErrorCode;
|
|
15
|
+
details?: unknown;
|
|
16
|
+
|
|
17
|
+
constructor(code: ErrorCode, message: string, details?: unknown) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'MediaFoxError';
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.details = details;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static mediaNotSupported(message = 'Media format not supported', details?: unknown): MediaFoxError {
|
|
25
|
+
return new MediaFoxError(ErrorCode.MEDIA_NOT_SUPPORTED, message, details);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static mediaLoadFailed(message = 'Failed to load media', details?: unknown): MediaFoxError {
|
|
29
|
+
return new MediaFoxError(ErrorCode.MEDIA_LOAD_FAILED, message, details);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static decodeError(message = 'Failed to decode media', details?: unknown): MediaFoxError {
|
|
33
|
+
return new MediaFoxError(ErrorCode.DECODE_ERROR, message, details);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static networkError(message = 'Network error occurred', details?: unknown): MediaFoxError {
|
|
37
|
+
return new MediaFoxError(ErrorCode.NETWORK_ERROR, message, details);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static permissionDenied(message = 'Permission denied', details?: unknown): MediaFoxError {
|
|
41
|
+
return new MediaFoxError(ErrorCode.PERMISSION_DENIED, message, details);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static playbackError(message = 'Playback error occurred', details?: unknown): MediaFoxError {
|
|
45
|
+
return new MediaFoxError(ErrorCode.PLAYBACK_ERROR, message, details);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static trackNotFound(message = 'Track not found', details?: unknown): MediaFoxError {
|
|
49
|
+
return new MediaFoxError(ErrorCode.TRACK_NOT_FOUND, message, details);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static invalidState(message = 'Invalid player state', details?: unknown): MediaFoxError {
|
|
53
|
+
return new MediaFoxError(ErrorCode.INVALID_STATE, message, details);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static unknownError(message = 'Unknown error occurred', details?: unknown): MediaFoxError {
|
|
57
|
+
return new MediaFoxError(ErrorCode.UNKNOWN_ERROR, message, details);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Wrap an error with additional context
|
|
63
|
+
*/
|
|
64
|
+
export function wrapError(error: unknown, context: string): MediaFoxError {
|
|
65
|
+
if (error instanceof MediaFoxError) {
|
|
66
|
+
return error;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (error instanceof Error) {
|
|
70
|
+
return new MediaFoxError(ErrorCode.UNKNOWN_ERROR, `${context}: ${error.message}`, { originalError: error });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return new MediaFoxError(ErrorCode.UNKNOWN_ERROR, `${context}: ${String(error)}`, { originalError: error });
|
|
74
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple logger utility for MediaFox
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export enum LogLevel {
|
|
6
|
+
DEBUG = 0,
|
|
7
|
+
INFO = 1,
|
|
8
|
+
WARN = 2,
|
|
9
|
+
ERROR = 3,
|
|
10
|
+
NONE = 4,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let logLevel: LogLevel = LogLevel.WARN;
|
|
14
|
+
const logPrefix = '[MediaFox]';
|
|
15
|
+
|
|
16
|
+
export function setLogLevel(level: LogLevel): void {
|
|
17
|
+
logLevel = level;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function debug(message: string, ...args: unknown[]): void {
|
|
21
|
+
if (logLevel <= LogLevel.DEBUG) {
|
|
22
|
+
console.debug(`${logPrefix} ${message}`, ...args);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function info(message: string, ...args: unknown[]): void {
|
|
27
|
+
if (logLevel <= LogLevel.INFO) {
|
|
28
|
+
console.info(`${logPrefix} ${message}`, ...args);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function warn(message: string, ...args: unknown[]): void {
|
|
33
|
+
if (logLevel <= LogLevel.WARN) {
|
|
34
|
+
console.warn(`${logPrefix} ${message}`, ...args);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function error(message: string, ...args: unknown[]): void {
|
|
39
|
+
if (logLevel <= LogLevel.ERROR) {
|
|
40
|
+
console.error(`${logPrefix} ${message}`, ...args);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const logger = {
|
|
45
|
+
setLevel: setLogLevel,
|
|
46
|
+
debug,
|
|
47
|
+
info,
|
|
48
|
+
warn,
|
|
49
|
+
error,
|
|
50
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format seconds into a human-readable time string
|
|
3
|
+
* @param seconds - Time in seconds
|
|
4
|
+
* @param showMilliseconds - Whether to show milliseconds
|
|
5
|
+
* @returns Formatted time string (e.g., "1:23:45" or "23:45.123")
|
|
6
|
+
*/
|
|
7
|
+
export function formatTime(seconds: number, showMilliseconds = false): string {
|
|
8
|
+
const absSeconds = Math.abs(seconds);
|
|
9
|
+
const hours = Math.floor(absSeconds / 3600);
|
|
10
|
+
const minutes = Math.floor((absSeconds % 3600) / 60);
|
|
11
|
+
const secs = Math.floor(absSeconds % 60);
|
|
12
|
+
const ms = Math.floor((absSeconds % 1) * 1000);
|
|
13
|
+
|
|
14
|
+
let result = '';
|
|
15
|
+
|
|
16
|
+
if (seconds < 0) {
|
|
17
|
+
result = '-';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (hours > 0) {
|
|
21
|
+
result += `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
22
|
+
} else {
|
|
23
|
+
result += `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (showMilliseconds) {
|
|
27
|
+
result += `.${ms.toString().padStart(3, '0')}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse a time string into seconds
|
|
35
|
+
* @param timeString - Time string (e.g., "1:23:45", "23:45", "45")
|
|
36
|
+
* @returns Time in seconds
|
|
37
|
+
*/
|
|
38
|
+
export function parseTime(timeString: string): number {
|
|
39
|
+
const parts = timeString.trim().split(':').map(Number);
|
|
40
|
+
|
|
41
|
+
if (parts.some(Number.isNaN)) {
|
|
42
|
+
throw new Error('Invalid time string');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let seconds = 0;
|
|
46
|
+
|
|
47
|
+
if (parts.length === 3) {
|
|
48
|
+
// HH:MM:SS
|
|
49
|
+
seconds = parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
50
|
+
} else if (parts.length === 2) {
|
|
51
|
+
// MM:SS
|
|
52
|
+
seconds = parts[0] * 60 + parts[1];
|
|
53
|
+
} else if (parts.length === 1) {
|
|
54
|
+
// SS
|
|
55
|
+
seconds = parts[0];
|
|
56
|
+
} else {
|
|
57
|
+
throw new Error('Invalid time format');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return seconds;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get frame number from time
|
|
65
|
+
* @param time - Time in seconds
|
|
66
|
+
* @param frameRate - Frame rate in Hz
|
|
67
|
+
* @returns Frame number
|
|
68
|
+
*/
|
|
69
|
+
export function timeToFrame(time: number, frameRate: number): number {
|
|
70
|
+
return Math.floor(time * frameRate);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get time from frame number
|
|
75
|
+
* @param frame - Frame number
|
|
76
|
+
* @param frameRate - Frame rate in Hz
|
|
77
|
+
* @returns Time in seconds
|
|
78
|
+
*/
|
|
79
|
+
export function frameToTime(frame: number, frameRate: number): number {
|
|
80
|
+
return frame / frameRate;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Clamp a value between min and max
|
|
85
|
+
* @param value - Value to clamp
|
|
86
|
+
* @param min - Minimum value
|
|
87
|
+
* @param max - Maximum value
|
|
88
|
+
* @returns Clamped value
|
|
89
|
+
*/
|
|
90
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
91
|
+
return Math.max(min, Math.min(max, value));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if two time ranges overlap
|
|
96
|
+
* @param range1 - First time range
|
|
97
|
+
* @param range2 - Second time range
|
|
98
|
+
* @returns True if ranges overlap
|
|
99
|
+
*/
|
|
100
|
+
export function timeRangesOverlap(
|
|
101
|
+
range1: { start: number; end: number },
|
|
102
|
+
range2: { start: number; end: number }
|
|
103
|
+
): boolean {
|
|
104
|
+
return range1.start < range2.end && range2.start < range1.end;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Merge overlapping time ranges
|
|
109
|
+
* @param ranges - Array of time ranges
|
|
110
|
+
* @returns Merged time ranges
|
|
111
|
+
*/
|
|
112
|
+
export function mergeTimeRanges(ranges: Array<{ start: number; end: number }>): Array<{ start: number; end: number }> {
|
|
113
|
+
if (ranges.length === 0) return [];
|
|
114
|
+
|
|
115
|
+
const sorted = [...ranges].sort((a, b) => a.start - b.start);
|
|
116
|
+
const merged: Array<{ start: number; end: number }> = [sorted[0]];
|
|
117
|
+
|
|
118
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
119
|
+
const last = merged[merged.length - 1];
|
|
120
|
+
const current = sorted[i];
|
|
121
|
+
|
|
122
|
+
if (current.start <= last.end) {
|
|
123
|
+
last.end = Math.max(last.end, current.end);
|
|
124
|
+
} else {
|
|
125
|
+
merged.push(current);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return merged;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Calculate total buffered duration
|
|
134
|
+
* @param ranges - Array of time ranges
|
|
135
|
+
* @returns Total duration in seconds
|
|
136
|
+
*/
|
|
137
|
+
export function totalBufferedDuration(ranges: Array<{ start: number; end: number }>): number {
|
|
138
|
+
return ranges.reduce((total, range) => total + (range.end - range.start), 0);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Find the buffered range containing a specific time
|
|
143
|
+
* @param ranges - Array of time ranges
|
|
144
|
+
* @param time - Time to search for
|
|
145
|
+
* @returns The containing range or null
|
|
146
|
+
*/
|
|
147
|
+
export function findBufferedRange(
|
|
148
|
+
ranges: Array<{ start: number; end: number }>,
|
|
149
|
+
time: number
|
|
150
|
+
): { start: number; end: number } | null {
|
|
151
|
+
for (const range of ranges) {
|
|
152
|
+
if (time >= range.start && time < range.end) {
|
|
153
|
+
return range;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|