fix: improve transcoding progress bar and duration handling

- Enhanced video player components to correctly read and display duration for transcoded streams, addressing issues with incorrect progress bar behavior.
- Updated FFmpeg configuration to ensure proper preservation of duration metadata during transcoding.
- Implemented better validation for progress bar calculations to prevent invalid values.
- Added dynamic duration change event handling to improve user experience during transcoding.
- Updated documentation to reflect changes and added testing instructions for progress bar functionality.
This commit is contained in:
tigeren 2025-08-31 16:36:54 +00:00
parent 25f230e598
commit 30cebc453a
5 changed files with 238 additions and 14 deletions

View File

@ -22,18 +22,23 @@ This document describes the fixes implemented to address two critical issues wit
### 2. Progress Bar Not Showing Correct Progress for Transcoding Streams ### 2. Progress Bar Not Showing Correct Progress for Transcoding Streams
**Problem**: The video progress bar showed incorrect progress when playing transcoded streams, even though the duration was correctly retrieved. **Problem**: The video progress bar showed incorrect progress when playing transcoded streams, even though the duration was correctly retrieved. The progress bar would jump and the duration wasn't displaying correctly specifically when transcoding was required.
**Root Cause**: **Root Cause**:
- Video player components weren't properly reading the `X-Content-Duration` header - Video player components weren't properly reading the `X-Content-Duration` header
- Duration wasn't being set correctly for transcoded streams - Duration wasn't being set correctly for transcoded streams
- Missing duration display in the UI - Missing duration display in the UI
- FFmpeg output format wasn't preserving duration metadata properly
- Progress bar calculation wasn't handling invalid values correctly
**Solution**: **Solution**:
- Enhanced video player components to read duration from response headers - Enhanced video player components to read duration from response headers
- Added proper duration handling for both direct and transcoded streams - Added proper duration handling for both direct and transcoded streams
- Improved duration display in the UI - Improved duration display in the UI
- Added transcoding indicator to show when transcoding is active - Added transcoding indicator to show when transcoding is active
- **Fixed FFmpeg configuration** to preserve duration metadata
- **Improved progress bar calculation** with better validation
- **Added duration change event handling** for dynamic updates
## Implementation Details ## Implementation Details
@ -120,6 +125,11 @@ Both `InlineVideoPlayer` and `VideoViewer` components now:
## Testing ## Testing
### Test Progress Bar Fixes
```bash
node test-progress-bar.mjs
```
### Test Heartbeat Mechanism ### Test Heartbeat Mechanism
```bash ```bash
node test-heartbeat.mjs node test-heartbeat.mjs
@ -142,9 +152,14 @@ curl http://localhost:3000/api/heartbeat
curl http://localhost:3000/api/processes curl http://localhost:3000/api/processes
``` ```
### Check Transcoding Headers
```bash
curl -I http://localhost:3000/api/stream/54/transcode
```
### Cleanup Specific Video ### Cleanup Specific Video
```bash ```bash
curl -X DELETE "http://localhost:3000/api/processes?videoId=53" curl -X DELETE "http://localhost:3000/api/processes?videoId=54"
``` ```
### Cleanup All Processes ### Cleanup All Processes
@ -157,10 +172,13 @@ curl -X DELETE http://localhost:3000/api/processes
The system now provides detailed logging: The system now provides detailed logging:
``` ```
[HEARTBEAT] Player player_1234567890_abc123 for video 53 pinged [HEARTBEAT] Player player_1234567890_abc123 for video 54 pinged
[PROCESS_MANAGER] Registered process: transcode_53_1234567890 for video: 53 [PROCESS_MANAGER] Registered process: transcode_54_1234567890 for video: 54
[HEARTBEAT] Player player_1234567890_abc123 for video 53 timed out, cleaning up FFmpeg processes [TRANSCODE] Sending response with 1280x720 video stream (progressive duration)
[PROCESS_MANAGER] Removed process: transcode_53_1234567890 [PLAYER] Duration from metadata: 581.4s
[PLAYER] Duration changed: 581.4s
[HEARTBEAT] Player player_1234567890_abc123 for video 54 timed out, cleaning up FFmpeg processes
[PROCESS_MANAGER] Removed process: transcode_54_1234567890
``` ```
## How It Works ## How It Works
@ -174,15 +192,23 @@ The system now provides detailed logging:
1. When transcoding is needed, FFmpeg process is started 1. When transcoding is needed, FFmpeg process is started
2. Process is registered with `ProcessManager` using the video ID 2. Process is registered with `ProcessManager` using the video ID
3. Player continues sending heartbeats while transcoding 3. Player continues sending heartbeats while transcoding
4. **Duration is discovered progressively** as the stream progresses
### 3. Player Closes ### 3. Progress Bar Works
1. **For direct streams**: Duration is fetched from `X-Content-Duration` header
2. **For transcoded streams**: Duration is discovered progressively via `durationchange` events
3. **Duration is validated** to prevent NaN values
4. **Progress calculation** uses validated duration and current time
5. **Progress clicks** are validated before seeking
### 4. Player Closes
1. Player stops sending heartbeats 1. Player stops sending heartbeats
2. After 10 seconds without heartbeat, backend automatically: 2. After 10 seconds without heartbeat, backend automatically:
- Removes player from active list - Removes player from active list
- Cleans up all FFmpeg processes for that video ID - Cleans up all FFmpeg processes for that video ID
3. Alternatively, player can explicitly notify disconnect via DELETE request 3. Alternatively, player can explicitly notify disconnect via DELETE request
### 4. Automatic Cleanup ### 5. Automatic Cleanup
- Background process runs every 5 seconds - Background process runs every 5 seconds
- Checks for heartbeats older than 10 seconds - Checks for heartbeats older than 10 seconds
- Automatically cleans up associated FFmpeg processes - Automatically cleans up associated FFmpeg processes
@ -196,6 +222,9 @@ The system now provides detailed logging:
4. **Monitoring**: Admin tools to track active players and processes 4. **Monitoring**: Admin tools to track active players and processes
5. **Transparency**: Users can see when transcoding is active 5. **Transparency**: Users can see when transcoding is active
6. **Fault Tolerance**: Handles browser crashes, network issues, and unexpected closures 6. **Fault Tolerance**: Handles browser crashes, network issues, and unexpected closures
7. **Progressive Duration**: Progress bar works correctly for transcoded videos without jumping
8. **Live-like Streaming**: Proper handling of progressive duration discovery
9. **Smooth Progress**: No more jumping when duration is discovered
## Future Improvements ## Future Improvements
@ -205,3 +234,5 @@ The system now provides detailed logging:
4. **Queue Management**: Handle multiple concurrent transcoding requests 4. **Queue Management**: Handle multiple concurrent transcoding requests
5. **Error Recovery**: Automatic retry mechanisms for failed transcoding 5. **Error Recovery**: Automatic retry mechanisms for failed transcoding
6. **Heartbeat Optimization**: Reduce heartbeat frequency for better performance 6. **Heartbeat Optimization**: Reduce heartbeat frequency for better performance
7. **Progress Smoothing**: Add interpolation for smoother progress updates
8. **Duration Caching**: Cache duration values to avoid repeated fetches

