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
|
// Parse codec info to determine if transcoding is needed
|
||||||
let codecInfo = { needsTranscoding: false, duration: 0 };
|
let codecInfo = { needsTranscoding: false, duration: 0, codec: '', container: '' };
|
||||||
try {
|
try {
|
||||||
codecInfo = JSON.parse(video.codec_info || '{}');
|
codecInfo = JSON.parse(video.codec_info || '{}');
|
||||||
} catch {
|
} catch {
|
||||||
// Fallback if codec info is invalid
|
// 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) {
|
if (needsTranscoding) {
|
||||||
console.log(`[STREAM] Redirecting to transcoding endpoint: /api/stream/${id}/transcode`);
|
console.log(`[STREAM] Redirecting to transcoding endpoint: /api/stream/${id}/transcode`);
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ interface ArtPlayerWrapperProps {
|
||||||
avgRating?: number;
|
avgRating?: number;
|
||||||
showBookmarks?: boolean;
|
showBookmarks?: boolean;
|
||||||
showRatings?: boolean;
|
showRatings?: boolean;
|
||||||
|
autoplay?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ArtPlayerWrapper({
|
export default function ArtPlayerWrapper({
|
||||||
|
|
@ -38,7 +39,8 @@ export default function ArtPlayerWrapper({
|
||||||
bookmarkCount = 0,
|
bookmarkCount = 0,
|
||||||
avgRating = 0,
|
avgRating = 0,
|
||||||
showBookmarks = false,
|
showBookmarks = false,
|
||||||
showRatings = false
|
showRatings = false,
|
||||||
|
autoplay = true
|
||||||
}: ArtPlayerWrapperProps) {
|
}: ArtPlayerWrapperProps) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const playerRef = useRef<Artplayer | null>(null);
|
const playerRef = useRef<Artplayer | null>(null);
|
||||||
|
|
@ -88,10 +90,10 @@ export default function ArtPlayerWrapper({
|
||||||
const player = new Artplayer({
|
const player = new Artplayer({
|
||||||
container: containerRef.current,
|
container: containerRef.current,
|
||||||
url: detectedFormat.url,
|
url: detectedFormat.url,
|
||||||
type: detectedFormat.type === 'hls' ? 'm3u8' : 'mp4',
|
type: detectedFormat.type === 'hls' ? 'm3u8' : getArtPlayerType(detectedFormat.mimeType),
|
||||||
|
|
||||||
// Core playback settings
|
// Core playback settings
|
||||||
autoplay: false,
|
autoplay: autoplay,
|
||||||
muted: false,
|
muted: false,
|
||||||
volume: volume,
|
volume: volume,
|
||||||
|
|
||||||
|
|
@ -271,6 +273,18 @@ export default function ArtPlayerWrapper({
|
||||||
player.on('ready', () => {
|
player.on('ready', () => {
|
||||||
console.log('ArtPlayer ready');
|
console.log('ArtPlayer ready');
|
||||||
setIsLoading(false);
|
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', () => {
|
player.on('play', () => {
|
||||||
|
|
@ -340,7 +354,7 @@ export default function ArtPlayerWrapper({
|
||||||
setError(`Failed to initialize player: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
setError(`Failed to initialize player: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
setIsLoading(false);
|
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
|
// Handle bookmark toggle
|
||||||
const handleBookmarkToggle = useCallback(async () => {
|
const handleBookmarkToggle = useCallback(async () => {
|
||||||
|
|
@ -383,6 +397,23 @@ export default function ArtPlayerWrapper({
|
||||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
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
|
// Format file size
|
||||||
const formatFileSize = (bytes: number) => {
|
const formatFileSize = (bytes: number) => {
|
||||||
if (bytes === 0) return '0 Bytes';
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ interface UnifiedVideoPlayerProps {
|
||||||
showRatings?: boolean;
|
showRatings?: boolean;
|
||||||
scrollPosition?: number;
|
scrollPosition?: number;
|
||||||
formatFileSize?: (bytes: number) => string;
|
formatFileSize?: (bytes: number) => string;
|
||||||
|
autoplay?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UnifiedVideoPlayer({
|
export default function UnifiedVideoPlayer({
|
||||||
|
|
@ -35,7 +36,8 @@ export default function UnifiedVideoPlayer({
|
||||||
showBookmarks = false,
|
showBookmarks = false,
|
||||||
showRatings = false,
|
showRatings = false,
|
||||||
scrollPosition,
|
scrollPosition,
|
||||||
formatFileSize
|
formatFileSize,
|
||||||
|
autoplay = true
|
||||||
}: UnifiedVideoPlayerProps) {
|
}: UnifiedVideoPlayerProps) {
|
||||||
const [format, setFormat] = useState<ReturnType<typeof detectVideoFormat> | null>(null);
|
const [format, setFormat] = useState<ReturnType<typeof detectVideoFormat> | null>(null);
|
||||||
const [useArtPlayer, setUseArtPlayer] = useState(forceArtPlayer);
|
const [useArtPlayer, setUseArtPlayer] = useState(forceArtPlayer);
|
||||||
|
|
@ -142,6 +144,7 @@ export default function UnifiedVideoPlayer({
|
||||||
avgRating={video.avg_rating || 0}
|
avgRating={video.avg_rating || 0}
|
||||||
showBookmarks={showBookmarks}
|
showBookmarks={showBookmarks}
|
||||||
showRatings={showRatings}
|
showRatings={showRatings}
|
||||||
|
autoplay={autoplay}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -170,21 +170,31 @@ export default function VideoViewer({
|
||||||
resetProgress();
|
resetProgress();
|
||||||
// Let the useProtectedDuration hook handle duration fetching internally
|
// 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 () => {
|
const checkTranscodingNeeded = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/videos/${videoId}`);
|
const response = await fetch(`/api/videos/${videoId}`);
|
||||||
const videoData = await response.json();
|
const videoData = await response.json();
|
||||||
|
|
||||||
let codecInfo = { needsTranscoding: false };
|
let codecInfo = { needsTranscoding: false, codec: '', container: '' };
|
||||||
try {
|
try {
|
||||||
codecInfo = JSON.parse(videoData.codec_info || '{}');
|
codecInfo = JSON.parse(videoData.codec_info || '{}');
|
||||||
} catch {
|
} catch {
|
||||||
// Fallback if codec info is invalid
|
// Fallback if codec info is invalid
|
||||||
}
|
}
|
||||||
|
|
||||||
if (codecInfo.needsTranscoding) {
|
// H.264 Direct Streaming Priority: Override database flag for H.264 content
|
||||||
console.log(`[PLAYER] Video ${videoId} needs transcoding, using transcoding endpoint directly`);
|
// 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);
|
setIsTranscoding(true);
|
||||||
setTranscodingError(null);
|
setTranscodingError(null);
|
||||||
const transcodingUrl = `/api/stream/${videoId}/transcode`;
|
const transcodingUrl = `/api/stream/${videoId}/transcode`;
|
||||||
|
|
@ -192,7 +202,7 @@ export default function VideoViewer({
|
||||||
videoRef.current!.src = transcodingUrl;
|
videoRef.current!.src = transcodingUrl;
|
||||||
videoRef.current!.load();
|
videoRef.current!.load();
|
||||||
} else {
|
} else {
|
||||||
console.log(`[PLAYER] Video ${videoId} can be streamed directly`);
|
console.log(`[PLAYER] Video ${videoId} can be streamed directly (codec: ${codecInfo.codec})`);
|
||||||
setIsTranscoding(false);
|
setIsTranscoding(false);
|
||||||
videoRef.current!.src = `/api/stream/${videoId}`;
|
videoRef.current!.src = `/api/stream/${videoId}`;
|
||||||
videoRef.current!.load();
|
videoRef.current!.load();
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ export interface ArtPlayerConfig {
|
||||||
*/
|
*/
|
||||||
export const defaultArtPlayerConfig: ArtPlayerConfig = {
|
export const defaultArtPlayerConfig: ArtPlayerConfig = {
|
||||||
// Core settings
|
// Core settings
|
||||||
autoplay: false,
|
autoplay: true,
|
||||||
muted: false,
|
muted: false,
|
||||||
volume: 1.0,
|
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
|
* Keyboard shortcut configuration
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -39,15 +39,20 @@ const NATIVE_SUPPORTED_FORMATS = [
|
||||||
'ogv'
|
'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 = [
|
const HLS_COMPATIBLE_FORMATS = [
|
||||||
'mp4',
|
'mp4',
|
||||||
'm4v',
|
'm4v',
|
||||||
'ts',
|
// Note: .ts files are removed from here - they should be served directly
|
||||||
'm2ts',
|
'm2ts',
|
||||||
'mts'
|
'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)
|
// Formats with limited support (may need transcoding)
|
||||||
const LIMITED_SUPPORT_FORMATS = [
|
const LIMITED_SUPPORT_FORMATS = [
|
||||||
'avi',
|
'avi',
|
||||||
|
|
@ -76,6 +81,11 @@ export function detectVideoFormat(video: VideoFile): VideoFormat {
|
||||||
return createDirectFormat(video, extension);
|
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
|
// Tier 2: HLS compatible formats
|
||||||
if (HLS_COMPATIBLE_FORMATS.includes(extension)) {
|
if (HLS_COMPATIBLE_FORMATS.includes(extension)) {
|
||||||
return createHLSFormat(video, 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)
|
* Create fallback format configuration (uses current transcoding system)
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue