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:
tigeren 2025-09-20 17:00:49 +00:00
parent 4e25da484a
commit d94fed7e01
6 changed files with 114 additions and 16 deletions

View File

@ -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`);

View File

@ -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';

View File

@ -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 {

View File

@ -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();

View File

@ -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
*/

View File

@ -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)
*/