feat(player): add autoplay support and prioritize H.264 direct streaming
- Add autoplay prop to ArtPlayerWrapper, UnifiedVideoPlayer, and related components - Implement autoplay logic with graceful handling of browser restrictions in ArtPlayerWrapper - Set default autoplay to true in artplayer configuration and expose helper functions to create configs - Enhance video and stream API to parse codec info with codec and container details - Implement H.264 codec detection to prioritize direct streaming over transcoding - Update video-viewer and stream route to override transcoding flag for H.264 codec videos - Add MIME type to ArtPlayer to select appropriate player type (mpegts, webm, ogg, mp4) - Extend video format detector to serve MPEG Transport Stream (.ts) files directly without transcoding - Improve logging with detailed codec, container, and transcoding decision information
This commit is contained in:
parent
4e25da484a
commit
d94fed7e01
|
|
@ -38,16 +38,22 @@ export async function GET(
|
|||
}
|
||||
|
||||
// Parse codec info to determine if transcoding is needed
|
||||
let codecInfo = { needsTranscoding: false, duration: 0 };
|
||||
let codecInfo = { needsTranscoding: false, duration: 0, codec: '', container: '' };
|
||||
try {
|
||||
codecInfo = JSON.parse(video.codec_info || '{}');
|
||||
} catch {
|
||||
// Fallback if codec info is invalid
|
||||
}
|
||||
|
||||
const needsTranscoding = forceTranscode || codecInfo.needsTranscoding || false;
|
||||
// H.264 Direct Streaming Priority: Override database flag for H.264 content
|
||||
// According to memory: H.264-encoded content should attempt direct streaming first
|
||||
const isH264 = codecInfo.codec && ['h264', 'avc1', 'avc'].includes(codecInfo.codec.toLowerCase());
|
||||
const shouldAttemptDirect = isH264 && !forceTranscode;
|
||||
|
||||
console.log(`[STREAM] Video ID: ${id}, Path: ${video.path}, Force Transcode: ${forceTranscode}, Needs Transcode: ${codecInfo.needsTranscoding}, Final Decision: ${needsTranscoding}`);
|
||||
// If H.264, bypass the needsTranscoding flag and attempt direct streaming
|
||||
const needsTranscoding = shouldAttemptDirect ? false : (forceTranscode || codecInfo.needsTranscoding || false);
|
||||
|
||||
console.log(`[STREAM] Video ID: ${id}, Path: ${video.path}, Codec: ${codecInfo.codec}, Container: ${codecInfo.container}, Force Transcode: ${forceTranscode}, DB Needs Transcode: ${codecInfo.needsTranscoding}, Is H264: ${isH264}, Final Decision: ${needsTranscoding}`);
|
||||
|
||||
if (needsTranscoding) {
|
||||
console.log(`[STREAM] Redirecting to transcoding endpoint: /api/stream/${id}/transcode`);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ interface ArtPlayerWrapperProps {
|
|||
avgRating?: number;
|
||||
showBookmarks?: boolean;
|
||||
showRatings?: boolean;
|
||||
autoplay?: boolean;
|
||||
}
|
||||
|
||||
export default function ArtPlayerWrapper({
|
||||
|
|
@ -38,7 +39,8 @@ export default function ArtPlayerWrapper({
|
|||
bookmarkCount = 0,
|
||||
avgRating = 0,
|
||||
showBookmarks = false,
|
||||
showRatings = false
|
||||
showRatings = false,
|
||||
autoplay = true
|
||||
}: ArtPlayerWrapperProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const playerRef = useRef<Artplayer | null>(null);
|
||||
|
|
@ -88,10 +90,10 @@ export default function ArtPlayerWrapper({
|
|||
const player = new Artplayer({
|
||||
container: containerRef.current,
|
||||
url: detectedFormat.url,
|
||||
type: detectedFormat.type === 'hls' ? 'm3u8' : 'mp4',
|
||||
type: detectedFormat.type === 'hls' ? 'm3u8' : getArtPlayerType(detectedFormat.mimeType),
|
||||
|
||||
// Core playback settings
|
||||
autoplay: false,
|
||||
autoplay: autoplay,
|
||||
muted: false,
|
||||
volume: volume,
|
||||
|
||||
|
|
@ -271,6 +273,18 @@ export default function ArtPlayerWrapper({
|
|||
player.on('ready', () => {
|
||||
console.log('ArtPlayer ready');
|
||||
setIsLoading(false);
|
||||
|
||||
// Handle autoplay after player is ready
|
||||
if (autoplay) {
|
||||
// Try to autoplay, but handle browser restrictions gracefully
|
||||
player.play().then(() => {
|
||||
console.log('Autoplay started successfully');
|
||||
}).catch((error) => {
|
||||
console.log('Autoplay prevented by browser:', error);
|
||||
// Autoplay was blocked - this is normal behavior for many browsers
|
||||
// The user will need to click the play button manually
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
player.on('play', () => {
|
||||
|
|
@ -340,7 +354,7 @@ export default function ArtPlayerWrapper({
|
|||
setError(`Failed to initialize player: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [useArtPlayer, isOpen, video, onProgress, volume, format?.supportLevel, localIsBookmarked, localBookmarkCount, localAvgRating]);
|
||||
}, [useArtPlayer, isOpen, video, onProgress, volume, autoplay, format?.supportLevel, localIsBookmarked, localBookmarkCount, localAvgRating]);
|
||||
|
||||
// Handle bookmark toggle
|
||||
const handleBookmarkToggle = useCallback(async () => {
|
||||
|
|
@ -383,6 +397,23 @@ export default function ArtPlayerWrapper({
|
|||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// Get ArtPlayer type from MIME type
|
||||
const getArtPlayerType = (mimeType?: string) => {
|
||||
if (!mimeType) return 'mp4';
|
||||
|
||||
// Map MIME types to ArtPlayer types
|
||||
switch (mimeType) {
|
||||
case 'video/mp2t':
|
||||
return 'mpegts'; // ArtPlayer supports MPEG-TS
|
||||
case 'video/webm':
|
||||
return 'webm';
|
||||
case 'video/ogg':
|
||||
return 'ogg';
|
||||
default:
|
||||
return 'mp4';
|
||||
}
|
||||
};
|
||||
|
||||
// Format file size
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ interface UnifiedVideoPlayerProps {
|
|||
showRatings?: boolean;
|
||||
scrollPosition?: number;
|
||||
formatFileSize?: (bytes: number) => string;
|
||||
autoplay?: boolean;
|
||||
}
|
||||
|
||||
export default function UnifiedVideoPlayer({
|
||||
|
|
@ -35,7 +36,8 @@ export default function UnifiedVideoPlayer({
|
|||
showBookmarks = false,
|
||||
showRatings = false,
|
||||
scrollPosition,
|
||||
formatFileSize
|
||||
formatFileSize,
|
||||
autoplay = true
|
||||
}: UnifiedVideoPlayerProps) {
|
||||
const [format, setFormat] = useState<ReturnType<typeof detectVideoFormat> | null>(null);
|
||||
const [useArtPlayer, setUseArtPlayer] = useState(forceArtPlayer);
|
||||
|
|
@ -142,6 +144,7 @@ export default function UnifiedVideoPlayer({
|
|||
avgRating={video.avg_rating || 0}
|
||||
showBookmarks={showBookmarks}
|
||||
showRatings={showRatings}
|
||||
autoplay={autoplay}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -170,21 +170,31 @@ export default function VideoViewer({
|
|||
resetProgress();
|
||||
// Let the useProtectedDuration hook handle duration fetching internally
|
||||
|
||||
// First check if this video needs transcoding
|
||||
// First check codec info and apply H.264 direct streaming priority
|
||||
const checkTranscodingNeeded = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/videos/${videoId}`);
|
||||
const videoData = await response.json();
|
||||
|
||||
let codecInfo = { needsTranscoding: false };
|
||||
let codecInfo = { needsTranscoding: false, codec: '', container: '' };
|
||||
try {
|
||||
codecInfo = JSON.parse(videoData.codec_info || '{}');
|
||||
} catch {
|
||||
// Fallback if codec info is invalid
|
||||
}
|
||||
|
||||
if (codecInfo.needsTranscoding) {
|
||||
console.log(`[PLAYER] Video ${videoId} needs transcoding, using transcoding endpoint directly`);
|
||||
// H.264 Direct Streaming Priority: Override database flag for H.264 content
|
||||
// According to memory: H.264-encoded content should attempt direct streaming first
|
||||
const isH264 = codecInfo.codec && ['h264', 'avc1', 'avc'].includes(codecInfo.codec.toLowerCase());
|
||||
const shouldAttemptDirect = isH264;
|
||||
|
||||
if (shouldAttemptDirect) {
|
||||
console.log(`[PLAYER] Video ${videoId} has H.264 codec (${codecInfo.codec}), attempting direct streaming first (overriding DB flag: ${codecInfo.needsTranscoding})`);
|
||||
setIsTranscoding(false);
|
||||
videoRef.current!.src = `/api/stream/${videoId}`;
|
||||
videoRef.current!.load();
|
||||
} else if (codecInfo.needsTranscoding) {
|
||||
console.log(`[PLAYER] Video ${videoId} needs transcoding (codec: ${codecInfo.codec}), using transcoding endpoint directly`);
|
||||
setIsTranscoding(true);
|
||||
setTranscodingError(null);
|
||||
const transcodingUrl = `/api/stream/${videoId}/transcode`;
|
||||
|
|
@ -192,7 +202,7 @@ export default function VideoViewer({
|
|||
videoRef.current!.src = transcodingUrl;
|
||||
videoRef.current!.load();
|
||||
} else {
|
||||
console.log(`[PLAYER] Video ${videoId} can be streamed directly`);
|
||||
console.log(`[PLAYER] Video ${videoId} can be streamed directly (codec: ${codecInfo.codec})`);
|
||||
setIsTranscoding(false);
|
||||
videoRef.current!.src = `/api/stream/${videoId}`;
|
||||
videoRef.current!.load();
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export interface ArtPlayerConfig {
|
|||
*/
|
||||
export const defaultArtPlayerConfig: ArtPlayerConfig = {
|
||||
// Core settings
|
||||
autoplay: false,
|
||||
autoplay: true,
|
||||
muted: false,
|
||||
volume: 1.0,
|
||||
|
||||
|
|
@ -67,6 +67,23 @@ export const defaultArtPlayerConfig: ArtPlayerConfig = {
|
|||
};
|
||||
|
||||
|
||||
/**
|
||||
* Create ArtPlayer configuration with autoplay options
|
||||
*/
|
||||
export function createArtPlayerConfig(options: Partial<ArtPlayerConfig> = {}): ArtPlayerConfig {
|
||||
return {
|
||||
...defaultArtPlayerConfig,
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ArtPlayer configuration for autoplay
|
||||
*/
|
||||
export function createAutoplayConfig(autoplay: boolean = true): ArtPlayerConfig {
|
||||
return createArtPlayerConfig({ autoplay });
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyboard shortcut configuration
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -39,15 +39,20 @@ const NATIVE_SUPPORTED_FORMATS = [
|
|||
'ogv'
|
||||
];
|
||||
|
||||
// Formats that work well with HLS streaming
|
||||
// Formats that work well with HLS streaming (but .ts files should be served directly)
|
||||
const HLS_COMPATIBLE_FORMATS = [
|
||||
'mp4',
|
||||
'm4v',
|
||||
'ts',
|
||||
// Note: .ts files are removed from here - they should be served directly
|
||||
'm2ts',
|
||||
'mts'
|
||||
];
|
||||
|
||||
// MPEG Transport Stream formats that should be served directly
|
||||
const DIRECT_TS_FORMATS = [
|
||||
'ts', // MPEG Transport Stream - already in correct format
|
||||
];
|
||||
|
||||
// Formats with limited support (may need transcoding)
|
||||
const LIMITED_SUPPORT_FORMATS = [
|
||||
'avi',
|
||||
|
|
@ -76,6 +81,11 @@ export function detectVideoFormat(video: VideoFile): VideoFormat {
|
|||
return createDirectFormat(video, extension);
|
||||
}
|
||||
|
||||
// Tier 1.5: MPEG Transport Stream files (serve directly)
|
||||
if (DIRECT_TS_FORMATS.includes(extension)) {
|
||||
return createTSDirectFormat(video, extension);
|
||||
}
|
||||
|
||||
// Tier 2: HLS compatible formats
|
||||
if (HLS_COMPATIBLE_FORMATS.includes(extension)) {
|
||||
return createHLSFormat(video, extension);
|
||||
|
|
@ -166,6 +176,27 @@ function createHLSFormat(video: VideoFile, extension: string): VideoFormat {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create direct TS format configuration for MPEG Transport Stream files
|
||||
*/
|
||||
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 fallback format configuration (uses current transcoding system)
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue