@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-nextjs",
3
- "version": "2.1.76",
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.76",
62
- "@djangocfg/ui-core": "^2.1.76",
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.76",
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
@@ -1,2 +1,5 @@
1
1
  // Re-export from ui-core
2
- export * from '@djangocfg/ui-core/lib';
2
+ export * from '@djangocfg/ui-core/lib';
3
+
4
+ // Logger (consola + zustand for Console panel)
5
+ export * from './logger';
@@ -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
+ }
@@ -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;
@@ -3,3 +3,4 @@
3
3
  */
4
4
 
5
5
  export { formatTime } from './formatTime';
6
+ export { audioDebug } from './debug';
@@ -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
- <div className="h-full flex flex-col">
229
- <div className="flex items-center justify-between px-4 py-2 border-b shrink-0">
230
- <span className="text-sm font-medium truncate">{file.name}</span>
231
- </div>
232
- <div className="flex-1 min-h-0 relative">
233
- <div className="absolute inset-0">
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
- setError(`Image too large: ${sizeMB}MB (maximum: 50MB)`);
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
- console.warn(`[ImageViewer] Large image: ${sizeMB}MB - may impact performance`);
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;
@@ -14,3 +14,4 @@ export {
14
14
  ZOOM_PRESETS,
15
15
  DEFAULT_TRANSFORM,
16
16
  } from './constants';
17
+ export { imageDebug } from './debug';
@@ -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?.(e.currentTarget.error?.message || 'Video playback error');
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 && videoRef.current) {
234
+ if (sourceKey && video) {
225
235
  const cachedPosition = getVideoPosition(sourceKey);
226
236
  if (cachedPosition && cachedPosition > 0) {
227
- const duration = videoRef.current.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
- videoRef.current.currentTime = cachedPosition;
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 && playerRef.current && !hasRestoredPositionRef.current) {
235
+ if (sourceKey && player && !hasRestoredPositionRef.current) {
219
236
  const cachedPosition = getVideoPosition(sourceKey);
220
237
  if (cachedPosition && cachedPosition > 0) {
221
- const duration = playerRef.current.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
- playerRef.current.currentTime = cachedPosition;
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;
@@ -9,3 +9,4 @@ export {
9
9
  } from './resolvers';
10
10
 
11
11
  export { resolveFileSource } from './fileSource';
12
+ export { videoDebug } from './debug';