@djangocfg/ui-nextjs 2.1.76 → 2.1.78
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/package.json +4 -4
- package/src/lib/index.ts +4 -1
- package/src/lib/logger/index.ts +10 -0
- package/src/lib/logger/logStore.ts +122 -0
- package/src/lib/logger/logger.ts +175 -0
- package/src/lib/logger/types.ts +82 -0
- package/src/stores/mediaCache.ts +10 -0
- package/src/tools/AudioPlayer/context/AudioProvider.tsx +68 -1
- package/src/tools/AudioPlayer/progressive/useAudioElement.ts +52 -1
- package/src/tools/AudioPlayer/utils/debug.ts +12 -0
- package/src/tools/AudioPlayer/utils/index.ts +1 -0
- package/src/tools/ImageViewer/components/ImageViewer.tsx +9 -11
- package/src/tools/ImageViewer/hooks/useImageLoading.ts +18 -4
- package/src/tools/ImageViewer/utils/debug.ts +12 -0
- package/src/tools/ImageViewer/utils/index.ts +1 -0
- package/src/tools/VideoPlayer/providers/NativeProvider.tsx +60 -1
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +61 -3
- package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +67 -3
- package/src/tools/VideoPlayer/utils/debug.ts +12 -0
- package/src/tools/VideoPlayer/utils/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-nextjs",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.78",
|
|
4
4
|
"description": "Next.js UI component library with Radix UI primitives, Tailwind CSS styling, charts, and form components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-components",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"check": "tsc --noEmit"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@djangocfg/api": "^2.1.
|
|
62
|
-
"@djangocfg/ui-core": "^2.1.
|
|
61
|
+
"@djangocfg/api": "^2.1.78",
|
|
62
|
+
"@djangocfg/ui-core": "^2.1.78",
|
|
63
63
|
"@types/react": "^19.1.0",
|
|
64
64
|
"@types/react-dom": "^19.1.0",
|
|
65
65
|
"consola": "^3.4.2",
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"wavesurfer.js": "^7.12.1"
|
|
111
111
|
},
|
|
112
112
|
"devDependencies": {
|
|
113
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
113
|
+
"@djangocfg/typescript-config": "^2.1.78",
|
|
114
114
|
"@types/node": "^24.7.2",
|
|
115
115
|
"eslint": "^9.37.0",
|
|
116
116
|
"tailwindcss-animate": "1.0.7",
|
package/src/lib/index.ts
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal Logger
|
|
3
|
+
*
|
|
4
|
+
* Combines console logging with zustand store for Console panel.
|
|
5
|
+
* Use createMediaLogger for media tools (AudioPlayer, VideoPlayer, ImageViewer).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { createLogger, createMediaLogger, logger, log } from './logger';
|
|
9
|
+
export { useLogStore, useFilteredLogs, useLogCount, useErrorCount } from './logStore';
|
|
10
|
+
export type { LogEntry, LogLevel, LogFilter, LogStore, Logger, MediaLogger } from './types';
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Store
|
|
3
|
+
*
|
|
4
|
+
* Zustand store for log accumulation and filtering.
|
|
5
|
+
* Keeps logs in memory for Console panel display.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import { create } from 'zustand';
|
|
11
|
+
import type { LogStore, LogEntry, LogFilter } from './types';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_FILTER: LogFilter = {
|
|
14
|
+
levels: ['debug', 'info', 'warn', 'error', 'success'],
|
|
15
|
+
component: undefined,
|
|
16
|
+
search: undefined,
|
|
17
|
+
since: undefined,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const MAX_LOGS = 1000;
|
|
21
|
+
|
|
22
|
+
function generateId(): string {
|
|
23
|
+
return `log-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function matchesFilter(entry: LogEntry, filter: LogFilter): boolean {
|
|
27
|
+
// Level filter
|
|
28
|
+
if (!filter.levels.includes(entry.level)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Component filter (case-insensitive partial match)
|
|
33
|
+
if (filter.component) {
|
|
34
|
+
const comp = filter.component.toLowerCase();
|
|
35
|
+
if (!entry.component.toLowerCase().includes(comp)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Search filter (case-insensitive, searches message and data)
|
|
41
|
+
if (filter.search) {
|
|
42
|
+
const search = filter.search.toLowerCase();
|
|
43
|
+
const inMessage = entry.message.toLowerCase().includes(search);
|
|
44
|
+
const inData = entry.data
|
|
45
|
+
? JSON.stringify(entry.data).toLowerCase().includes(search)
|
|
46
|
+
: false;
|
|
47
|
+
if (!inMessage && !inData) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Time filter
|
|
53
|
+
if (filter.since && entry.timestamp < filter.since) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const useLogStore = create<LogStore>((set, get) => ({
|
|
61
|
+
logs: [],
|
|
62
|
+
filter: DEFAULT_FILTER,
|
|
63
|
+
maxLogs: MAX_LOGS,
|
|
64
|
+
|
|
65
|
+
addLog: (entry) => {
|
|
66
|
+
const newEntry: LogEntry = {
|
|
67
|
+
...entry,
|
|
68
|
+
id: generateId(),
|
|
69
|
+
timestamp: new Date(),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
set((state) => {
|
|
73
|
+
const newLogs = [...state.logs, newEntry];
|
|
74
|
+
// Trim to max size
|
|
75
|
+
if (newLogs.length > state.maxLogs) {
|
|
76
|
+
return { logs: newLogs.slice(-state.maxLogs) };
|
|
77
|
+
}
|
|
78
|
+
return { logs: newLogs };
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
clearLogs: () => {
|
|
83
|
+
set({ logs: [] });
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
setFilter: (filter) => {
|
|
87
|
+
set((state) => ({
|
|
88
|
+
filter: { ...state.filter, ...filter },
|
|
89
|
+
}));
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
resetFilter: () => {
|
|
93
|
+
set({ filter: DEFAULT_FILTER });
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
getFilteredLogs: () => {
|
|
97
|
+
const { logs, filter } = get();
|
|
98
|
+
return logs.filter((entry) => matchesFilter(entry, filter));
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
exportLogs: () => {
|
|
102
|
+
const { logs } = get();
|
|
103
|
+
return JSON.stringify(logs, null, 2);
|
|
104
|
+
},
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
// Selector hooks for performance
|
|
108
|
+
export const useFilteredLogs = () => {
|
|
109
|
+
const logs = useLogStore((state) => state.logs);
|
|
110
|
+
const filter = useLogStore((state) => state.filter);
|
|
111
|
+
return logs.filter((entry) => matchesFilter(entry, filter));
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const useLogCount = () => {
|
|
115
|
+
return useLogStore((state) => state.logs.length);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const useErrorCount = () => {
|
|
119
|
+
return useLogStore((state) =>
|
|
120
|
+
state.logs.filter((log) => log.level === 'error').length
|
|
121
|
+
);
|
|
122
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger
|
|
3
|
+
*
|
|
4
|
+
* Universal logger with consola + zustand store integration.
|
|
5
|
+
* Logs are accumulated for Console panel display.
|
|
6
|
+
* In production, only logs to store (no console output).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use client';
|
|
10
|
+
|
|
11
|
+
import { consola, type ConsolaInstance } from 'consola';
|
|
12
|
+
import { useLogStore } from './logStore';
|
|
13
|
+
import type { Logger, LogLevel, MediaLogger } from './types';
|
|
14
|
+
|
|
15
|
+
// Check environment
|
|
16
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
17
|
+
const isBrowser = typeof window !== 'undefined';
|
|
18
|
+
|
|
19
|
+
// Create base consola instance
|
|
20
|
+
const baseConsola = consola.create({
|
|
21
|
+
level: isDev ? 4 : 2, // 4 = debug, 2 = warn
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extract error details from unknown error type
|
|
26
|
+
*/
|
|
27
|
+
function extractErrorData(data?: Record<string, unknown>): {
|
|
28
|
+
cleanData: Record<string, unknown> | undefined;
|
|
29
|
+
stack: string | undefined;
|
|
30
|
+
} {
|
|
31
|
+
if (!data) return { cleanData: undefined, stack: undefined };
|
|
32
|
+
|
|
33
|
+
const cleanData = { ...data };
|
|
34
|
+
let stack: string | undefined;
|
|
35
|
+
|
|
36
|
+
// Extract stack from error object
|
|
37
|
+
if (data.error instanceof Error) {
|
|
38
|
+
stack = data.error.stack;
|
|
39
|
+
cleanData.error = {
|
|
40
|
+
name: data.error.name,
|
|
41
|
+
message: data.error.message,
|
|
42
|
+
};
|
|
43
|
+
} else if (typeof data.error === 'object' && data.error !== null) {
|
|
44
|
+
const errObj = data.error as Record<string, unknown>;
|
|
45
|
+
if (typeof errObj.stack === 'string') {
|
|
46
|
+
stack = errObj.stack;
|
|
47
|
+
}
|
|
48
|
+
if (typeof errObj.message === 'string') {
|
|
49
|
+
cleanData.error = errObj.message;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { cleanData, stack };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Format buffer ranges for logging
|
|
58
|
+
*/
|
|
59
|
+
function formatBufferRanges(buffered: TimeRanges, duration: number): string {
|
|
60
|
+
if (buffered.length === 0) return 'empty';
|
|
61
|
+
|
|
62
|
+
const ranges: string[] = [];
|
|
63
|
+
for (let i = 0; i < buffered.length; i++) {
|
|
64
|
+
const start = buffered.start(i);
|
|
65
|
+
const end = buffered.end(i);
|
|
66
|
+
const percent = ((end - start) / duration * 100).toFixed(1);
|
|
67
|
+
ranges.push(`${start.toFixed(1)}-${end.toFixed(1)}s (${percent}%)`);
|
|
68
|
+
}
|
|
69
|
+
return ranges.join(', ');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create a logger for a specific component/module
|
|
74
|
+
*/
|
|
75
|
+
export function createLogger(component: string): Logger {
|
|
76
|
+
const consolaTagged = baseConsola.withTag(component);
|
|
77
|
+
|
|
78
|
+
const log = (level: LogLevel, message: string, data?: Record<string, unknown>) => {
|
|
79
|
+
const { cleanData, stack } = extractErrorData(data);
|
|
80
|
+
|
|
81
|
+
// Add to store (for Console panel)
|
|
82
|
+
if (isBrowser) {
|
|
83
|
+
useLogStore.getState().addLog({
|
|
84
|
+
level,
|
|
85
|
+
component,
|
|
86
|
+
message,
|
|
87
|
+
data: cleanData,
|
|
88
|
+
stack,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Log to console via consola (in dev mode)
|
|
93
|
+
if (isDev) {
|
|
94
|
+
const consolaMethod = level === 'success' ? 'success' : level;
|
|
95
|
+
if (cleanData) {
|
|
96
|
+
consolaTagged[consolaMethod](message, cleanData);
|
|
97
|
+
} else {
|
|
98
|
+
consolaTagged[consolaMethod](message);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
debug: (message, data) => log('debug', message, data),
|
|
105
|
+
info: (message, data) => log('info', message, data),
|
|
106
|
+
warn: (message, data) => log('warn', message, data),
|
|
107
|
+
error: (message, data) => log('error', message, data),
|
|
108
|
+
success: (message, data) => log('success', message, data),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create a media-specific logger with helper methods
|
|
114
|
+
* for AudioPlayer, VideoPlayer, ImageViewer
|
|
115
|
+
*/
|
|
116
|
+
export function createMediaLogger(component: string): MediaLogger {
|
|
117
|
+
const baseLogger = createLogger(component);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
...baseLogger,
|
|
121
|
+
|
|
122
|
+
load: (src: string, type?: string) => {
|
|
123
|
+
const typeStr = type ? ` (${type})` : '';
|
|
124
|
+
baseLogger.info(`LOAD: ${src}${typeStr}`);
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
state: (state: string, details?: Record<string, unknown>) => {
|
|
128
|
+
baseLogger.debug(`STATE: ${state}`, details);
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
seek: (from: number, to: number, duration: number) => {
|
|
132
|
+
baseLogger.debug(`SEEK: ${from.toFixed(2)}s -> ${to.toFixed(2)}s`, {
|
|
133
|
+
from,
|
|
134
|
+
to,
|
|
135
|
+
duration,
|
|
136
|
+
progress: `${((to / duration) * 100).toFixed(1)}%`,
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
buffer: (buffered: TimeRanges, duration: number) => {
|
|
141
|
+
if (buffered.length > 0) {
|
|
142
|
+
baseLogger.debug(`BUFFER: ${formatBufferRanges(buffered, duration)}`);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
event: (name: string, data?: unknown) => {
|
|
147
|
+
if (data !== undefined) {
|
|
148
|
+
baseLogger.debug(`EVENT: ${name}`, { data });
|
|
149
|
+
} else {
|
|
150
|
+
baseLogger.debug(`EVENT: ${name}`);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Global logger for non-component code
|
|
158
|
+
*/
|
|
159
|
+
export const logger = createLogger('App');
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Quick access for one-off logs
|
|
163
|
+
*/
|
|
164
|
+
export const log = {
|
|
165
|
+
debug: (component: string, message: string, data?: Record<string, unknown>) =>
|
|
166
|
+
createLogger(component).debug(message, data),
|
|
167
|
+
info: (component: string, message: string, data?: Record<string, unknown>) =>
|
|
168
|
+
createLogger(component).info(message, data),
|
|
169
|
+
warn: (component: string, message: string, data?: Record<string, unknown>) =>
|
|
170
|
+
createLogger(component).warn(message, data),
|
|
171
|
+
error: (component: string, message: string, data?: Record<string, unknown>) =>
|
|
172
|
+
createLogger(component).error(message, data),
|
|
173
|
+
success: (component: string, message: string, data?: Record<string, unknown>) =>
|
|
174
|
+
createLogger(component).success(message, data),
|
|
175
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for the universal logging system.
|
|
5
|
+
* Compatible with Console panel in FileWorkspace IDE layout.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'success';
|
|
9
|
+
|
|
10
|
+
export interface LogEntry {
|
|
11
|
+
/** Unique log ID */
|
|
12
|
+
id: string;
|
|
13
|
+
/** Timestamp when log was created */
|
|
14
|
+
timestamp: Date;
|
|
15
|
+
/** Log level */
|
|
16
|
+
level: LogLevel;
|
|
17
|
+
/** Component/module name that created the log */
|
|
18
|
+
component: string;
|
|
19
|
+
/** Log message */
|
|
20
|
+
message: string;
|
|
21
|
+
/** Additional data (objects, errors, etc.) */
|
|
22
|
+
data?: Record<string, unknown>;
|
|
23
|
+
/** Error stack trace (for error level) */
|
|
24
|
+
stack?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface LogFilter {
|
|
28
|
+
/** Filter by log levels */
|
|
29
|
+
levels: LogLevel[];
|
|
30
|
+
/** Filter by component name (partial match) */
|
|
31
|
+
component?: string;
|
|
32
|
+
/** Filter by message text (partial match) */
|
|
33
|
+
search?: string;
|
|
34
|
+
/** Filter by time range */
|
|
35
|
+
since?: Date;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface LogStore {
|
|
39
|
+
/** All accumulated logs */
|
|
40
|
+
logs: LogEntry[];
|
|
41
|
+
/** Current filter settings */
|
|
42
|
+
filter: LogFilter;
|
|
43
|
+
/** Maximum logs to keep */
|
|
44
|
+
maxLogs: number;
|
|
45
|
+
|
|
46
|
+
/** Add new log entry */
|
|
47
|
+
addLog: (entry: Omit<LogEntry, 'id' | 'timestamp'>) => void;
|
|
48
|
+
/** Clear all logs */
|
|
49
|
+
clearLogs: () => void;
|
|
50
|
+
/** Update filter settings */
|
|
51
|
+
setFilter: (filter: Partial<LogFilter>) => void;
|
|
52
|
+
/** Reset filter to defaults */
|
|
53
|
+
resetFilter: () => void;
|
|
54
|
+
/** Get filtered logs */
|
|
55
|
+
getFilteredLogs: () => LogEntry[];
|
|
56
|
+
/** Export logs as JSON string */
|
|
57
|
+
exportLogs: () => string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface Logger {
|
|
61
|
+
debug: (message: string, data?: Record<string, unknown>) => void;
|
|
62
|
+
info: (message: string, data?: Record<string, unknown>) => void;
|
|
63
|
+
warn: (message: string, data?: Record<string, unknown>) => void;
|
|
64
|
+
error: (message: string, data?: Record<string, unknown>) => void;
|
|
65
|
+
success: (message: string, data?: Record<string, unknown>) => void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Media-specific log helper methods
|
|
70
|
+
*/
|
|
71
|
+
export interface MediaLogger extends Logger {
|
|
72
|
+
/** Log source load event */
|
|
73
|
+
load: (src: string, type?: string) => void;
|
|
74
|
+
/** Log state change */
|
|
75
|
+
state: (state: string, details?: Record<string, unknown>) => void;
|
|
76
|
+
/** Log seek event */
|
|
77
|
+
seek: (from: number, to: number, duration: number) => void;
|
|
78
|
+
/** Log buffer ranges */
|
|
79
|
+
buffer: (buffered: TimeRanges, duration: number) => void;
|
|
80
|
+
/** Log generic event */
|
|
81
|
+
event: (name: string, data?: unknown) => void;
|
|
82
|
+
}
|
package/src/stores/mediaCache.ts
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
import { create } from 'zustand';
|
|
4
4
|
import { persist, devtools } from 'zustand/middleware';
|
|
5
5
|
import { useShallow } from 'zustand/react/shallow';
|
|
6
|
+
import { createLogger } from '../lib/logger';
|
|
7
|
+
|
|
8
|
+
const cacheDebug = createLogger('MediaCache');
|
|
6
9
|
|
|
7
10
|
// Types
|
|
8
11
|
interface BlobUrlEntry {
|
|
@@ -120,6 +123,7 @@ export const useMediaCacheStore = create<MediaCacheStore>()(
|
|
|
120
123
|
const existing = get().blobUrls.get(key);
|
|
121
124
|
if (existing) {
|
|
122
125
|
// Increment ref count
|
|
126
|
+
cacheDebug.debug(`Blob URL reused: ${key}`, { refCount: existing.refCount + 1 });
|
|
123
127
|
set(
|
|
124
128
|
(state) => ({
|
|
125
129
|
blobUrls: new Map(state.blobUrls).set(key, {
|
|
@@ -136,6 +140,8 @@ export const useMediaCacheStore = create<MediaCacheStore>()(
|
|
|
136
140
|
// Create new blob URL
|
|
137
141
|
const blob = new Blob([content], { type: mimeType });
|
|
138
142
|
const url = URL.createObjectURL(blob);
|
|
143
|
+
const sizeMB = (content.byteLength / 1024 / 1024).toFixed(2);
|
|
144
|
+
cacheDebug.debug(`Blob URL created: ${key}`, { mimeType, size: `${sizeMB}MB` });
|
|
139
145
|
set(
|
|
140
146
|
(state) => ({
|
|
141
147
|
blobUrls: new Map(state.blobUrls).set(key, {
|
|
@@ -156,6 +162,7 @@ export const useMediaCacheStore = create<MediaCacheStore>()(
|
|
|
156
162
|
|
|
157
163
|
if (entry.refCount <= 1) {
|
|
158
164
|
// Last reference - revoke and remove
|
|
165
|
+
cacheDebug.debug(`Blob URL revoked: ${key}`);
|
|
159
166
|
URL.revokeObjectURL(entry.url);
|
|
160
167
|
set(
|
|
161
168
|
(state) => {
|
|
@@ -168,6 +175,7 @@ export const useMediaCacheStore = create<MediaCacheStore>()(
|
|
|
168
175
|
);
|
|
169
176
|
} else {
|
|
170
177
|
// Decrement ref count
|
|
178
|
+
cacheDebug.debug(`Blob URL released: ${key}`, { refCount: entry.refCount - 1 });
|
|
171
179
|
set(
|
|
172
180
|
(state) => ({
|
|
173
181
|
blobUrls: new Map(state.blobUrls).set(key, {
|
|
@@ -315,6 +323,8 @@ export const useMediaCacheStore = create<MediaCacheStore>()(
|
|
|
315
323
|
// ========== Global ==========
|
|
316
324
|
|
|
317
325
|
clearCache: () => {
|
|
326
|
+
const stats = get().getCacheStats();
|
|
327
|
+
cacheDebug.info('Clearing cache', stats);
|
|
318
328
|
// Revoke all blob URLs before clearing
|
|
319
329
|
get().blobUrls.forEach(({ url }) => URL.revokeObjectURL(url));
|
|
320
330
|
set(initialState, false, 'clearCache');
|
|
@@ -20,6 +20,7 @@ import { useWavesurfer } from '@wavesurfer/react';
|
|
|
20
20
|
import type { AudioContextState, AudioSource, WaveformOptions } from '../types';
|
|
21
21
|
import { useSharedWebAudio, useAudioAnalysis } from '../hooks';
|
|
22
22
|
import { useAudioCache } from '../../../stores/mediaCache';
|
|
23
|
+
import { audioDebug } from '../utils/debug';
|
|
23
24
|
|
|
24
25
|
// =============================================================================
|
|
25
26
|
// CONTEXT
|
|
@@ -85,6 +86,54 @@ export function AudioProvider({
|
|
|
85
86
|
// Use official wavesurfer-react hook
|
|
86
87
|
const { wavesurfer, isReady, isPlaying, currentTime } = useWavesurfer(options);
|
|
87
88
|
|
|
89
|
+
// Debug: Log when WaveSurfer is ready
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (isReady && wavesurfer) {
|
|
92
|
+
const duration = wavesurfer.getDuration();
|
|
93
|
+
audioDebug.load(source.uri || 'unknown');
|
|
94
|
+
audioDebug.state('READY', { duration, isReady });
|
|
95
|
+
|
|
96
|
+
// Log buffer info
|
|
97
|
+
const audio = wavesurfer.getMediaElement();
|
|
98
|
+
if (audio) {
|
|
99
|
+
audioDebug.buffer(audio.buffered, duration);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}, [isReady, wavesurfer, source.uri]);
|
|
103
|
+
|
|
104
|
+
// Debug: Log WaveSurfer events
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!wavesurfer) return;
|
|
107
|
+
|
|
108
|
+
const handleSeeking = () => {
|
|
109
|
+
const audio = wavesurfer.getMediaElement();
|
|
110
|
+
if (audio) {
|
|
111
|
+
audioDebug.seek(currentTime, audio.currentTime, wavesurfer.getDuration());
|
|
112
|
+
audioDebug.buffer(audio.buffered, wavesurfer.getDuration());
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const handleError = (err: Error) => {
|
|
117
|
+
audioDebug.error('WaveSurfer error', { error: err });
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const handleLoading = (percent: number) => {
|
|
121
|
+
if (percent === 100 || percent % 25 === 0) {
|
|
122
|
+
audioDebug.debug(`Loading: ${percent}%`);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
wavesurfer.on('seeking', handleSeeking);
|
|
127
|
+
wavesurfer.on('error', handleError);
|
|
128
|
+
wavesurfer.on('loading', handleLoading);
|
|
129
|
+
|
|
130
|
+
return () => {
|
|
131
|
+
wavesurfer.un('seeking', handleSeeking);
|
|
132
|
+
wavesurfer.un('error', handleError);
|
|
133
|
+
wavesurfer.un('loading', handleLoading);
|
|
134
|
+
};
|
|
135
|
+
}, [wavesurfer, currentTime]);
|
|
136
|
+
|
|
88
137
|
// Restore cached playback position when ready
|
|
89
138
|
useEffect(() => {
|
|
90
139
|
if (isReady && wavesurfer && source.uri) {
|
|
@@ -172,10 +221,28 @@ export function AudioProvider({
|
|
|
172
221
|
(time: number) => {
|
|
173
222
|
if (wavesurfer) {
|
|
174
223
|
const clampedTime = Math.max(0, Math.min(time, duration));
|
|
224
|
+
audioDebug.seek(currentTime, clampedTime, duration);
|
|
225
|
+
|
|
226
|
+
// Check buffer before seeking
|
|
227
|
+
const audio = wavesurfer.getMediaElement();
|
|
228
|
+
if (audio) {
|
|
229
|
+
let isInBuffer = false;
|
|
230
|
+
for (let i = 0; i < audio.buffered.length; i++) {
|
|
231
|
+
if (clampedTime >= audio.buffered.start(i) && clampedTime <= audio.buffered.end(i)) {
|
|
232
|
+
isInBuffer = true;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (!isInBuffer) {
|
|
237
|
+
audioDebug.warn(`Seeking to UNBUFFERED position: ${clampedTime.toFixed(2)}s`);
|
|
238
|
+
audioDebug.buffer(audio.buffered, duration);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
175
242
|
wavesurfer.setTime(clampedTime);
|
|
176
243
|
}
|
|
177
244
|
},
|
|
178
|
-
[wavesurfer, duration]
|
|
245
|
+
[wavesurfer, duration, currentTime]
|
|
179
246
|
);
|
|
180
247
|
|
|
181
248
|
const seekTo = useCallback(
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { useRef, useState, useCallback, useEffect } from 'react';
|
|
11
11
|
import type { AudioState, AudioControls } from './types';
|
|
12
|
+
import { audioDebug } from '../utils/debug';
|
|
12
13
|
|
|
13
14
|
// =============================================================================
|
|
14
15
|
// TYPES
|
|
@@ -102,9 +103,27 @@ export function useAudioElement(options: UseAudioElementOptions): UseAudioElemen
|
|
|
102
103
|
if (!audio || !isFinite(time)) return;
|
|
103
104
|
|
|
104
105
|
const clampedTime = Math.max(0, Math.min(time, duration));
|
|
106
|
+
audioDebug.seek(currentTime, clampedTime, duration);
|
|
107
|
+
|
|
108
|
+
// Check if target position is in buffer
|
|
109
|
+
let isInBuffer = false;
|
|
110
|
+
if (audio.buffered.length > 0) {
|
|
111
|
+
for (let i = 0; i < audio.buffered.length; i++) {
|
|
112
|
+
if (clampedTime >= audio.buffered.start(i) && clampedTime <= audio.buffered.end(i)) {
|
|
113
|
+
isInBuffer = true;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!isInBuffer) {
|
|
120
|
+
audioDebug.warn(`Seeking to UNBUFFERED position: ${clampedTime.toFixed(2)}s`);
|
|
121
|
+
audioDebug.buffer(audio.buffered, duration);
|
|
122
|
+
}
|
|
123
|
+
|
|
105
124
|
audio.currentTime = clampedTime;
|
|
106
125
|
setCurrentTime(clampedTime);
|
|
107
|
-
}, [duration]);
|
|
126
|
+
}, [duration, currentTime]);
|
|
108
127
|
|
|
109
128
|
const seekTo = useCallback((progress: number) => {
|
|
110
129
|
const clampedProgress = Math.max(0, Math.min(progress, 1));
|
|
@@ -162,18 +181,40 @@ export function useAudioElement(options: UseAudioElementOptions): UseAudioElemen
|
|
|
162
181
|
audio.loop = loop;
|
|
163
182
|
|
|
164
183
|
const handleLoadedMetadata = () => {
|
|
184
|
+
audioDebug.state('loadedmetadata', { duration: audio.duration });
|
|
165
185
|
setDuration(audio.duration);
|
|
166
186
|
setIsReady(true);
|
|
167
187
|
onDurationChange?.(audio.duration);
|
|
168
188
|
};
|
|
169
189
|
|
|
170
190
|
const handleCanPlay = () => {
|
|
191
|
+
audioDebug.state('canplay', { duration: audio.duration, buffered: audio.buffered.length });
|
|
192
|
+
audioDebug.buffer(audio.buffered, audio.duration);
|
|
171
193
|
setIsReady(true);
|
|
172
194
|
if (autoPlay) {
|
|
173
195
|
audio.play().catch(() => {});
|
|
174
196
|
}
|
|
175
197
|
};
|
|
176
198
|
|
|
199
|
+
const handleSeeking = () => {
|
|
200
|
+
audioDebug.event('seeking', { currentTime: audio.currentTime });
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const handleSeeked = () => {
|
|
204
|
+
audioDebug.event('seeked', { currentTime: audio.currentTime });
|
|
205
|
+
audioDebug.buffer(audio.buffered, audio.duration);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const handleWaiting = () => {
|
|
209
|
+
audioDebug.warn('WAITING - buffering...');
|
|
210
|
+
audioDebug.buffer(audio.buffered, audio.duration);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const handleStalled = () => {
|
|
214
|
+
audioDebug.warn('STALLED - network issue');
|
|
215
|
+
audioDebug.buffer(audio.buffered, audio.duration);
|
|
216
|
+
};
|
|
217
|
+
|
|
177
218
|
const handlePlay = () => {
|
|
178
219
|
setIsPlaying(true);
|
|
179
220
|
onPlay?.();
|
|
@@ -204,6 +245,7 @@ export function useAudioElement(options: UseAudioElementOptions): UseAudioElemen
|
|
|
204
245
|
const err = new Error(
|
|
205
246
|
audio.error?.message || 'Audio loading failed'
|
|
206
247
|
);
|
|
248
|
+
audioDebug.error('Audio error', { code: audio.error?.code, message: audio.error?.message });
|
|
207
249
|
setError(err);
|
|
208
250
|
setIsReady(false);
|
|
209
251
|
onError?.(err);
|
|
@@ -224,6 +266,10 @@ export function useAudioElement(options: UseAudioElementOptions): UseAudioElemen
|
|
|
224
266
|
audio.addEventListener('progress', handleProgress);
|
|
225
267
|
audio.addEventListener('error', handleError);
|
|
226
268
|
audio.addEventListener('volumechange', handleVolumeChange);
|
|
269
|
+
audio.addEventListener('seeking', handleSeeking);
|
|
270
|
+
audio.addEventListener('seeked', handleSeeked);
|
|
271
|
+
audio.addEventListener('waiting', handleWaiting);
|
|
272
|
+
audio.addEventListener('stalled', handleStalled);
|
|
227
273
|
|
|
228
274
|
return () => {
|
|
229
275
|
audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
|
@@ -235,6 +281,10 @@ export function useAudioElement(options: UseAudioElementOptions): UseAudioElemen
|
|
|
235
281
|
audio.removeEventListener('progress', handleProgress);
|
|
236
282
|
audio.removeEventListener('error', handleError);
|
|
237
283
|
audio.removeEventListener('volumechange', handleVolumeChange);
|
|
284
|
+
audio.removeEventListener('seeking', handleSeeking);
|
|
285
|
+
audio.removeEventListener('seeked', handleSeeked);
|
|
286
|
+
audio.removeEventListener('waiting', handleWaiting);
|
|
287
|
+
audio.removeEventListener('stalled', handleStalled);
|
|
238
288
|
};
|
|
239
289
|
}, [
|
|
240
290
|
autoPlay,
|
|
@@ -254,6 +304,7 @@ export function useAudioElement(options: UseAudioElementOptions): UseAudioElemen
|
|
|
254
304
|
const audio = audioRef.current;
|
|
255
305
|
if (!audio) return;
|
|
256
306
|
|
|
307
|
+
audioDebug.load(src);
|
|
257
308
|
setIsReady(false);
|
|
258
309
|
setError(null);
|
|
259
310
|
setCurrentTime(0);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AudioPlayer Debug Logger
|
|
3
|
+
*
|
|
4
|
+
* Uses universal logger with media-specific helpers.
|
|
5
|
+
* Logs go to both console (dev) and zustand store (for Console panel).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createMediaLogger } from '../../../lib/logger';
|
|
9
|
+
|
|
10
|
+
export const audioDebug = createMediaLogger('AudioPlayer');
|
|
11
|
+
|
|
12
|
+
export default audioDebug;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { useEffect, useState, useRef, useCallback } from 'react';
|
|
16
16
|
import { ImageIcon, AlertCircle } from 'lucide-react';
|
|
17
17
|
import { TransformWrapper, TransformComponent, useControls } from 'react-zoom-pan-pinch';
|
|
18
|
-
import { cn, Dialog, DialogContent, Alert, AlertDescription } from '@djangocfg/ui-core';
|
|
18
|
+
import { cn, Dialog, DialogContent, DialogTitle, Alert, AlertDescription } from '@djangocfg/ui-core';
|
|
19
19
|
|
|
20
20
|
import { ImageToolbar } from './ImageToolbar';
|
|
21
21
|
import { ImageInfo } from './ImageInfo';
|
|
@@ -156,6 +156,7 @@ export function ImageViewer({ file, content, src: directSrc, inDialog = false }:
|
|
|
156
156
|
minScale={0.1}
|
|
157
157
|
maxScale={8}
|
|
158
158
|
centerOnInit
|
|
159
|
+
centerZoomedOut
|
|
159
160
|
onTransformed={(ref, state) => {
|
|
160
161
|
setScale(state.scale);
|
|
161
162
|
controlsRef.current = ref;
|
|
@@ -224,16 +225,13 @@ export function ImageViewer({ file, content, src: directSrc, inDialog = false }:
|
|
|
224
225
|
{/* Fullscreen dialog */}
|
|
225
226
|
{!inDialog && (
|
|
226
227
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
|
227
|
-
<DialogContent className="max-w-[95vw] max-h-[95vh] w-[95vw] h-[95vh] p-0 overflow-hidden [&>button]:hidden">
|
|
228
|
-
<
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
<ImageViewer file={file} content={content} src={directSrc} inDialog />
|
|
235
|
-
</div>
|
|
236
|
-
</div>
|
|
228
|
+
<DialogContent className="max-w-[95vw] max-h-[95vh] w-[95vw] h-[95vh] p-0 overflow-hidden [&>button]:hidden flex flex-col">
|
|
229
|
+
<DialogTitle className="sr-only">{file.name}</DialogTitle>
|
|
230
|
+
<div className="flex items-center justify-between px-4 py-2 border-b shrink-0">
|
|
231
|
+
<span className="text-sm font-medium truncate">{file.name}</span>
|
|
232
|
+
</div>
|
|
233
|
+
<div className="flex-1 min-h-0">
|
|
234
|
+
<ImageViewer file={file} content={content} src={directSrc} inDialog />
|
|
237
235
|
</div>
|
|
238
236
|
</DialogContent>
|
|
239
237
|
</Dialog>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { useState, useEffect, useRef } from 'react';
|
|
8
8
|
import { useImageCache, generateContentKey } from '../../../stores/mediaCache';
|
|
9
|
-
import { createLQIP, MAX_IMAGE_SIZE, WARNING_IMAGE_SIZE, PROGRESSIVE_LOADING_THRESHOLD } from '../utils';
|
|
9
|
+
import { createLQIP, MAX_IMAGE_SIZE, WARNING_IMAGE_SIZE, PROGRESSIVE_LOADING_THRESHOLD, imageDebug } from '../utils';
|
|
10
10
|
|
|
11
11
|
// =============================================================================
|
|
12
12
|
// TYPES
|
|
@@ -72,6 +72,7 @@ export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadin
|
|
|
72
72
|
|
|
73
73
|
// Direct URL mode - use as-is without blob conversion
|
|
74
74
|
if (directSrc) {
|
|
75
|
+
imageDebug.load(directSrc, 'url');
|
|
75
76
|
setSrc(directSrc);
|
|
76
77
|
setIsFullyLoaded(true);
|
|
77
78
|
return;
|
|
@@ -85,7 +86,9 @@ export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadin
|
|
|
85
86
|
// Size validation - reject oversized images
|
|
86
87
|
if (size > MAX_IMAGE_SIZE) {
|
|
87
88
|
const sizeMB = (size / 1024 / 1024).toFixed(1);
|
|
88
|
-
|
|
89
|
+
const errorMsg = `Image too large: ${sizeMB}MB (maximum: 50MB)`;
|
|
90
|
+
imageDebug.error(errorMsg, { size, sizeMB, maxSize: MAX_IMAGE_SIZE });
|
|
91
|
+
setError(errorMsg);
|
|
89
92
|
setSrc(null);
|
|
90
93
|
return;
|
|
91
94
|
}
|
|
@@ -93,13 +96,14 @@ export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadin
|
|
|
93
96
|
// Warn about large images
|
|
94
97
|
if (size > WARNING_IMAGE_SIZE) {
|
|
95
98
|
const sizeMB = (size / 1024 / 1024).toFixed(1);
|
|
96
|
-
|
|
99
|
+
imageDebug.warn(`Large image: ${sizeMB}MB - may impact performance`);
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
// Handle string content (data URLs or binary strings)
|
|
100
103
|
if (typeof content === 'string') {
|
|
101
104
|
// Pass through data URLs directly
|
|
102
105
|
if (content.startsWith('data:')) {
|
|
106
|
+
imageDebug.load(content.slice(0, 50) + '...', 'data-url');
|
|
103
107
|
setSrc(content);
|
|
104
108
|
return;
|
|
105
109
|
}
|
|
@@ -110,6 +114,8 @@ export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadin
|
|
|
110
114
|
const contentKey = generateContentKey(buffer);
|
|
111
115
|
contentKeyRef.current = contentKey;
|
|
112
116
|
const url = getOrCreateBlobUrl(contentKey, buffer, mimeType || 'image/png');
|
|
117
|
+
imageDebug.load(url, 'blob');
|
|
118
|
+
imageDebug.state('loaded', { size, mimeType, contentKey });
|
|
113
119
|
setSrc(url);
|
|
114
120
|
return;
|
|
115
121
|
}
|
|
@@ -118,6 +124,8 @@ export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadin
|
|
|
118
124
|
const contentKey = generateContentKey(content);
|
|
119
125
|
contentKeyRef.current = contentKey;
|
|
120
126
|
const url = getOrCreateBlobUrl(contentKey, content, mimeType || 'image/png');
|
|
127
|
+
imageDebug.load(url, 'blob');
|
|
128
|
+
imageDebug.state('loaded', { size, mimeType, contentKey });
|
|
121
129
|
setSrc(url);
|
|
122
130
|
|
|
123
131
|
return () => {
|
|
@@ -137,10 +145,12 @@ export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadin
|
|
|
137
145
|
}
|
|
138
146
|
|
|
139
147
|
setIsFullyLoaded(false);
|
|
148
|
+
imageDebug.state('progressive loading', { size });
|
|
140
149
|
|
|
141
150
|
// Create low-quality placeholder
|
|
142
151
|
createLQIP(src).then((placeholder) => {
|
|
143
152
|
if (placeholder) {
|
|
153
|
+
imageDebug.debug('LQIP created');
|
|
144
154
|
setLqip(placeholder);
|
|
145
155
|
}
|
|
146
156
|
});
|
|
@@ -148,10 +158,14 @@ export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadin
|
|
|
148
158
|
// Pre-load full image
|
|
149
159
|
const img = new Image();
|
|
150
160
|
img.onload = () => {
|
|
161
|
+
imageDebug.state('fully loaded');
|
|
151
162
|
setIsFullyLoaded(true);
|
|
152
163
|
};
|
|
164
|
+
img.onerror = () => {
|
|
165
|
+
imageDebug.error('Failed to load full image');
|
|
166
|
+
};
|
|
153
167
|
img.src = src;
|
|
154
|
-
}, [src, useProgressiveLoading]);
|
|
168
|
+
}, [src, useProgressiveLoading, size]);
|
|
155
169
|
|
|
156
170
|
return {
|
|
157
171
|
src,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImageViewer Debug Logger
|
|
3
|
+
*
|
|
4
|
+
* Uses universal logger with media-specific helpers.
|
|
5
|
+
* Logs go to both console (dev) and zustand store (for Console panel).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createMediaLogger } from '../../../lib/logger';
|
|
9
|
+
|
|
10
|
+
export const imageDebug = createMediaLogger('ImageViewer');
|
|
11
|
+
|
|
12
|
+
export default imageDebug;
|
|
@@ -11,6 +11,7 @@ import { cn } from '@djangocfg/ui-core/lib';
|
|
|
11
11
|
import { Preloader, AspectRatio } from '@djangocfg/ui-core';
|
|
12
12
|
|
|
13
13
|
import type { NativeProviderProps, VideoPlayerRef } from '../types';
|
|
14
|
+
import { videoDebug } from '../utils/debug';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Get video URL from source
|
|
@@ -57,6 +58,11 @@ export const NativeProvider = forwardRef<VideoPlayerRef, NativeProviderProps>(
|
|
|
57
58
|
|
|
58
59
|
const videoUrl = getVideoUrl(source);
|
|
59
60
|
|
|
61
|
+
// Debug: Log video source
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
videoDebug.load(videoUrl, source.type);
|
|
64
|
+
}, [videoUrl, source.type]);
|
|
65
|
+
|
|
60
66
|
// Expose video element methods via ref
|
|
61
67
|
useImperativeHandle(
|
|
62
68
|
ref,
|
|
@@ -122,6 +128,56 @@ export const NativeProvider = forwardRef<VideoPlayerRef, NativeProviderProps>(
|
|
|
122
128
|
};
|
|
123
129
|
}, [showPreloader, preloaderTimeout]);
|
|
124
130
|
|
|
131
|
+
// Debug: Log video events
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
const video = videoRef.current;
|
|
134
|
+
if (!video) return;
|
|
135
|
+
|
|
136
|
+
const handleLoadedMetadata = () => {
|
|
137
|
+
videoDebug.state('loadedmetadata', { duration: video.duration });
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const handleCanPlayDebug = () => {
|
|
141
|
+
videoDebug.state('canplay', { duration: video.duration, buffered: video.buffered.length });
|
|
142
|
+
videoDebug.buffer(video.buffered, video.duration);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const handleSeeking = () => {
|
|
146
|
+
videoDebug.event('seeking', { currentTime: video.currentTime });
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const handleSeeked = () => {
|
|
150
|
+
videoDebug.event('seeked', { currentTime: video.currentTime });
|
|
151
|
+
videoDebug.buffer(video.buffered, video.duration);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const handleWaiting = () => {
|
|
155
|
+
videoDebug.warn('WAITING - buffering...');
|
|
156
|
+
videoDebug.buffer(video.buffered, video.duration);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const handleStalled = () => {
|
|
160
|
+
videoDebug.warn('STALLED - network issue');
|
|
161
|
+
videoDebug.buffer(video.buffered, video.duration);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
video.addEventListener('loadedmetadata', handleLoadedMetadata);
|
|
165
|
+
video.addEventListener('canplay', handleCanPlayDebug);
|
|
166
|
+
video.addEventListener('seeking', handleSeeking);
|
|
167
|
+
video.addEventListener('seeked', handleSeeked);
|
|
168
|
+
video.addEventListener('waiting', handleWaiting);
|
|
169
|
+
video.addEventListener('stalled', handleStalled);
|
|
170
|
+
|
|
171
|
+
return () => {
|
|
172
|
+
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
|
173
|
+
video.removeEventListener('canplay', handleCanPlayDebug);
|
|
174
|
+
video.removeEventListener('seeking', handleSeeking);
|
|
175
|
+
video.removeEventListener('seeked', handleSeeked);
|
|
176
|
+
video.removeEventListener('waiting', handleWaiting);
|
|
177
|
+
video.removeEventListener('stalled', handleStalled);
|
|
178
|
+
};
|
|
179
|
+
}, []);
|
|
180
|
+
|
|
125
181
|
const handleContextMenu = (e: React.MouseEvent) => {
|
|
126
182
|
if (disableContextMenu) {
|
|
127
183
|
e.preventDefault();
|
|
@@ -129,8 +185,11 @@ export const NativeProvider = forwardRef<VideoPlayerRef, NativeProviderProps>(
|
|
|
129
185
|
};
|
|
130
186
|
|
|
131
187
|
const handleError = (e: React.SyntheticEvent<HTMLVideoElement>) => {
|
|
188
|
+
const video = e.currentTarget;
|
|
189
|
+
const errorMsg = video.error?.message || 'Video playback error';
|
|
190
|
+
videoDebug.error('Video error', { code: video.error?.code, message: errorMsg });
|
|
132
191
|
setIsLoading(false);
|
|
133
|
-
onError?.(
|
|
192
|
+
onError?.(errorMsg);
|
|
134
193
|
};
|
|
135
194
|
|
|
136
195
|
const handleTimeUpdate = () => {
|
|
@@ -17,6 +17,7 @@ import { Preloader, AspectRatio } from '@djangocfg/ui-core';
|
|
|
17
17
|
import { useVideoCache, generateContentKey } from '../../../stores/mediaCache';
|
|
18
18
|
|
|
19
19
|
import type { StreamProviderProps, VideoPlayerRef, StreamSource, BlobSource, DataUrlSource, ErrorFallbackProps } from '../types';
|
|
20
|
+
import { videoDebug } from '../utils/debug';
|
|
20
21
|
|
|
21
22
|
/** Default error fallback UI */
|
|
22
23
|
function DefaultErrorFallback({ error }: ErrorFallbackProps) {
|
|
@@ -162,6 +163,7 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
|
|
|
162
163
|
streamSource.path,
|
|
163
164
|
streamSource.getStreamUrl
|
|
164
165
|
);
|
|
166
|
+
videoDebug.load(url, 'stream');
|
|
165
167
|
setVideoUrl(url);
|
|
166
168
|
break;
|
|
167
169
|
}
|
|
@@ -177,17 +179,20 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
|
|
|
177
179
|
blobSource.data,
|
|
178
180
|
blobSource.mimeType || 'video/mp4'
|
|
179
181
|
);
|
|
182
|
+
videoDebug.load(url, 'blob');
|
|
180
183
|
setVideoUrl(url);
|
|
181
184
|
break;
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
case 'data-url': {
|
|
185
188
|
const dataUrlSource = source as DataUrlSource;
|
|
189
|
+
videoDebug.load(dataUrlSource.data.slice(0, 50) + '...', 'data-url');
|
|
186
190
|
setVideoUrl(dataUrlSource.data);
|
|
187
191
|
break;
|
|
188
192
|
}
|
|
189
193
|
|
|
190
194
|
default:
|
|
195
|
+
videoDebug.error('Invalid video source type', { type: (source as { type: string }).type });
|
|
191
196
|
setVideoUrl(null);
|
|
192
197
|
setHasError(true);
|
|
193
198
|
setErrorMessage('Invalid video source');
|
|
@@ -217,17 +222,23 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
|
|
|
217
222
|
|
|
218
223
|
// Restore cached playback position when video is ready
|
|
219
224
|
const handleCanPlay = useCallback(() => {
|
|
225
|
+
const video = videoRef.current;
|
|
226
|
+
if (video) {
|
|
227
|
+
videoDebug.state('canplay', { duration: video.duration, buffered: video.buffered.length });
|
|
228
|
+
videoDebug.buffer(video.buffered, video.duration);
|
|
229
|
+
}
|
|
220
230
|
setIsLoading(false);
|
|
221
231
|
|
|
222
232
|
// Restore position from cache
|
|
223
233
|
const sourceKey = getSourceKey();
|
|
224
|
-
if (sourceKey &&
|
|
234
|
+
if (sourceKey && video) {
|
|
225
235
|
const cachedPosition = getVideoPosition(sourceKey);
|
|
226
236
|
if (cachedPosition && cachedPosition > 0) {
|
|
227
|
-
const duration =
|
|
237
|
+
const duration = video.duration;
|
|
228
238
|
// Only restore if position is valid (not at the end)
|
|
229
239
|
if (cachedPosition < duration - 1) {
|
|
230
|
-
|
|
240
|
+
videoDebug.debug(`Restoring position: ${cachedPosition}s`);
|
|
241
|
+
video.currentTime = cachedPosition;
|
|
231
242
|
}
|
|
232
243
|
}
|
|
233
244
|
}
|
|
@@ -303,12 +314,59 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
|
|
|
303
314
|
};
|
|
304
315
|
|
|
305
316
|
const handleError = () => {
|
|
317
|
+
const video = videoRef.current;
|
|
318
|
+
if (video) {
|
|
319
|
+
videoDebug.error('Video error', { code: video.error?.code, message: video.error?.message });
|
|
320
|
+
}
|
|
306
321
|
setIsLoading(false);
|
|
307
322
|
setHasError(true);
|
|
308
323
|
setErrorMessage('Failed to load video');
|
|
309
324
|
onError?.('Video playback error');
|
|
310
325
|
};
|
|
311
326
|
|
|
327
|
+
// Debug: Log video events
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
const video = videoRef.current;
|
|
330
|
+
if (!video) return;
|
|
331
|
+
|
|
332
|
+
const handleLoadedMetadata = () => {
|
|
333
|
+
videoDebug.state('loadedmetadata', { duration: video.duration });
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const handleSeeking = () => {
|
|
337
|
+
videoDebug.event('seeking', { currentTime: video.currentTime });
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const handleSeeked = () => {
|
|
341
|
+
videoDebug.event('seeked', { currentTime: video.currentTime });
|
|
342
|
+
videoDebug.buffer(video.buffered, video.duration);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const handleWaiting = () => {
|
|
346
|
+
videoDebug.warn('WAITING - buffering...');
|
|
347
|
+
videoDebug.buffer(video.buffered, video.duration);
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const handleStalled = () => {
|
|
351
|
+
videoDebug.warn('STALLED - network issue');
|
|
352
|
+
videoDebug.buffer(video.buffered, video.duration);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
video.addEventListener('loadedmetadata', handleLoadedMetadata);
|
|
356
|
+
video.addEventListener('seeking', handleSeeking);
|
|
357
|
+
video.addEventListener('seeked', handleSeeked);
|
|
358
|
+
video.addEventListener('waiting', handleWaiting);
|
|
359
|
+
video.addEventListener('stalled', handleStalled);
|
|
360
|
+
|
|
361
|
+
return () => {
|
|
362
|
+
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
|
363
|
+
video.removeEventListener('seeking', handleSeeking);
|
|
364
|
+
video.removeEventListener('seeked', handleSeeked);
|
|
365
|
+
video.removeEventListener('waiting', handleWaiting);
|
|
366
|
+
video.removeEventListener('stalled', handleStalled);
|
|
367
|
+
};
|
|
368
|
+
}, [videoUrl]);
|
|
369
|
+
|
|
312
370
|
// Determine if we should use AspectRatio wrapper or fill mode
|
|
313
371
|
const isFillMode = aspectRatio === 'fill';
|
|
314
372
|
const computedAspectRatio = aspectRatio === 'auto' || aspectRatio === 'fill' ? undefined : aspectRatio;
|
|
@@ -19,6 +19,7 @@ import { useVideoCache } from '../../../stores/mediaCache';
|
|
|
19
19
|
|
|
20
20
|
import type { MediaPlayerInstance, PlayerSrc } from '@vidstack/react';
|
|
21
21
|
import type { VidstackProviderProps, VideoPlayerRef, ErrorFallbackProps } from '../types';
|
|
22
|
+
import { videoDebug } from '../utils/debug';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Convert source to Vidstack-compatible format
|
|
@@ -136,6 +137,12 @@ export const VidstackProvider = forwardRef<VideoPlayerRef, VidstackProviderProps
|
|
|
136
137
|
// Get Vidstack-compatible source URL
|
|
137
138
|
const vidstackSrc = useMemo(() => getVidstackSrc(source), [source]);
|
|
138
139
|
|
|
140
|
+
// Debug: Log video source
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
const srcString = typeof vidstackSrc === 'string' ? vidstackSrc : (vidstackSrc as { src: string }).src;
|
|
143
|
+
videoDebug.load(srcString, source.type);
|
|
144
|
+
}, [vidstackSrc, source.type]);
|
|
145
|
+
|
|
139
146
|
// Retry function
|
|
140
147
|
const retry = useCallback(() => {
|
|
141
148
|
setHasError(false);
|
|
@@ -204,6 +211,7 @@ export const VidstackProvider = forwardRef<VideoPlayerRef, VidstackProviderProps
|
|
|
204
211
|
const handleError = (detail: unknown) => {
|
|
205
212
|
const error = detail as { message?: string };
|
|
206
213
|
const msg = error?.message || 'Video playback error';
|
|
214
|
+
videoDebug.error('Vidstack error', { message: msg });
|
|
207
215
|
setHasError(true);
|
|
208
216
|
setErrorMessage(msg);
|
|
209
217
|
onError?.(msg);
|
|
@@ -212,16 +220,26 @@ export const VidstackProvider = forwardRef<VideoPlayerRef, VidstackProviderProps
|
|
|
212
220
|
const handleLoadStart = () => onLoadStart?.();
|
|
213
221
|
|
|
214
222
|
const handleCanPlay = useCallback(() => {
|
|
223
|
+
const player = playerRef.current;
|
|
224
|
+
if (player) {
|
|
225
|
+
videoDebug.state('canplay', { duration: player.duration });
|
|
226
|
+
// Log buffer state if media element is available
|
|
227
|
+
const mediaEl = (player.provider as { media?: HTMLVideoElement } | null)?.media;
|
|
228
|
+
if (mediaEl?.buffered) {
|
|
229
|
+
videoDebug.buffer(mediaEl.buffered, player.duration);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
215
232
|
setHasError(false);
|
|
216
233
|
|
|
217
234
|
// Restore position from cache (only once per source)
|
|
218
|
-
if (sourceKey &&
|
|
235
|
+
if (sourceKey && player && !hasRestoredPositionRef.current) {
|
|
219
236
|
const cachedPosition = getVideoPosition(sourceKey);
|
|
220
237
|
if (cachedPosition && cachedPosition > 0) {
|
|
221
|
-
const duration =
|
|
238
|
+
const duration = player.duration;
|
|
222
239
|
// Only restore if position is valid (not at the end)
|
|
223
240
|
if (cachedPosition < duration - 1) {
|
|
224
|
-
|
|
241
|
+
videoDebug.debug(`Restoring position: ${cachedPosition}s`);
|
|
242
|
+
player.currentTime = cachedPosition;
|
|
225
243
|
}
|
|
226
244
|
}
|
|
227
245
|
hasRestoredPositionRef.current = true;
|
|
@@ -252,6 +270,52 @@ export const VidstackProvider = forwardRef<VideoPlayerRef, VidstackProviderProps
|
|
|
252
270
|
lastSavedTimeRef.current = 0;
|
|
253
271
|
}, [sourceKey]);
|
|
254
272
|
|
|
273
|
+
// Debug: Log player events
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
const player = playerRef.current;
|
|
276
|
+
if (!player) return;
|
|
277
|
+
|
|
278
|
+
const handleSeeking = () => {
|
|
279
|
+
videoDebug.event('seeking', { currentTime: player.currentTime });
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const handleSeeked = () => {
|
|
283
|
+
videoDebug.event('seeked', { currentTime: player.currentTime });
|
|
284
|
+
const mediaEl = (player.provider as { media?: HTMLVideoElement } | null)?.media;
|
|
285
|
+
if (mediaEl?.buffered) {
|
|
286
|
+
videoDebug.buffer(mediaEl.buffered, player.duration);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const handleWaiting = () => {
|
|
291
|
+
videoDebug.warn('WAITING - buffering...');
|
|
292
|
+
const mediaEl = (player.provider as { media?: HTMLVideoElement } | null)?.media;
|
|
293
|
+
if (mediaEl?.buffered) {
|
|
294
|
+
videoDebug.buffer(mediaEl.buffered, player.duration);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const handleStalled = () => {
|
|
299
|
+
videoDebug.warn('STALLED - network issue');
|
|
300
|
+
const mediaEl = (player.provider as { media?: HTMLVideoElement } | null)?.media;
|
|
301
|
+
if (mediaEl?.buffered) {
|
|
302
|
+
videoDebug.buffer(mediaEl.buffered, player.duration);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
player.addEventListener('seeking', handleSeeking);
|
|
307
|
+
player.addEventListener('seeked', handleSeeked);
|
|
308
|
+
player.addEventListener('waiting', handleWaiting);
|
|
309
|
+
player.addEventListener('stalled', handleStalled);
|
|
310
|
+
|
|
311
|
+
return () => {
|
|
312
|
+
player.removeEventListener('seeking', handleSeeking);
|
|
313
|
+
player.removeEventListener('seeked', handleSeeked);
|
|
314
|
+
player.removeEventListener('waiting', handleWaiting);
|
|
315
|
+
player.removeEventListener('stalled', handleStalled);
|
|
316
|
+
};
|
|
317
|
+
}, [vidstackSrc]);
|
|
318
|
+
|
|
255
319
|
// Determine layout mode
|
|
256
320
|
const isFillMode = aspectRatio === 'fill';
|
|
257
321
|
const computedAspectRatio = aspectRatio === 'auto' || aspectRatio === 'fill' ? undefined : aspectRatio;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VideoPlayer Debug Logger
|
|
3
|
+
*
|
|
4
|
+
* Uses universal logger with media-specific helpers.
|
|
5
|
+
* Logs go to both console (dev) and zustand store (for Console panel).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createMediaLogger } from '../../../lib/logger';
|
|
9
|
+
|
|
10
|
+
export const videoDebug = createMediaLogger('VideoPlayer');
|
|
11
|
+
|
|
12
|
+
export default videoDebug;
|