Initial commit: Video player with HLS support
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
94
server/middleware/videos.ts
Normal file
94
server/middleware/videos.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { createReadStream, statSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const url = getRequestURL(event)
|
||||
|
||||
// Solo manejar rutas que empiecen con /videos/
|
||||
if (!url.pathname.startsWith('/videos/')) {
|
||||
return
|
||||
}
|
||||
|
||||
// Extraer el path del archivo (puede incluir subcarpetas para HLS)
|
||||
const file = url.pathname.substring('/videos/'.length)
|
||||
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
|
||||
// Validar que el path solo contenga caracteres seguros (permite subcarpetas con /)
|
||||
if (!/^[a-zA-Z0-9_\-\.\/]+$/.test(file)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Invalid file path'
|
||||
})
|
||||
}
|
||||
|
||||
const filePath = join(process.cwd(), 'videos', file)
|
||||
|
||||
try {
|
||||
// Verificar que el archivo existe
|
||||
const stats = statSync(filePath)
|
||||
|
||||
if (!stats.isFile()) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'File not found'
|
||||
})
|
||||
}
|
||||
|
||||
// Determinar el tipo MIME basado en la extensión
|
||||
const ext = file.toLowerCase().substring(file.lastIndexOf('.'))
|
||||
const mimeTypes: Record<string, string> = {
|
||||
'.mp4': 'video/mp4',
|
||||
'.webm': 'video/webm',
|
||||
'.ogg': 'video/ogg',
|
||||
'.mov': 'video/quicktime',
|
||||
'.avi': 'video/x-msvideo',
|
||||
'.mkv': 'video/x-matroska',
|
||||
'.m3u8': 'application/vnd.apple.mpegurl',
|
||||
'.ts': 'video/mp2t'
|
||||
}
|
||||
const mimeType = mimeTypes[ext] || 'application/octet-stream'
|
||||
|
||||
// Configurar headers para streaming de video
|
||||
setResponseHeaders(event, {
|
||||
'Content-Type': mimeType,
|
||||
'Content-Length': String(stats.size),
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Cache-Control': 'public, max-age=604800' // 7 días
|
||||
})
|
||||
|
||||
// Manejar range requests para seeking en el video
|
||||
const range = getRequestHeader(event, 'range')
|
||||
|
||||
if (range) {
|
||||
const parts = range.replace(/bytes=/, '').split('-')
|
||||
const start = parseInt(parts[0], 10)
|
||||
const end = parts[1] ? parseInt(parts[1], 10) : stats.size - 1
|
||||
const chunkSize = (end - start) + 1
|
||||
|
||||
setResponseStatus(event, 206) // Partial Content
|
||||
setResponseHeaders(event, {
|
||||
'Content-Range': `bytes ${start}-${end}/${stats.size}`,
|
||||
'Content-Length': String(chunkSize)
|
||||
})
|
||||
|
||||
return createReadStream(filePath, { start, end })
|
||||
}
|
||||
|
||||
// Retornar el archivo completo si no hay range request
|
||||
return createReadStream(filePath)
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'File not found'
|
||||
})
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Error reading file'
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user