diff --git a/nuxt4/app/composables/useFrigateEvents.ts b/nuxt4/app/composables/useFrigateEvents.ts index 940abb8..0455e5e 100644 --- a/nuxt4/app/composables/useFrigateEvents.ts +++ b/nuxt4/app/composables/useFrigateEvents.ts @@ -37,7 +37,7 @@ export interface FrigateEvent { } export const useFrigateEvents = () => { - const FRIGATE_PUBLIC_URL = 'https://camaras.nucleoriofrio.com' + // Ya no usamos URL publica, todo va por proxy interno const isCreating = useState('frigate_creating', () => false) const isLoadingEvents = useState('frigate_loading_events', () => false) @@ -74,17 +74,17 @@ export const useFrigateEvents = () => { } /** - * Genera la URL del clip de un evento + * Genera la URL del clip de un evento (via proxy interno) */ const getEventClipUrl = (eventId: string): string => { - return `${FRIGATE_PUBLIC_URL}/api/events/${eventId}/clip.mp4` + return `/api/frigate/events/${eventId}/clip` } /** - * Genera la URL del snapshot de un evento + * Genera la URL del snapshot de un evento (via proxy interno) */ const getEventSnapshotUrl = (eventId: string): string => { - return `${FRIGATE_PUBLIC_URL}/api/events/${eventId}/snapshot.jpg` + return `/api/frigate/events/${eventId}/snapshot` } /** diff --git a/nuxt4/server/api/frigate/events/[eventId]/clip.get.ts b/nuxt4/server/api/frigate/events/[eventId]/clip.get.ts new file mode 100644 index 0000000..abc3221 --- /dev/null +++ b/nuxt4/server/api/frigate/events/[eventId]/clip.get.ts @@ -0,0 +1,95 @@ +/** + * Proxy endpoint para obtener clips de eventos de Frigate + * GET /api/frigate/events/:eventId/clip + * Soporta range requests para seeking en video + */ + +export default defineEventHandler(async (event) => { + // Verificar autenticación via headers de Authentik + const headers = getRequestHeaders(event) + const username = headers['x-authentik-username'] + + if (!username) { + throw createError({ + statusCode: 401, + message: 'No autenticado' + }) + } + + const eventId = getRouterParam(event, 'eventId') + + if (!eventId) { + throw createError({ + statusCode: 400, + message: 'Se requiere eventId' + }) + } + + // URL interna de Frigate + const FRIGATE_URL = process.env.FRIGATE_URL || 'http://192.168.87.29:5000' + + try { + // Pasar headers de range si existen (para seeking) + const requestHeaders: Record = { + 'Accept': 'video/mp4' + } + + const rangeHeader = headers['range'] + if (rangeHeader) { + requestHeaders['Range'] = rangeHeader + } + + const response = await fetch( + `${FRIGATE_URL}/api/events/${eventId}/clip.mp4`, + { + method: 'GET', + headers: requestHeaders + } + ) + + if (!response.ok && response.status !== 206) { + throw createError({ + statusCode: response.status, + message: `Error al obtener clip: ${response.statusText}` + }) + } + + // Configurar headers de respuesta + const responseHeaders: Record = { + 'Content-Type': 'video/mp4', + 'Accept-Ranges': 'bytes' + } + + const contentLength = response.headers.get('content-length') + if (contentLength) { + responseHeaders['Content-Length'] = contentLength + } + + const contentRange = response.headers.get('content-range') + if (contentRange) { + responseHeaders['Content-Range'] = contentRange + } + + // Establecer headers + for (const [key, value] of Object.entries(responseHeaders)) { + setResponseHeader(event, key, value) + } + + // Establecer status code (200 o 206 para partial content) + setResponseStatus(event, response.status) + + // Retornar el stream del video + return response.body + } catch (error: unknown) { + console.error('[Frigate Clip Proxy] Error:', error) + + if ((error as any).statusCode) { + throw error + } + + throw createError({ + statusCode: 500, + message: (error as Error)?.message || 'Error al obtener clip' + }) + } +}) diff --git a/nuxt4/server/api/frigate/events/[eventId]/snapshot.get.ts b/nuxt4/server/api/frigate/events/[eventId]/snapshot.get.ts new file mode 100644 index 0000000..6031e48 --- /dev/null +++ b/nuxt4/server/api/frigate/events/[eventId]/snapshot.get.ts @@ -0,0 +1,73 @@ +/** + * Proxy endpoint para obtener snapshots de eventos de Frigate + * GET /api/frigate/events/:eventId/snapshot + */ + +export default defineEventHandler(async (event) => { + // Verificar autenticación via headers de Authentik + const headers = getRequestHeaders(event) + const username = headers['x-authentik-username'] + + if (!username) { + throw createError({ + statusCode: 401, + message: 'No autenticado' + }) + } + + const eventId = getRouterParam(event, 'eventId') + + if (!eventId) { + throw createError({ + statusCode: 400, + message: 'Se requiere eventId' + }) + } + + // URL interna de Frigate + const FRIGATE_URL = process.env.FRIGATE_URL || 'http://192.168.87.29:5000' + + try { + const response = await fetch( + `${FRIGATE_URL}/api/events/${eventId}/snapshot.jpg`, + { + method: 'GET', + headers: { + 'Accept': 'image/jpeg' + } + } + ) + + if (!response.ok) { + throw createError({ + statusCode: response.status, + message: `Error al obtener snapshot: ${response.statusText}` + }) + } + + // Configurar headers de respuesta + setResponseHeader(event, 'Content-Type', 'image/jpeg') + + const contentLength = response.headers.get('content-length') + if (contentLength) { + setResponseHeader(event, 'Content-Length', contentLength) + } + + // Cache por 5 minutos (los snapshots no cambian) + setResponseHeader(event, 'Cache-Control', 'public, max-age=300') + + // Retornar el stream de la imagen + return response.body + } catch (error: unknown) { + console.error('[Frigate Snapshot Proxy] Error:', error) + + if ((error as any).statusCode) { + throw error + } + + throw createError({ + statusCode: 500, + message: (error as Error)?.message || 'Error al obtener snapshot' + }) + } +})