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 });
|
return NextResponse.json({ error: "Invalid video ID" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force cleanup the session
|
// Check if this is a reference release or force cleanup
|
||||||
await hlsSessionManager.forceCleanupSession(videoId);
|
const body = await request.json().catch(() => ({ action: 'force_cleanup' }));
|
||||||
|
const { action = 'force_cleanup' } = body;
|
||||||
|
|
||||||
console.log(`[HLS-Cleanup] Manual cleanup completed for video ${videoId}`);
|
if (action === 'release_reference') {
|
||||||
|
// Release a reference to the session
|
||||||
return NextResponse.json({
|
await hlsSessionManager.releaseSession(videoId);
|
||||||
success: true,
|
console.log(`[HLS-Cleanup] Reference released for video ${videoId}`);
|
||||||
message: `Session for video ${videoId} cleaned up successfully`
|
|
||||||
});
|
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) {
|
} catch (error: any) {
|
||||||
console.error("[HLS-Cleanup] Error during manual cleanup:", error);
|
console.error("[HLS-Cleanup] Error during cleanup:", error);
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
error: "Cleanup failed",
|
error: "Cleanup failed",
|
||||||
details: error.message
|
details: error.message
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ import { Builder } from 'xml2js';
|
||||||
* Supported Methods:
|
* Supported Methods:
|
||||||
* - GET: Download/stream files
|
* - GET: Download/stream files
|
||||||
* - HEAD: File information
|
* - HEAD: File information
|
||||||
* - PROPFIND: Directory listing and file properties
|
|
||||||
* - OPTIONS: WebDAV capability discovery
|
* - OPTIONS: WebDAV capability discovery
|
||||||
|
* Note: PROPFIND is handled via POST with x-webdav-method header for compatibility
|
||||||
*/
|
*/
|
||||||
|
|
||||||
interface WebDAVProp {
|
interface WebDAVProp {
|
||||||
|
|
@ -37,52 +37,17 @@ export async function OPTIONS(request: NextRequest) {
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
'Allow': 'GET, HEAD, PROPFIND, OPTIONS',
|
'Allow': 'GET, HEAD, POST, OPTIONS',
|
||||||
'DAV': '1, 2',
|
'DAV': '1, 2',
|
||||||
'MS-Author-Via': 'DAV',
|
'MS-Author-Via': 'DAV',
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
'Access-Control-Allow-Methods': 'GET, HEAD, PROPFIND, OPTIONS',
|
'Access-Control-Allow-Methods': 'GET, HEAD, POST, OPTIONS',
|
||||||
'Access-Control-Allow-Headers': 'Content-Type, Depth, Range, Authorization',
|
'Access-Control-Allow-Headers': 'Content-Type, Depth, Range, Authorization, X-WebDAV-Method',
|
||||||
'Access-Control-Max-Age': '86400',
|
'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[] }> }) {
|
export async function GET(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
|
||||||
const { path: pathSegments } = await params;
|
const { path: pathSegments } = await params;
|
||||||
const requestPath = pathSegments ? '/' + pathSegments.join('/') : '/';
|
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
|
* Handle PROPFIND for root directory - lists all libraries
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ export default function ArtPlayerWrapper({
|
||||||
}: ArtPlayerWrapperProps) {
|
}: ArtPlayerWrapperProps) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const playerRef = useRef<Artplayer | null>(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 [format, setFormat] = useState<VideoFormat | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
@ -206,6 +207,9 @@ export default function ArtPlayerWrapper({
|
||||||
abrBandWidthUpFactor: 0.7,
|
abrBandWidthUpFactor: 0.7,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Store reference for cleanup
|
||||||
|
hlsInstanceRef.current = hlsInstance;
|
||||||
|
|
||||||
// Set up comprehensive error handling
|
// Set up comprehensive error handling
|
||||||
const errorHandler = createHLSErrorHandler({
|
const errorHandler = createHLSErrorHandler({
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
|
@ -387,15 +391,30 @@ export default function ArtPlayerWrapper({
|
||||||
playerRef.current = player;
|
playerRef.current = player;
|
||||||
|
|
||||||
return () => {
|
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) {
|
if (playerRef.current) {
|
||||||
|
console.log('[ArtPlayer] Destroying player...');
|
||||||
playerRef.current.destroy();
|
playerRef.current.destroy();
|
||||||
playerRef.current = null;
|
playerRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up HLS error handler
|
// Clean up HLS error handler
|
||||||
if (hlsErrorHandlerRef.current) {
|
if (hlsErrorHandlerRef.current) {
|
||||||
hlsErrorHandlerRef.current.detach();
|
hlsErrorHandlerRef.current.detach();
|
||||||
hlsErrorHandlerRef.current = null;
|
hlsErrorHandlerRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[ArtPlayer] Cleanup completed');
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize ArtPlayer:', error);
|
console.error('Failed to initialize ArtPlayer:', error);
|
||||||
|
|
@ -531,15 +550,28 @@ export default function ArtPlayerWrapper({
|
||||||
// Cleanup on unmount
|
// Cleanup on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
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) {
|
if (playerRef.current) {
|
||||||
playerRef.current.destroy();
|
playerRef.current.destroy();
|
||||||
playerRef.current = null;
|
playerRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up HLS error handler
|
// Clean up HLS error handler
|
||||||
if (hlsErrorHandlerRef.current) {
|
if (hlsErrorHandlerRef.current) {
|
||||||
hlsErrorHandlerRef.current.detach();
|
hlsErrorHandlerRef.current.detach();
|
||||||
hlsErrorHandlerRef.current = null;
|
hlsErrorHandlerRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up custom styles
|
// Clean up custom styles
|
||||||
const styleElement = document.getElementById('artplayer-styles');
|
const styleElement = document.getElementById('artplayer-styles');
|
||||||
if (styleElement) {
|
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;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue