nextav/src/lib/video-format-detector.ts

367 lines
10 KiB
TypeScript

/**
* Video format detection and support level classification
* Determines the optimal playback method for different video formats
*/
export interface VideoFormat {
type: 'direct' | 'hls' | 'local-player'; // Removed 'fallback', added 'local-player'
url: string;
mimeType?: string;
qualities?: QualityLevel[];
supportLevel: 'native' | 'hls' | 'local-player-required'; // Updated 'limited' to 'local-player-required'
warning?: string;
action?: 'launch-local-player'; // New field for local player guidance
recommendedPlayers?: string[]; // New field for player recommendations
streamInfo?: {
contentType: string;
acceptRanges: string;
supportsSeek: boolean;
authentication: string;
}; // New field for stream metadata
}
export interface QualityLevel {
html: string;
url: string;
default?: boolean;
}
export interface VideoFile {
id: number;
title: string;
path: string;
size: number;
thumbnail: string;
type: string;
codec_info?: string;
bookmark_count?: number;
avg_rating?: number;
star_count?: number;
}
// Native browser support formats (direct streaming)
const NATIVE_SUPPORTED_FORMATS = [
'mp4', // H.264, H.265
'webm', // VP8, VP9
'ogg', // Theora
'ogv'
];
// Formats that work well with HLS streaming (but .ts files should be served directly)
const HLS_COMPATIBLE_FORMATS = [
'mp4',
'm4v',
// Note: .ts files are removed from here - they should be served directly
'm2ts',
'mts'
];
// MPEG Transport Stream formats - can be served via HLS or converted
const TS_STREAM_FORMATS = [
'ts', // MPEG Transport Stream - optimal for HLS streaming
'm2ts', // Blu-ray Transport Stream
'mts' // AVCHD Transport Stream
];
// Formats with limited support (may need transcoding)
const LIMITED_SUPPORT_FORMATS = [
'avi',
'wmv',
'flv',
'mkv',
'mov', // Some MOV files may work
'3gp',
'vob'
];
/**
* Detect video format and determine optimal playback method
* TRANSCODING REMOVED: Binary decision - native browser OR local player
*/
export function detectVideoFormat(video: VideoFile): VideoFormat {
console.log('[FormatDetector] Input video:', video);
const extension = getFileExtension(video.path).toLowerCase();
console.log('[FormatDetector] Extracted extension:', extension);
const codecInfo = parseCodecInfo(video.codec_info);
console.log('[FormatDetector] Codec info:', codecInfo);
// EXPLICIT TEST: Force .avi files to local player
if (extension === 'avi') {
console.log('[FormatDetector] .avi file detected, forcing local player');
return createLocalPlayerFormat(video, extension);
}
// TRANSCODING DISABLED: No more transcoding fallback
// If codec specifically needs transcoding, go directly to local player
if (codecInfo.needsTranscoding) {
console.log('[FormatDetector] Codec needs transcoding, using local player');
return createLocalPlayerFormat(video, extension);
}
// Tier 1: Native browser support (direct streaming)
if (NATIVE_SUPPORTED_FORMATS.includes(extension)) {
console.log('[FormatDetector] Native format detected:', extension);
return createDirectFormat(video, extension);
}
// Tier 1.5: MPEG Transport Stream files (optimal for HLS)
if (TS_STREAM_FORMATS.includes(extension)) {
console.log('[FormatDetector] TS format detected, using HLS streaming:', extension);
return createTSHLSFormat(video, extension);
}
// Tier 2: HLS compatible formats - try direct first, HLS as backup
if (HLS_COMPATIBLE_FORMATS.includes(extension)) {
console.log('[FormatDetector] HLS compatible format detected:', extension);
// For now, try direct streaming first, HLS as fallback
// In future, we could detect if HLS is actually needed
return createDirectFormat(video, extension);
}
// TRANSCODING DISABLED: All other formats go directly to local player
// No more fallback to transcoding system
console.log('[FormatDetector] Using local player for format:', extension);
// Check if this is a format from LIMITED_SUPPORT_FORMATS
if (LIMITED_SUPPORT_FORMATS.includes(extension)) {
console.log('[FormatDetector] Format in LIMITED_SUPPORT_FORMATS, forcing local player');
}
return createLocalPlayerFormat(video, extension);
}
/**
* Get file extension from path
*/
function getFileExtension(path: string): string {
const basename = path.split('/').pop() || '';
const parts = basename.split('.');
return parts.length > 1 ? parts[parts.length - 1] : '';
}
/**
* Parse codec information from video metadata
*/
function parseCodecInfo(codecInfo?: string): { needsTranscoding: boolean } {
if (!codecInfo) return { needsTranscoding: false };
try {
const info = JSON.parse(codecInfo);
return {
needsTranscoding: info.needsTranscoding || false
};
} catch {
return { needsTranscoding: false };
}
}
/**
* Create direct streaming format configuration
*/
function createDirectFormat(video: VideoFile, extension: string): VideoFormat {
const mimeType = getMimeType(extension);
return {
type: 'direct',
supportLevel: 'native',
url: `/api/stream/direct/${video.id}`,
mimeType,
qualities: [
{
html: 'Auto',
url: `/api/stream/direct/${video.id}`,
default: true
}
]
};
}
/**
* Create HLS streaming format configuration
*/
function createHLSFormat(video: VideoFile, extension: string): VideoFormat {
return {
type: 'hls',
supportLevel: 'hls',
url: `/api/stream/hls/${video.id}/playlist.m3u8`,
qualities: [
{
html: 'Auto',
url: `/api/stream/hls/${video.id}/playlist.m3u8`,
default: true
},
{
html: '1080p',
url: `/api/stream/hls/${video.id}/playlist.m3u8?quality=1080`
},
{
html: '720p',
url: `/api/stream/hls/${video.id}/playlist.m3u8?quality=720`
},
{
html: '480p',
url: `/api/stream/hls/${video.id}/playlist.m3u8?quality=480`
}
]
};
}
/**
* Create HLS format configuration for MPEG Transport Stream files
* .ts files are already in HLS-compatible format, so we use HLS streaming
*/
function createTSHLSFormat(video: VideoFile, extension: string): VideoFormat {
return {
type: 'hls',
supportLevel: 'hls',
url: `/api/stream/hls/${video.id}/playlist.m3u8`,
qualities: [
{
html: 'Auto (HLS)',
url: `/api/stream/hls/${video.id}/playlist.m3u8`,
default: true
},
{
html: 'Direct Stream',
url: `/api/stream/direct/${video.id}`
}
]
};
}
/**
* Create direct TS format configuration for MPEG Transport Stream files
* Alternative approach - serve .ts directly (may not work in all browsers)
*/
function createTSDirectFormat(video: VideoFile, extension: string): VideoFormat {
const mimeType = getMimeType(extension);
return {
type: 'direct',
supportLevel: 'native',
url: `/api/stream/direct/${video.id}`,
mimeType,
qualities: [
{
html: 'Original (TS)',
url: `/api/stream/direct/${video.id}`,
default: true
}
]
};
}
/**
* Create local player format configuration (replaces transcoding fallback)
*/
function createLocalPlayerFormat(video: VideoFile, extension: string): VideoFormat {
const contentType = getMimeType(extension);
// Use the optimized external streaming endpoint
const baseUrl = `/api/external-stream/${video.id}`;
return {
type: 'local-player',
supportLevel: 'local-player-required',
url: baseUrl, // Optimized endpoint for external players
action: 'launch-local-player',
warning: 'This format requires a local video player',
recommendedPlayers: getRecommendedPlayersForFormat(extension),
streamInfo: {
contentType,
acceptRanges: 'bytes',
supportsSeek: true,
authentication: 'none'
},
qualities: [
{
html: 'Original',
url: baseUrl,
default: true
}
]
};
}
/**
* Get recommended players based on format and platform
*/
function getRecommendedPlayersForFormat(extension: string): string[] {
const players: string[] = ['vlc']; // VLC is always recommended as it's cross-platform
// Platform-specific recommendations
if (typeof window !== 'undefined') {
const userAgent = window.navigator.userAgent.toLowerCase();
const isMac = userAgent.includes('mac');
const isWindows = userAgent.includes('win');
const isLinux = userAgent.includes('linux');
if (isMac) {
players.push('iina', 'elmedia');
} else if (isWindows) {
players.push('potplayer');
}
} else {
// Server-side: recommend all major players
players.push('iina', 'elmedia', 'potplayer');
}
return players;
}
/**
* Get MIME type for file extension
*/
function getMimeType(extension: string): string {
const mimeTypes: Record<string, string> = {
'mp4': 'video/mp4',
'webm': 'video/webm',
'ogg': 'video/ogg',
'ogv': 'video/ogg',
'm4v': 'video/x-m4v',
'ts': 'video/mp2t',
'm2ts': 'video/mp2t',
'mts': 'video/mp2t'
};
return mimeTypes[extension] || 'video/mp4'; // Default fallback
}
/**
* Check if format is supported by ArtPlayer natively
*/
export function isNativeSupported(format: VideoFormat): boolean {
return format.supportLevel === 'native';
}
/**
* Check if format requires HLS streaming
*/
export function requiresHLS(format: VideoFormat): boolean {
return format.type === 'hls';
}
/**
* Check if format requires local player (replaces transcoding fallback)
*/
export function requiresLocalPlayer(format: VideoFormat): boolean {
return format.type === 'local-player';
}
/**
* Check if format requires fallback to transcoding (DEPRECATED - transcoding removed)
*/
export function requiresFallback(format: VideoFormat): boolean {
// TRANSCODING DISABLED: This function is deprecated
// All formats now either work natively or require local player
return false;
}
/**
* ArtPlayer is now the only player used throughout the application
* This function is kept for backward compatibility but always returns 'artplayer'
*/
export function getOptimalPlayerType(format: VideoFormat): 'artplayer' {
// Always return ArtPlayer since it's the only player we use now
return 'artplayer';
}