- Implemented a reusable Pagination component with first/last, previous/next, and numbered page buttons. - Added PageInfo component to display current page and total items. - Integrated pagination into VideoList component, allowing users to navigate through video pages. - Updated useVideoList hook to manage current page and total pages state. - Modified videoApi service to support pagination with offset-based API. - Enhanced VideoCard styling for better UI consistency. - Updated Tailwind CSS configuration to include custom colors and shadows for branding. - Refactored video file settings to use 'h264' codec for better compatibility.
303 lines
7.7 KiB
TypeScript
303 lines
7.7 KiB
TypeScript
/**
|
|
* Video File Utilities
|
|
*
|
|
* Utility functions for handling video files, extensions, MIME types, and format validation.
|
|
* Supports both MP4 and AVI formats with backward compatibility.
|
|
*/
|
|
|
|
/**
|
|
* Supported video file extensions
|
|
*/
|
|
export const VIDEO_EXTENSIONS = ['.mp4', '.avi', '.webm', '.mov', '.mkv'] as const;
|
|
|
|
/**
|
|
* Video format to MIME type mapping
|
|
*/
|
|
export const VIDEO_MIME_TYPES: Record<string, string> = {
|
|
'mp4': 'video/mp4',
|
|
'avi': 'video/x-msvideo',
|
|
'webm': 'video/webm',
|
|
'mov': 'video/quicktime',
|
|
'mkv': 'video/x-matroska',
|
|
} as const;
|
|
|
|
/**
|
|
* Video codec options for each format
|
|
*/
|
|
export const VIDEO_CODECS: Record<string, string[]> = {
|
|
'mp4': ['mp4v', 'h264', 'h265'],
|
|
'avi': ['XVID', 'MJPG', 'h264'],
|
|
'webm': ['vp8', 'vp9'],
|
|
'mov': ['h264', 'h265', 'prores'],
|
|
'mkv': ['h264', 'h265', 'vp9'],
|
|
} as const;
|
|
|
|
/**
|
|
* Check if a filename has a video file extension
|
|
*/
|
|
export function isVideoFile(filename: string): boolean {
|
|
if (!filename || typeof filename !== 'string') {
|
|
return false;
|
|
}
|
|
|
|
const lowerFilename = filename.toLowerCase();
|
|
return VIDEO_EXTENSIONS.some(ext => lowerFilename.endsWith(ext));
|
|
}
|
|
|
|
/**
|
|
* Extract file extension from filename (without the dot)
|
|
*/
|
|
export function getFileExtension(filename: string): string {
|
|
if (!filename || typeof filename !== 'string') {
|
|
return '';
|
|
}
|
|
|
|
const lastDotIndex = filename.lastIndexOf('.');
|
|
if (lastDotIndex === -1 || lastDotIndex === filename.length - 1) {
|
|
return '';
|
|
}
|
|
|
|
return filename.substring(lastDotIndex + 1).toLowerCase();
|
|
}
|
|
|
|
/**
|
|
* Get video format from filename
|
|
*/
|
|
export function getVideoFormat(filename: string): string {
|
|
const extension = getFileExtension(filename);
|
|
return extension || 'unknown';
|
|
}
|
|
|
|
/**
|
|
* Get MIME type for a video file based on filename
|
|
*/
|
|
export function getVideoMimeType(filename: string): string {
|
|
const format = getVideoFormat(filename);
|
|
return VIDEO_MIME_TYPES[format] || 'video/mp4'; // Default to MP4 for new files
|
|
}
|
|
|
|
/**
|
|
* Check if a video format is web-compatible (can be played in browsers)
|
|
*/
|
|
export function isWebCompatibleFormat(format: string): boolean {
|
|
const webCompatibleFormats = ['mp4', 'webm', 'ogg'];
|
|
return webCompatibleFormats.includes(format.toLowerCase());
|
|
}
|
|
|
|
/**
|
|
* Get display name for video format
|
|
*/
|
|
export function getFormatDisplayName(format: string): string {
|
|
const formatNames: Record<string, string> = {
|
|
'mp4': 'MP4',
|
|
'avi': 'AVI',
|
|
'webm': 'WebM',
|
|
'mov': 'QuickTime',
|
|
'mkv': 'Matroska',
|
|
};
|
|
|
|
return formatNames[format.toLowerCase()] || format.toUpperCase();
|
|
}
|
|
|
|
/**
|
|
* Validate video format setting
|
|
*/
|
|
export function isValidVideoFormat(format: string): boolean {
|
|
const validFormats = ['mp4', 'avi', 'webm', 'mov', 'mkv'];
|
|
return validFormats.includes(format.toLowerCase());
|
|
}
|
|
|
|
/**
|
|
* Validate video codec for a given format
|
|
*/
|
|
export function isValidCodecForFormat(codec: string, format: string): boolean {
|
|
const validCodecs = VIDEO_CODECS[format.toLowerCase()];
|
|
return validCodecs ? validCodecs.includes(codec) : false;
|
|
}
|
|
|
|
/**
|
|
* Get available codecs for a video format
|
|
*/
|
|
export function getAvailableCodecs(format: string): string[] {
|
|
return VIDEO_CODECS[format.toLowerCase()] || [];
|
|
}
|
|
|
|
/**
|
|
* Validate video quality setting (0-100)
|
|
*/
|
|
export function isValidVideoQuality(quality: number): boolean {
|
|
return typeof quality === 'number' && quality >= 0 && quality <= 100;
|
|
}
|
|
|
|
/**
|
|
* Get recommended video settings for different use cases
|
|
*/
|
|
export function getRecommendedVideoSettings(useCase: 'production' | 'storage-optimized' | 'legacy') {
|
|
const settings = {
|
|
production: {
|
|
video_format: 'mp4',
|
|
video_codec: 'h264',
|
|
video_quality: 95,
|
|
},
|
|
'storage-optimized': {
|
|
video_format: 'mp4',
|
|
video_codec: 'h264',
|
|
video_quality: 85,
|
|
},
|
|
legacy: {
|
|
video_format: 'avi',
|
|
video_codec: 'XVID',
|
|
video_quality: 95,
|
|
},
|
|
};
|
|
|
|
return settings[useCase];
|
|
}
|
|
|
|
/**
|
|
* Check if video format change requires camera restart
|
|
*/
|
|
export function requiresRestart(currentFormat: string, newFormat: string): boolean {
|
|
// Format changes always require restart
|
|
return currentFormat !== newFormat;
|
|
}
|
|
|
|
/**
|
|
* Get format-specific file size estimation factor
|
|
* (relative to AVI baseline)
|
|
*/
|
|
export function getFileSizeFactor(format: string): number {
|
|
const factors: Record<string, number> = {
|
|
'mp4': 0.6, // ~40% smaller than AVI
|
|
'avi': 1.0, // baseline
|
|
'webm': 0.5, // even smaller
|
|
'mov': 0.8, // slightly smaller
|
|
'mkv': 0.7, // moderately smaller
|
|
};
|
|
|
|
return factors[format.toLowerCase()] || 1.0;
|
|
}
|
|
|
|
/**
|
|
* Estimate file size for a video recording
|
|
*/
|
|
export function estimateFileSize(
|
|
durationSeconds: number,
|
|
format: string,
|
|
quality: number,
|
|
baselineMBPerMinute: number = 30
|
|
): number {
|
|
const durationMinutes = durationSeconds / 60;
|
|
const qualityFactor = quality / 100;
|
|
const formatFactor = getFileSizeFactor(format);
|
|
|
|
return durationMinutes * baselineMBPerMinute * qualityFactor * formatFactor;
|
|
}
|
|
|
|
/**
|
|
* Generate video filename with proper extension
|
|
*/
|
|
export function generateVideoFilename(
|
|
cameraName: string,
|
|
format: string,
|
|
timestamp?: Date
|
|
): string {
|
|
const date = timestamp || new Date();
|
|
const dateStr = date.toISOString().slice(0, 19).replace(/[-:]/g, '').replace('T', '_');
|
|
const extension = format.toLowerCase();
|
|
|
|
return `${cameraName}_recording_${dateStr}.${extension}`;
|
|
}
|
|
|
|
/**
|
|
* Parse video filename to extract metadata
|
|
*/
|
|
export function parseVideoFilename(filename: string): {
|
|
cameraName?: string;
|
|
timestamp?: Date;
|
|
format: string;
|
|
isValid: boolean;
|
|
} {
|
|
const format = getVideoFormat(filename);
|
|
|
|
// Try to match pattern: cameraName_recording_YYYYMMDD_HHMMSS.ext
|
|
const match = filename.match(/^([^_]+)_recording_(\d{8})_(\d{6})\./);
|
|
|
|
if (match) {
|
|
const [, cameraName, dateStr, timeStr] = match;
|
|
const year = parseInt(dateStr.slice(0, 4));
|
|
const month = parseInt(dateStr.slice(4, 6)) - 1; // Month is 0-indexed
|
|
const day = parseInt(dateStr.slice(6, 8));
|
|
const hour = parseInt(timeStr.slice(0, 2));
|
|
const minute = parseInt(timeStr.slice(2, 4));
|
|
const second = parseInt(timeStr.slice(4, 6));
|
|
|
|
const timestamp = new Date(year, month, day, hour, minute, second);
|
|
|
|
return {
|
|
cameraName,
|
|
timestamp,
|
|
format,
|
|
isValid: true,
|
|
};
|
|
}
|
|
|
|
return {
|
|
format,
|
|
isValid: false,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Video format configuration validation
|
|
*/
|
|
export interface VideoFormatValidationResult {
|
|
isValid: boolean;
|
|
errors: string[];
|
|
warnings: string[];
|
|
}
|
|
|
|
/**
|
|
* Validate complete video format configuration
|
|
*/
|
|
export function validateVideoFormatConfig(config: {
|
|
video_format?: string;
|
|
video_codec?: string;
|
|
video_quality?: number;
|
|
}): VideoFormatValidationResult {
|
|
const errors: string[] = [];
|
|
const warnings: string[] = [];
|
|
|
|
// Validate format
|
|
if (config.video_format && !isValidVideoFormat(config.video_format)) {
|
|
errors.push(`Invalid video format: ${config.video_format}`);
|
|
}
|
|
|
|
// Validate codec
|
|
if (config.video_format && config.video_codec) {
|
|
if (!isValidCodecForFormat(config.video_codec, config.video_format)) {
|
|
errors.push(`Codec ${config.video_codec} is not valid for format ${config.video_format}`);
|
|
}
|
|
}
|
|
|
|
// Validate quality
|
|
if (config.video_quality !== undefined && !isValidVideoQuality(config.video_quality)) {
|
|
errors.push(`Video quality must be between 0 and 100, got: ${config.video_quality}`);
|
|
}
|
|
|
|
// Add warnings
|
|
if (config.video_format === 'avi') {
|
|
warnings.push('AVI format has limited web compatibility. Consider using MP4 for better browser support.');
|
|
}
|
|
|
|
if (config.video_quality && config.video_quality < 70) {
|
|
warnings.push('Low video quality may affect analysis accuracy.');
|
|
}
|
|
|
|
return {
|
|
isValid: errors.length === 0,
|
|
errors,
|
|
warnings,
|
|
};
|
|
}
|