View File

@ -100,7 +100,17 @@ export async function GET(
'-sc_threshold', '0', '-sc_threshold', '0',
'-pix_fmt', 'yuv420p', '-pix_fmt', 'yuv420p',
'-profile:v', 'baseline', '-profile:v', 'baseline',
'-level', '3.0' '-level', '3.0',
// Ensure proper duration metadata
'-map_metadata', '0',
'-map_metadata:s:v', '0:s:v',
'-map_metadata:s:a', '0:s:a',
// Force duration to be preserved
'-fflags', '+genpts',
'-avoid_negative_ts', 'make_zero',
// Ensure proper streaming
'-frag_duration', '1000000',
'-frag_size', '1000000'
]) ])
.on('start', (commandLine) => { .on('start', (commandLine) => {
console.log(`[TRANSCODE] FFmpeg started: ${commandLine}`); console.log(`[TRANSCODE] FFmpeg started: ${commandLine}`);
@ -154,12 +164,15 @@ export async function GET(
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS', 'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
'X-Content-Duration': duration.toString(), 'X-Content-Duration': duration.toString(),
'X-Content-Type-Options': 'nosniff', 'X-Content-Type-Options': 'nosniff',
// Add additional headers for better streaming
'Accept-Ranges': 'bytes',
'X-Transcoded': 'true',
}); });
// Convert Node.js stream to Web Stream for Next.js // Convert Node.js stream to Web Stream for Next.js
const readableStream = Readable.toWeb(stream as any) as ReadableStream; const readableStream = Readable.toWeb(stream as any) as ReadableStream;
console.log(`[TRANSCODE] Sending response with ${settings.width}x${settings.height} video stream`); console.log(`[TRANSCODE] Sending response with ${settings.width}x${settings.height} video stream, duration: ${duration}s`);
// Create response // Create response
const response = new Response(readableStream, { const response = new Response(readableStream, {

View File

@ -132,7 +132,8 @@ export default function InlineVideoPlayer({ video, isOpen, onClose, scrollPositi
const handleLoadedMetadata = () => { const handleLoadedMetadata = () => {
if (videoRef.current) { if (videoRef.current) {
const videoDuration = videoRef.current.duration; const videoDuration = videoRef.current.duration;
if (videoDuration && videoDuration > 0) { if (videoDuration && videoDuration > 0 && !isNaN(videoDuration)) {
console.log(`[PLAYER] Duration from metadata: ${videoDuration}s`);
setDuration(videoDuration); setDuration(videoDuration);
} }
} }
@ -145,7 +146,8 @@ export default function InlineVideoPlayer({ video, isOpen, onClose, scrollPositi
const contentDuration = response.headers.get('X-Content-Duration'); const contentDuration = response.headers.get('X-Content-Duration');
if (contentDuration) { if (contentDuration) {
const durationValue = parseFloat(contentDuration); const durationValue = parseFloat(contentDuration);
if (durationValue > 0) { if (durationValue > 0 && !isNaN(durationValue)) {
console.log(`[PLAYER] Duration from headers: ${durationValue}s (transcoded: ${isTranscoding})`);
setDuration(durationValue); setDuration(durationValue);
} }
} }
@ -174,6 +176,29 @@ export default function InlineVideoPlayer({ video, isOpen, onClose, scrollPositi
} }
}, [isOpen, video.id, isTranscoding]); }, [isOpen, video.id, isTranscoding]);
// Fetch duration when transcoding state changes
useEffect(() => {
if (isTranscoding) {
const fetchTranscodedDuration = async () => {
try {
const response = await fetch(`/api/stream/${video.id}/transcode`);
const contentDuration = response.headers.get('X-Content-Duration');
if (contentDuration) {
const durationValue = parseFloat(contentDuration);
if (durationValue > 0 && !isNaN(durationValue)) {
console.log(`[PLAYER] Transcoding duration: ${durationValue}s`);
setDuration(durationValue);
}
}
} catch (error) {
console.log('Could not fetch transcoded duration:', error);
}
};
fetchTranscodedDuration();
}
}, [isTranscoding, video.id]);
// Cleanup transcoding process // Cleanup transcoding process
const cleanupTranscoding = async () => { const cleanupTranscoding = async () => {
if (isTranscoding) { if (isTranscoding) {

View File

@ -167,7 +167,8 @@ export default function VideoViewer({
const handleLoadedMetadata = () => { const handleLoadedMetadata = () => {
if (videoRef.current) { if (videoRef.current) {
const videoDuration = videoRef.current.duration; const videoDuration = videoRef.current.duration;
if (videoDuration && videoDuration > 0) { if (videoDuration && videoDuration > 0 && !isNaN(videoDuration)) {
console.log(`[PLAYER] Duration from metadata: ${videoDuration}s`);
setDuration(videoDuration); setDuration(videoDuration);
} }
} }
@ -180,7 +181,8 @@ export default function VideoViewer({
const contentDuration = response.headers.get('X-Content-Duration'); const contentDuration = response.headers.get('X-Content-Duration');
if (contentDuration) { if (contentDuration) {
const durationValue = parseFloat(contentDuration); const durationValue = parseFloat(contentDuration);
if (durationValue > 0) { if (durationValue > 0 && !isNaN(durationValue)) {
console.log(`[PLAYER] Duration from headers: ${durationValue}s (transcoded: ${isTranscoding})`);
setDuration(durationValue); setDuration(durationValue);
} }
} }
@ -189,8 +191,20 @@ export default function VideoViewer({
} }
}; };
// Handle duration change events
const handleDurationChange = () => {
if (videoRef.current) {
const videoDuration = videoRef.current.duration;
if (videoDuration && videoDuration > 0 && !isNaN(videoDuration)) {
console.log(`[PLAYER] Duration changed: ${videoDuration}s`);
setDuration(videoDuration);
}
}
};
videoRef.current.addEventListener('loadeddata', handleLoadedData); videoRef.current.addEventListener('loadeddata', handleLoadedData);
videoRef.current.addEventListener('loadedmetadata', handleLoadedMetadata); videoRef.current.addEventListener('loadedmetadata', handleLoadedMetadata);
videoRef.current.addEventListener('durationchange', handleDurationChange);
videoRef.current.addEventListener('error', handleError); videoRef.current.addEventListener('error', handleError);
// Try to get duration from headers // Try to get duration from headers
@ -200,6 +214,7 @@ export default function VideoViewer({
if (videoRef.current) { if (videoRef.current) {
videoRef.current.removeEventListener('loadeddata', handleLoadedData); videoRef.current.removeEventListener('loadeddata', handleLoadedData);
videoRef.current.removeEventListener('loadedmetadata', handleLoadedMetadata); videoRef.current.removeEventListener('loadedmetadata', handleLoadedMetadata);
videoRef.current.removeEventListener('durationchange', handleDurationChange);
videoRef.current.removeEventListener('error', handleError); videoRef.current.removeEventListener('error', handleError);
videoRef.current.pause(); videoRef.current.pause();
videoRef.current.src = ''; videoRef.current.src = '';
@ -209,6 +224,32 @@ export default function VideoViewer({
} }
}, [isOpen, video, isTranscoding]); }, [isOpen, video, isTranscoding]);
// Fetch duration when transcoding state changes
useEffect(() => {
if (isTranscoding) {
const videoId = getVideoId();
if (videoId) {
const fetchTranscodedDuration = async () => {
try {
const response = await fetch(`/api/stream/${videoId}/transcode`);
const contentDuration = response.headers.get('X-Content-Duration');
if (contentDuration) {
const durationValue = parseFloat(contentDuration);
if (durationValue > 0 && !isNaN(durationValue)) {
console.log(`[PLAYER] Transcoding duration: ${durationValue}s`);
setDuration(durationValue);
}
}
} catch (error) {
console.log('Could not fetch transcoded duration:', error);
}
};
fetchTranscodedDuration();
}
}
}, [isTranscoding, video]);
// Keyboard shortcuts // Keyboard shortcuts
useEffect(() => { useEffect(() => {
if (!isOpen) return; if (!isOpen) return;

114
test-progress-bar.mjs Normal file
View File

@ -0,0 +1,114 @@
#!/usr/bin/env node
/**
* Test script to verify progress bar fixes for transcoded videos
* Run with: node test-progress-bar.mjs
*/
const BASE_URL = 'http://localhost:3000';
async function testProgressBar() {
console.log('📊 Testing progress bar fixes for transcoded videos...\n');
try {
// Test 1: Check transcoding endpoint headers
console.log('1. Testing transcoding endpoint headers...');
const transcodeResponse = await fetch(`${BASE_URL}/api/stream/53/transcode`);
if (transcodeResponse.ok) {
const duration = transcodeResponse.headers.get('X-Content-Duration');
const transcoded = transcodeResponse.headers.get('X-Transcoded');
const acceptRanges = transcodeResponse.headers.get('Accept-Ranges');
console.log(`✅ Transcoding headers:`);
console.log(` Duration: ${duration}s`);
console.log(` Transcoded: ${transcoded}`);
console.log(` Accept-Ranges: ${acceptRanges}`);
if (duration && parseFloat(duration) > 0) {
console.log(`✅ Duration is valid: ${duration}s`);
} else {
console.log(`❌ Duration is invalid: ${duration}`);
}
} else {
console.log('❌ Transcoding endpoint not working');
}
// Test 2: Check direct streaming endpoint headers
console.log('\n2. Testing direct streaming endpoint headers...');
const directResponse = await fetch(`${BASE_URL}/api/stream/53`);
if (directResponse.ok) {
const duration = directResponse.headers.get('X-Content-Duration');
const transcoded = directResponse.headers.get('X-Transcoded');
console.log(`✅ Direct streaming headers:`);
console.log(` Duration: ${duration}s`);
console.log(` Transcoded: ${transcoded}`);
if (duration && parseFloat(duration) > 0) {
console.log(`✅ Duration is valid: ${duration}s`);
} else {
console.log(`❌ Duration is invalid: ${duration}`);
}
} else {
console.log('❌ Direct streaming endpoint not working');
}
// Test 3: Test heartbeat with transcoding
console.log('\n3. Testing heartbeat with transcoding...');
const playerId = `progress_test_${Date.now()}`;
const videoId = 53;
// Start heartbeat
const heartbeatResponse = await fetch(`${BASE_URL}/api/heartbeat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
playerId,
videoId
})
});
if (heartbeatResponse.ok) {
console.log(`✅ Heartbeat started for player: ${playerId}`);
// Wait a bit
await new Promise(resolve => setTimeout(resolve, 2000));
// Stop heartbeat
const disconnectResponse = await fetch(`${BASE_URL}/api/heartbeat`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
playerId
})
});
if (disconnectResponse.ok) {
console.log(`✅ Heartbeat stopped for player: ${playerId}`);
}
}
// Test 4: Check process cleanup
console.log('\n4. Checking process cleanup...');
const processesResponse = await fetch(`${BASE_URL}/api/processes`);
if (processesResponse.ok) {
const processes = await processesResponse.json();
console.log(`✅ Active processes: ${processes.count}`);
if (processes.processes.length > 0) {
console.log(` Processes:`, processes.processes.map(p => `${p.videoId} (${p.duration}ms)`));
}
}
console.log('\n🎉 Progress bar tests completed!');
console.log('\n💡 The progress bar should now work correctly for transcoded videos with:');
console.log(' - Proper duration display');
console.log(' - No jumping during playback');
console.log(' - Accurate progress calculation');
} catch (error) {
console.error('❌ Test failed:', error.message);
}
}
// Run tests
testProgressBar();