feat(video): Implement MP4 format support across frontend and backend

- Updated VideoModal to display web compatibility status for video formats.
- Enhanced VideoPlayer to dynamically fetch video MIME types and handle MP4 streaming.
- Introduced video file utilities for better handling of video formats and MIME types.
- Modified CameraConfig interface to include new video recording settings (format, codec, quality).
- Created comprehensive documentation for MP4 format integration and frontend implementation.
- Ensured backward compatibility with existing AVI files while promoting MP4 as the preferred format.
- Added validation and error handling for video format configurations.
This commit is contained in:
Alireza Vaezi
2025-08-04 16:21:22 -04:00
parent ddecbf7baa
commit 14adfbd973
36 changed files with 1446 additions and 4578 deletions

View File

@@ -15,6 +15,7 @@ import {
getStatusBadgeClass,
getResolutionString,
formatDuration,
isWebCompatible,
} from '../utils/videoUtils';
interface VideoModalProps {
@@ -103,13 +104,21 @@ export const VideoModal: React.FC<VideoModalProps> = ({
<div className="w-full lg:w-80 bg-gray-50 overflow-y-auto">
<div className="p-4 space-y-4">
{/* Status and Format */}
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-2 flex-wrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusBadgeClass(video.status)}`}>
{video.status}
</span>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${isWebCompatible(video.format)
? 'bg-green-100 text-green-800'
: 'bg-orange-100 text-orange-800'
}`}>
{getFormatDisplayName(video.format)}
</span>
{isWebCompatible(video.format) && (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
Web Compatible
</span>
)}
</div>
{/* Basic Info */}

View File

@@ -5,11 +5,11 @@
* Uses the useVideoPlayer hook for state management and provides a clean interface.
*/
import React, { forwardRef } from 'react';
import React, { forwardRef, useState, useEffect } from 'react';
import { useVideoPlayer } from '../hooks/useVideoPlayer';
import { videoApiService } from '../services/videoApi';
import { type VideoPlayerProps } from '../types';
import { formatDuration } from '../utils/videoUtils';
import { formatDuration, getVideoMimeType } from '../utils/videoUtils';
export const VideoPlayer = forwardRef<HTMLVideoElement, VideoPlayerProps>(({
fileId,
@@ -23,6 +23,10 @@ export const VideoPlayer = forwardRef<HTMLVideoElement, VideoPlayerProps>(({
onEnded,
onError,
}, forwardedRef) => {
const [videoInfo, setVideoInfo] = useState<{ filename?: string; mimeType: string }>({
mimeType: 'video/mp4' // Default to MP4
});
const { state, actions, ref } = useVideoPlayer({
autoPlay,
onPlay,
@@ -36,6 +40,26 @@ export const VideoPlayer = forwardRef<HTMLVideoElement, VideoPlayerProps>(({
const streamingUrl = videoApiService.getStreamingUrl(fileId);
// Fetch video info to determine MIME type
useEffect(() => {
const fetchVideoInfo = async () => {
try {
const info = await videoApiService.getVideoInfo(fileId);
if (info.file_id) {
// Extract filename from file_id or use a default pattern
const filename = info.file_id.includes('.') ? info.file_id : `${info.file_id}.mp4`;
const mimeType = getVideoMimeType(filename);
setVideoInfo({ filename, mimeType });
}
} catch (error) {
console.warn('Could not fetch video info, using default MIME type:', error);
// Keep default MP4 MIME type
}
};
fetchVideoInfo();
}, [fileId]);
const handleSeek = (e: React.MouseEvent<HTMLDivElement>) => {
if (!ref.current) return;
@@ -59,8 +83,13 @@ export const VideoPlayer = forwardRef<HTMLVideoElement, VideoPlayerProps>(({
className="w-full h-full bg-black"
controls={!controls} // Use native controls if custom controls are disabled
style={{ width, height }}
playsInline // Important for iOS compatibility
>
<source src={streamingUrl} type="video/mp4" />
<source src={streamingUrl} type={videoInfo.mimeType} />
{/* Fallback for MP4 if original format fails */}
{videoInfo.mimeType !== 'video/mp4' && (
<source src={streamingUrl} type="video/mp4" />
)}
Your browser does not support the video tag.
</video>

View File

@@ -1,11 +1,19 @@
/**
* Video Streaming Utilities
*
*
* Pure utility functions for video operations, formatting, and data processing.
* These functions have no side effects and can be easily tested.
* Enhanced with MP4 format support and improved file handling.
*/
import { type VideoFile, type VideoWithMetadata } from '../types';
import {
isVideoFile as isVideoFileUtil,
getVideoMimeType as getVideoMimeTypeUtil,
getVideoFormat,
isWebCompatibleFormat,
getFormatDisplayName as getFormatDisplayNameUtil
} from '../../../utils/videoFileUtils';
/**
* Format file size in bytes to human readable format
@@ -72,6 +80,20 @@ export function getRelativeTime(dateString: string): string {
}
}
/**
* Check if a filename is a video file (supports MP4, AVI, and other formats)
*/
export function isVideoFile(filename: string): boolean {
return isVideoFileUtil(filename);
}
/**
* Get MIME type for video file based on filename
*/
export function getVideoMimeType(filename: string): string {
return getVideoMimeTypeUtil(filename);
}
/**
* Extract camera name from filename if not provided
*/
@@ -85,23 +107,14 @@ export function extractCameraName(filename: string): string {
* Get video format display name
*/
export function getFormatDisplayName(format: string): string {
const formatMap: Record<string, string> = {
'avi': 'AVI',
'mp4': 'MP4',
'webm': 'WebM',
'mov': 'MOV',
'mkv': 'MKV',
};
return formatMap[format.toLowerCase()] || format.toUpperCase();
return getFormatDisplayNameUtil(format);
}
/**
* Check if video format is web-compatible
*/
export function isWebCompatible(format: string): boolean {
const webFormats = ['mp4', 'webm', 'ogg'];
return webFormats.includes(format.toLowerCase());
return isWebCompatibleFormat(format);
}
/**