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:
tigeren 2025-10-04 14:36:19 +00:00
parent 4489fc5381
commit e019389770
3 changed files with 143 additions and 48 deletions

View File

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

View File

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

View File

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