feat(player): improve HLS player cleanup and session management
- Add support for releasing HLS session references via API cleanup route - Extend HLS cleanup API to handle 'release_reference' and 'force_cleanup' actions - Refactor WebDAV API to handle PROPFIND method via POST with x-webdav-method header - Replace PROPFIND handler with GET and POST handlers for better WebDAV compatibility - Add comprehensive cleanup of HLS instances, players, and error handlers on unmount and modal close - Implement immediate stop and destroy of HLS instance when modal closes - Automatically release backend HLS session reference on modal close via cleanup API call - Improve logging throughout cleanup and error handling processes
This commit is contained in:
parent
4489fc5381
commit
e019389770
|
|
@ -21,18 +21,34 @@ export async function POST(
|
|||
return NextResponse.json({ error: "Invalid video ID" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Force cleanup the session
|
||||
await hlsSessionManager.forceCleanupSession(videoId);
|
||||
// Check if this is a reference release or force cleanup
|
||||
const body = await request.json().catch(() => ({ action: 'force_cleanup' }));
|
||||
const { action = 'force_cleanup' } = body;
|
||||
|
||||
console.log(`[HLS-Cleanup] Manual cleanup completed for video ${videoId}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Session for video ${videoId} cleaned up successfully`
|
||||
});
|
||||
if (action === 'release_reference') {
|
||||
// Release a reference to the session
|
||||
await hlsSessionManager.releaseSession(videoId);
|
||||
console.log(`[HLS-Cleanup] Reference released for video ${videoId}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Reference released for video ${videoId}`,
|
||||
videoId
|
||||
});
|
||||
|
||||
} else {
|
||||
// Force cleanup the session (default behavior)
|
||||
await hlsSessionManager.forceCleanupSession(videoId);
|
||||
console.log(`[HLS-Cleanup] Manual cleanup completed for video ${videoId}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Session for video ${videoId} cleaned up successfully`
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("[HLS-Cleanup] Error during manual cleanup:", error);
|
||||
console.error("[HLS-Cleanup] Error during cleanup:", error);
|
||||
return NextResponse.json({
|
||||
error: "Cleanup failed",
|
||||
details: error.message
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import { Builder } from 'xml2js';
|
|||
* Supported Methods:
|
||||
* - GET: Download/stream files
|
||||
* - HEAD: File information
|
||||
* - PROPFIND: Directory listing and file properties
|
||||
* - OPTIONS: WebDAV capability discovery
|
||||
* Note: PROPFIND is handled via POST with x-webdav-method header for compatibility
|
||||
*/
|
||||
|
||||
interface WebDAVProp {
|
||||
|
|
@ -37,52 +37,17 @@ export async function OPTIONS(request: NextRequest) {
|
|||
return new Response(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Allow': 'GET, HEAD, PROPFIND, OPTIONS',
|
||||
'Allow': 'GET, HEAD, POST, OPTIONS',
|
||||
'DAV': '1, 2',
|
||||
'MS-Author-Via': 'DAV',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, HEAD, PROPFIND, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Depth, Range, Authorization',
|
||||
'Access-Control-Allow-Methods': 'GET, HEAD, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Depth, Range, Authorization, X-WebDAV-Method',
|
||||
'Access-Control-Max-Age': '86400',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function PROPFIND(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
|
||||
const { path: pathSegments } = await params;
|
||||
const requestPath = pathSegments ? '/' + pathSegments.join('/') : '/';
|
||||
const depth = request.headers.get('depth') || '1';
|
||||
|
||||
console.log(`[WebDAV] PROPFIND request for path: ${requestPath}, depth: ${depth}`);
|
||||
|
||||
try {
|
||||
const db = getDatabase();
|
||||
|
||||
// Root directory listing
|
||||
if (requestPath === '/' || requestPath === '') {
|
||||
return await handleRootPropfind(request, depth);
|
||||
}
|
||||
|
||||
// Library listing (/library/{id})
|
||||
if (requestPath.startsWith('/library/')) {
|
||||
const libraryId = parseInt(pathSegments[1]);
|
||||
return await handleLibraryPropfind(request, libraryId, pathSegments.slice(2), depth);
|
||||
}
|
||||
|
||||
// Video file access (/video/{id})
|
||||
if (requestPath.startsWith('/video/')) {
|
||||
const videoId = parseInt(pathSegments[1]);
|
||||
return await handleVideoPropfind(request, videoId, depth);
|
||||
}
|
||||
|
||||
return new Response('Not Found', { status: 404 });
|
||||
|
||||
} catch (error) {
|
||||
console.error('[WebDAV] PROPFIND error:', error);
|
||||
return new Response('Internal Server Error', { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
|
||||
const { path: pathSegments } = await params;
|
||||
const requestPath = pathSegments ? '/' + pathSegments.join('/') : '/';
|
||||
|
|
@ -125,6 +90,51 @@ export async function HEAD(request: NextRequest, { params }: { params: Promise<{
|
|||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
|
||||
const { path: pathSegments } = await params;
|
||||
const requestPath = pathSegments ? '/' + pathSegments.join('/') : '/';
|
||||
|
||||
// Handle PROPFIND method via POST for WebDAV compatibility
|
||||
if (request.headers.get('x-webdav-method') === 'PROPFIND') {
|
||||
return handlePropfindRequest(request, pathSegments, requestPath);
|
||||
}
|
||||
|
||||
return new Response('Method Not Allowed', { status: 405 });
|
||||
}
|
||||
|
||||
async function handlePropfindRequest(request: NextRequest, pathSegments: string[], requestPath: string) {
|
||||
const depth = request.headers.get('depth') || '1';
|
||||
|
||||
console.log(`[WebDAV] PROPFIND request for path: ${requestPath}, depth: ${depth}`);
|
||||
|
||||
try {
|
||||
const db = getDatabase();
|
||||
|
||||
// Root directory listing
|
||||
if (requestPath === '/' || requestPath === '') {
|
||||
return await handleRootPropfind(request, depth);
|
||||
}
|
||||
|
||||
// Library listing (/library/{id})
|
||||
if (requestPath.startsWith('/library/')) {
|
||||
const libraryId = parseInt(pathSegments[1]);
|
||||
return await handleLibraryPropfind(request, libraryId, pathSegments.slice(2), depth);
|
||||
}
|
||||
|
||||
// Video file access (/video/{id})
|
||||
if (requestPath.startsWith('/video/')) {
|
||||
const videoId = parseInt(pathSegments[1]);
|
||||
return await handleVideoPropfind(request, videoId, depth);
|
||||
}
|
||||
|
||||
return new Response('Not Found', { status: 404 });
|
||||
|
||||
} catch (error) {
|
||||
console.error('[WebDAV] PROPFIND error:', error);
|
||||
return new Response('Internal Server Error', { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle PROPFIND for root directory - lists all libraries
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export default function ArtPlayerWrapper({
|
|||
}: ArtPlayerWrapperProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const playerRef = useRef<Artplayer | null>(null);
|
||||
const hlsInstanceRef = useRef<Hls | null>(null); // Store HLS instance for cleanup
|
||||
const [format, setFormat] = useState<VideoFormat | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
|
@ -206,6 +207,9 @@ export default function ArtPlayerWrapper({
|
|||
abrBandWidthUpFactor: 0.7,
|
||||
});
|
||||
|
||||
// Store reference for cleanup
|
||||
hlsInstanceRef.current = hlsInstance;
|
||||
|
||||
// Set up comprehensive error handling
|
||||
const errorHandler = createHLSErrorHandler({
|
||||
onError: (error) => {
|
||||
|
|
@ -387,15 +391,30 @@ export default function ArtPlayerWrapper({
|
|||
playerRef.current = player;
|
||||
|
||||
return () => {
|
||||
console.log('[ArtPlayer] Starting cleanup...');
|
||||
|
||||
// Stop HLS loading immediately
|
||||
if (hlsInstanceRef.current) {
|
||||
console.log('[ArtPlayer] Stopping HLS loading...');
|
||||
hlsInstanceRef.current.stopLoad();
|
||||
hlsInstanceRef.current.destroy();
|
||||
hlsInstanceRef.current = null;
|
||||
}
|
||||
|
||||
// Destroy player
|
||||
if (playerRef.current) {
|
||||
console.log('[ArtPlayer] Destroying player...');
|
||||
playerRef.current.destroy();
|
||||
playerRef.current = null;
|
||||
}
|
||||
|
||||
// Clean up HLS error handler
|
||||
if (hlsErrorHandlerRef.current) {
|
||||
hlsErrorHandlerRef.current.detach();
|
||||
hlsErrorHandlerRef.current = null;
|
||||
}
|
||||
|
||||
console.log('[ArtPlayer] Cleanup completed');
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize ArtPlayer:', error);
|
||||
|
|
@ -531,15 +550,28 @@ export default function ArtPlayerWrapper({
|
|||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
console.log('[ArtPlayer] Unmount cleanup...');
|
||||
|
||||
// Stop HLS loading
|
||||
if (hlsInstanceRef.current) {
|
||||
console.log('[ArtPlayer] Stopping HLS on unmount...');
|
||||
hlsInstanceRef.current.stopLoad();
|
||||
hlsInstanceRef.current.destroy();
|
||||
hlsInstanceRef.current = null;
|
||||
}
|
||||
|
||||
// Destroy player
|
||||
if (playerRef.current) {
|
||||
playerRef.current.destroy();
|
||||
playerRef.current = null;
|
||||
}
|
||||
|
||||
// Clean up HLS error handler
|
||||
if (hlsErrorHandlerRef.current) {
|
||||
hlsErrorHandlerRef.current.detach();
|
||||
hlsErrorHandlerRef.current = null;
|
||||
}
|
||||
|
||||
// Clean up custom styles
|
||||
const styleElement = document.getElementById('artplayer-styles');
|
||||
if (styleElement) {
|
||||
|
|
@ -548,6 +580,43 @@ export default function ArtPlayerWrapper({
|
|||
};
|
||||
}, []);
|
||||
|
||||
// Cleanup when modal closes
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
console.log('[ArtPlayer] Modal closed, stopping HLS...');
|
||||
|
||||
// Stop HLS loading immediately when modal closes
|
||||
if (hlsInstanceRef.current) {
|
||||
hlsInstanceRef.current.stopLoad();
|
||||
hlsInstanceRef.current.destroy();
|
||||
hlsInstanceRef.current = null;
|
||||
}
|
||||
|
||||
// Also destroy player when modal closes
|
||||
if (playerRef.current) {
|
||||
playerRef.current.destroy();
|
||||
playerRef.current = null;
|
||||
}
|
||||
|
||||
// Clean up error handler
|
||||
if (hlsErrorHandlerRef.current) {
|
||||
hlsErrorHandlerRef.current.detach();
|
||||
hlsErrorHandlerRef.current = null;
|
||||
}
|
||||
|
||||
// Release backend session reference
|
||||
if (format?.type === 'hls') {
|
||||
fetch(`/api/stream/hls/${video.id}/cleanup`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'release_reference' })
|
||||
}).catch(error => {
|
||||
console.warn('[ArtPlayer] Failed to release session reference:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [isOpen, format?.type, video.id]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
|
|
|
|||
Loading…
Reference in New Issue