# Live Transcoding Progress Bar Accuracy - Anti-Jitter Mechanisms ## Core Problem Analysis Live transcoding creates unique challenges for accurate progress reporting: 1. **Duration Drift**: Transcoded segments may have slightly different durations than expected 2. **Buffer Timing**: Real-time transcoding can't provide accurate total duration until completion 3. **Seek Inconsistency**: Seeking to unbuffered positions causes progress jumps 4. **Network Variability**: Connection issues cause buffering delays 5. **Segment Validation**: Incomplete or corrupted segments affect progress accuracy ## Stash's Anti-Jitter Strategy ### 1. Precise Duration Extraction **From ffprobe.go** - Accurate duration calculation: ```go // pkg/ffmpeg/ffprobe.go - parse function func parse(filePath string, probeJSON *FFProbeJSON) (*VideoFile, error) { result := &VideoFile{} // Primary duration from video stream duration, _ := strconv.ParseFloat(videoStream.Duration, 64) result.VideoStreamDuration = math.Round(duration*100) / 100 // Fallback to container duration with rounding if result.VideoStreamDuration == 0 { duration, _ := strconv.ParseFloat(probeJSON.Format.Duration, 64) result.VideoStreamDuration = math.Round(duration*100) / 100 } // Store both for validation result.FileDuration = result.VideoStreamDuration return result } ``` ### 2. Segment-Based Progress Tracking **From stream_segmented.go** - Atomic segment generation: ```go // internal/manager/stream_segmented.go func (tp *transcodeProcess) serveHLSManifest() []byte { var buf bytes.Buffer buf.WriteString("#EXTM3U\n") buf.WriteString("#EXT-X-VERSION:3\n") buf.WriteString("#EXT-X-TARGETDURATION:2\n") // Pre-calculated exact durations leftover := tp.videoFile.VideoStreamDuration segment := 0 for leftover > 0 { thisLength := math.Min(float64(segmentLength), leftover) fmt.Fprintf(&buf, "#EXTINF:%f,\n", thisLength) fmt.Fprintf(&buf, "segment_%06d.ts\n", segment) leftover -= thisLength segment++ } return buf.Bytes() } ``` ### 3. Anti-Jitter Mechanisms #### A. Segment Atomicity ```typescript // Frontend segment validation interface SegmentValidation { segmentNumber: number; expectedDuration: number; actualDuration?: number; isComplete: boolean; isValid: boolean; } const validateSegment = async ( segmentNumber: number, expectedDuration: number ): Promise => { try { const response = await fetch(`/api/segments/${segmentNumber}`); const contentLength = response.headers.get('content-length'); const actualDuration = await calculateDurationFromBytes( parseInt(contentLength || '0') ); return { segmentNumber, expectedDuration, actualDuration, isComplete: actualDuration >= expectedDuration * 0.95, isValid: actualDuration >= expectedDuration * 0.8 }; } catch (error) { return { segmentNumber, expectedDuration, isComplete: false, isValid: false }; } }; ``` #### B. Buffer State Management ```typescript // From ScenePlayerScrubber.tsx - Smooth progress updates const useSmoothProgress = (currentTime: number, duration: number) => { const [displayTime, setDisplayTime] = useState(currentTime); useEffect(() => { const targetTime = Math.min(currentTime, duration); const diff = targetTime - displayTime; // Only update if significant change or approaching end if (Math.abs(diff) > 0.1 || targetTime >= duration - 1) { setDisplayTime(targetTime); } }, [currentTime, duration]); return displayTime; }; ``` #### C. Seek Position Validation ```typescript // Validate seek positions against actual buffer state const validateSeekPosition = ( requestedTime: number, bufferedRanges: TimeRanges, duration: number ): number => { let validTime = requestedTime; // Find nearest buffered segment for (let i = 0; i < bufferedRanges.length; i++) { if ( requestedTime >= bufferedRanges.start(i) && requestedTime <= bufferedRanges.end(i) ) { return requestedTime; // Exact match } if (requestedTime < bufferedRanges.start(i)) { validTime = bufferedRanges.start(i); // Snap to nearest available break; } } return Math.max(0, Math.min(validTime, duration)); }; ``` ## Real-time Transcoding Coordination ### Backend Process Monitoring ```go // From internal/manager/stream_segmented.go type transcodeProcess struct { videoFile *models.VideoFile outputDir string segmentCount int // Real-time state tracking lastSegment int waitingSegment chan int // Progress synchronization mu sync.RWMutex currentTime float64 totalTime float64 } func (tp *transcodeProcess) updateProgress(segment int, duration float64) { tp.mu.Lock() tp.currentTime = float64(segment) * 2.0 // 2-second segments tp.mu.Unlock() // Notify waiting clients select { case tp.waitingSegment <- segment: default: } } func (tp *transcodeProcess) getProgress() (float64, float64) { tp.mu.RLock() defer tp.mu.RUnlock() return tp.currentTime, tp.totalTime } ``` ## Frontend Smoothing Layers ### Multi-layer Progress Smoothing ```typescript // Progressive enhancement for progress accuracy const useAccurateProgress = ( playerRef: RefObject, totalDuration: number ) => { const [progress, setProgress] = useState(0); const [buffered, setBuffered] = useState(0); const [isTranscoding, setIsTranscoding] = useState(true); useEffect(() => { const video = playerRef.current; if (!video) return; // Primary source: video.currentTime const handleTimeUpdate = debounce(() => { const newProgress = video.currentTime / totalDuration; // Prevent backward jumps if (newProgress >= progress || newProgress >= 0.99) { setProgress(Math.min(newProgress, 1)); } }, 100); // Secondary source: buffered ranges const handleProgress = debounce(() => { const bufferedEnd = video.buffered.length > 0 ? video.buffered.end(video.buffered.length - 1) : 0; setBuffered(bufferedEnd / totalDuration); }, 250); // Debounced updates for smooth UI const debouncedTimeUpdate = debounce(handleTimeUpdate, 100); const debouncedProgress = debounce(handleProgress, 250); video.addEventListener('timeupdate', debouncedTimeUpdate); video.addEventListener('progress', debouncedProgress); return () => { video.removeEventListener('timeupdate', debouncedTimeUpdate); video.removeEventListener('progress', debouncedProgress); }; }, [playerRef, totalDuration]); return { progress, buffered, isTranscoding }; }; ``` ## Next.js Implementation Strategy ### API Route with Progress Tracking ```typescript // pages/api/transcode/[...slug].ts export default async function handler(req: NextApiRequest, res: NextApiResponse) { const { file, quality, start } = req.query; // 1. Extract precise duration first const metadata = await extractVideoMetadata(file as string); res.setHeader('X-Total-Duration', metadata.duration); // 2. Segment-based streaming const segmentDuration = 2; // 2-second segments const totalSegments = Math.ceil(metadata.duration / segmentDuration); // 3. Generate accurate manifest const manifest = generateHLSManifest(totalSegments, segmentDuration, metadata.duration); res.setHeader('Content-Type', 'application/vnd.apple.mpegurl'); res.send(manifest); } // Segment serving with validation export const serveSegment = async (req: NextApiRequest, res: NextApiResponse) => { const { segment } = req.query; const segmentPath = `segments/segment_${segment.toString().padStart(6, '0')}.ts`; // Validate segment exists and is complete try { const stats = await fs.stat(segmentPath); if (stats.size === 0) { return res.status(404).end(); } // Check segment integrity const isValid = await validateSegmentIntegrity(segmentPath); if (!isValid) { return res.status(410).end(); // Gone - segment invalid } res.setHeader('Content-Type', 'video/mp2t'); res.setHeader('Content-Length', stats.size); const stream = createReadStream(segmentPath); stream.pipe(res); // Cleanup on client disconnect req.on('close', () => { stream.destroy(); }); } catch (error) { res.status(404).end(); } }; ``` ### Client-Side Integration ```typescript // hooks/useTranscodeProgress.ts import { useState, useEffect, useRef } from 'react'; export const useTranscodeProgress = ( videoUrl: string, totalDuration: number ) => { const [progress, setProgress] = useState(0); const [buffered, setBuffered] = useState(0); const [isTranscoding, setIsTranscoding] = useState(true); const [quality, setQuality] = useState('720p'); const videoRef = useRef(null); const progressRef = useRef(0); const lastUpdateRef = useRef(Date.now()); useEffect(() => { const video = videoRef.current; if (!video) return; // Prevent backward jumps and smooth updates const handleTimeUpdate = () => { const now = Date.now(); if (now - lastUpdateRef.current < 100) return; // Throttle updates const newProgress = video.currentTime / totalDuration; // Only allow forward progress or small backward adjustments if (newProgress >= progressRef.current - 0.005) { progressRef.current = newProgress; setProgress(newProgress); lastUpdateRef.current = now; } }; // Monitor transcoding completion const checkTranscodingStatus = async () => { try { const response = await fetch(`/api/transcode/status?url=${encodeURIComponent(videoUrl)}`); const data = await response.json(); setIsTranscoding(data.transcoding); // Update total duration if changed if (data.duration && Math.abs(data.duration - totalDuration) > 1) { // Handle duration correction } } catch (error) { console.error('Failed to check transcoding status:', error); } }; // Monitor buffer state const handleBufferUpdate = () => { if (video.buffered.length > 0) { const bufferedEnd = video.buffered.end(video.buffered.length - 1); setBuffered(bufferedEnd / totalDuration); } }; video.addEventListener('timeupdate', handleTimeUpdate); video.addEventListener('progress', handleBufferUpdate); const interval = setInterval(checkTranscodingStatus, 2000); return () => { video.removeEventListener('timeupdate', handleTimeUpdate); video.removeEventListener('progress', handleBufferUpdate); clearInterval(interval); }; }, [videoUrl, totalDuration]); return { progress, buffered, isTranscoding, videoRef, quality, setQuality }; }; ``` ### Advanced Anti-Jitter Features #### 1. Predictive Buffering ```typescript // Predict next segment and pre-buffer const usePredictiveBuffering = ( currentTime: number, totalDuration: number, playbackRate: number ) => { const [nextSegment, setNextSegment] = useState(null); useEffect(() => { const currentSegment = Math.floor(currentTime / 2); // 2-second segments const bufferAhead = Math.ceil(playbackRate * 10); // 10 seconds ahead const targetSegment = currentSegment + bufferAhead; if (targetSegment * 2 < totalDuration) { setNextSegment(targetSegment); } }, [currentTime, totalDuration, playbackRate]); return nextSegment; }; ``` #### 2. Quality Adaptation ```typescript // Adapt quality based on buffer state const useAdaptiveQuality = ( buffered: number, progress: number, bandwidth: number ) => { const [quality, setQuality] = useState('720p'); useEffect(() => { const bufferRatio = buffered - progress; if (bufferRatio < 0.1) { setQuality('480p'); // Lower quality for poor buffering } else if (bandwidth > 5000000) { setQuality('1080p'); // Higher quality for good bandwidth } else { setQuality('720p'); // Default quality } }, [buffered, progress, bandwidth]); return quality; }; ``` ## Key Implementation Guidelines ### 1. Duration Accuracy - Always extract duration from video stream metadata, not container - Use ffprobe/ffmpeg for precise duration extraction - Validate duration against actual transcoded segments - Handle edge cases (variable frame rate, corrupted metadata) ### 2. Progress Synchronization - Use segment-based streaming (HLS/DASH) for granular control - Implement atomic segment generation (never overwrite existing) - Provide real-time transcoding status to frontend - Handle dynamic duration changes gracefully ### 3. Anti-Jitter Strategies - **Segment Validation**: Only serve complete, validated segments - **State Consistency**: Maintain consistent state between backend and frontend - **Smooth Updates**: Debounce progress updates and prevent backward jumps - **Buffer Awareness**: Track actual buffer state vs transcoding progress ### 4. Error Handling - **Graceful Degradation**: Remove incomplete segments rather than serving corrupted data - **Timeout Management**: 15-second timeout for segment generation - **Fallback Strategies**: Multiple quality levels for connection issues - **Recovery Mechanisms**: Restart failed segments without user intervention This comprehensive approach ensures smooth, accurate progress bars even during live transcoding, eliminating the jittery behavior common in streaming applications.