11 KiB
Anti-Jitter Progress System Implementation
Overview
This document describes the implementation of Stash's anti-jitter mechanisms in NextAV to solve the streaming buffer jitter problem where progress bars jump backward as new data arrives.
Problem Solved
Before: The progress bar would jump backward when:
- Buffer underruns occurred
- Network delays caused time to "catch up"
- Raw video time updates were directly reflected in the UI
After: Smooth, consistent progress that:
- Prevents backward jumps beyond a threshold
- Throttles updates to prevent excessive re-renders
- Allows small backward adjustments for smooth playback
- Provides visual buffer state feedback
Implementation Details
1. Custom Hook: useAntiJitterProgress
Location: src/lib/use-anti-jitter-progress.ts
Key Features:
- Jitter Threshold: 0.5 seconds - maximum allowed backward jump
- Update Throttling: 100ms minimum between updates
- Progress Reference: Maintains stable progress state
- Buffer Monitoring: Tracks actual buffer state
Core Logic:
const handleTimeUpdate = () => {
if (videoRef.current) {
const now = Date.now();
const video = videoRef.current;
const rawTime = video.currentTime;
const currentProgress = progressRef.current;
// Prevent backward jumps beyond threshold
if (rawTime < currentProgress - jitterThreshold) {
console.log(`[ANTI-JITTER] Blocked backward jump: ${rawTime}s -> ${currentProgress}s`);
return; // Block this update
}
// Throttle updates to prevent excessive re-renders
if (now - lastUpdateRef.current < updateThrottle) {
return;
}
// Validate and update progress
if (rawTime >= 0 && rawTime <= (duration || Infinity)) {
if (rawTime >= currentProgress - 0.1) {
// Forward progress or small adjustment
progressRef.current = rawTime;
setCurrentTime(rawTime);
lastUpdateRef.current = now;
}
}
}
};
2. Enhanced Progress Bar
Features:
- Buffer Visualization: Blue overlay shows buffered content
- Smooth Progress: Blue bar shows current playback position
- Seek Overlay: Invisible range input for seeking
- Buffer Status: Text display of buffered duration
Visual Elements:
<div className="relative w-full h-2 bg-gray-600 rounded-lg overflow-hidden">
{/* Buffer indicator */}
{bufferState.buffered > 0 && (
<div
className="absolute top-0 h-full bg-blue-400/30 rounded-lg"
style={{
width: `${Math.min((bufferState.buffered / (duration || 1)) * 100, 100)}%`,
left: '0'
}}
/>
)}
{/* Progress indicator */}
<div
className="absolute top-0 h-full bg-blue-500 rounded-lg transition-all duration-100"
style={{
width: `${Math.min((currentTime / (duration || 1)) * 100, 100)}%`,
left: '0'
}}
/>
{/* Seek input overlay */}
<input
type="range"
min="0"
max={duration || 0}
value={currentTime}
onChange={handleSeek}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
</div>
3. Anti-Jitter Mechanisms
A. Backward Jump Prevention
- Threshold: 0.5 seconds maximum backward jump
- Logic: Blocks updates that would cause large backward movement
- Benefit: Prevents jarring progress bar jumps
B. Update Throttling
- Frequency: Maximum 10 updates per second (100ms throttle)
- Logic: Skips updates that come too quickly
- Benefit: Smoother UI performance, less CPU usage
C. Small Adjustment Allowance
- Threshold: 0.1 seconds for small backward adjustments
- Logic: Allows minor corrections for smooth playback
- Benefit: Maintains playback quality while preventing large jumps
D. Progress Reference Management
- State: Maintains stable progress reference
- Updates: Only updates when progress is valid
- Reset: Resets on video source changes
4. Buffer State Monitoring
Features:
- Real-time Tracking: Monitors
progressevents - Visual Feedback: Shows buffered content in progress bar
- Status Display: Shows buffered duration below progress bar
Implementation:
const handleProgress = () => {
if (videoRef.current) {
const video = videoRef.current;
if (video.buffered.length > 0) {
const bufferedEnd = video.buffered.end(video.buffered.length - 1);
bufferStateRef.current = {
buffered: bufferedEnd,
lastBufferUpdate: Date.now()
};
console.log(`[BUFFER] Buffered to ${bufferedEnd}s`);
}
}
};
Usage
1. In Video Components
import { useAntiJitterProgress } from '@/lib/use-anti-jitter-progress';
export default function VideoViewer({ video, isOpen, onClose }) {
const videoRef = useRef<HTMLVideoElement>(null);
// Use the anti-jitter progress hook
const {
currentTime,
bufferState,
handleTimeUpdate,
handleProgress,
seekTo,
resetProgress
} = useAntiJitterProgress(videoRef, duration);
// Add event listeners
useEffect(() => {
if (videoRef.current) {
videoRef.current.addEventListener('timeupdate', handleTimeUpdate);
videoRef.current.addEventListener('progress', handleProgress);
return () => {
videoRef.current?.removeEventListener('timeupdate', handleTimeUpdate);
videoRef.current?.removeEventListener('progress', handleProgress);
};
}
}, []);
// Handle seeking
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
const newTime = parseFloat(e.target.value);
seekTo(newTime);
};
// Reset on video change
useEffect(() => {
resetProgress();
}, [video]);
}
2. Enhanced Progress Bar
{/* Enhanced Progress bar with buffer visualization */}
<div className="mb-4">
<div className="relative w-full h-2 bg-gray-600 rounded-lg overflow-hidden">
{/* Buffer indicator */}
{bufferState.buffered > 0 && (
<div
className="absolute top-0 h-full bg-blue-400/30 rounded-lg"
style={{
width: `${Math.min((bufferState.buffered / (duration || 1)) * 100, 100)}%`,
left: '0'
}}
/>
)}
{/* Progress indicator */}
<div
className="absolute top-0 h-full bg-blue-500 rounded-lg transition-all duration-100"
style={{
width: `${Math.min((currentTime / (duration || 1)) * 100, 100)}%`,
left: '0'
}}
/>
{/* Seek input overlay */}
<input
type="range"
min="0"
max={duration || 0}
value={currentTime}
onChange={handleSeek}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
</div>
<div className="flex justify-between text-white text-sm mt-1">
<span>{formatTime(currentTime)}</span>
<span>{formatTime(duration)}</span>
</div>
{/* Buffer status */}
{bufferState.buffered > 0 && (
<div className="text-xs text-blue-300 mt-1">
Buffered: {formatTime(bufferState.buffered)}
</div>
)}
</div>
Configuration
Jitter Threshold
const jitterThreshold = 0.5; // Maximum allowed backward jump in seconds
- Lower values: More strict, less jitter but potentially choppy
- Higher values: More lenient, smoother but potential for jumps
- Recommended: 0.5 seconds (good balance)
Update Throttle
const updateThrottle = 100; // Minimum ms between updates
- Lower values: More responsive but higher CPU usage
- Higher values: Smoother but less responsive
- Recommended: 100ms (10 FPS, good balance)
Small Adjustment Threshold
if (rawTime >= currentProgress - 0.1) // 0.1 seconds
- Lower values: Stricter progress validation
- Higher values: More lenient with small adjustments
- Recommended: 0.1 seconds (allows smooth playback)
Testing
Test Script
Run the test script to verify the anti-jitter system:
node test-anti-jitter.mjs
Test Scenarios
- Normal Forward Progress: All forward updates should pass
- Small Backward Adjustment: Small adjustments should pass
- Large Backward Jump: Large backward jumps should be blocked
- Rapid Updates: Rapid updates should be throttled
Benefits
1. User Experience
- Smooth Progress: No more jarring backward jumps
- Visual Feedback: Clear buffer state indication
- Consistent Behavior: Predictable progress bar movement
2. Performance
- Reduced Re-renders: Throttled updates improve performance
- Stable State: Progress reference prevents unnecessary updates
- Efficient Monitoring: Smart event handling
3. Reliability
- Jitter Prevention: Blocks problematic time updates
- Buffer Awareness: Tracks actual buffered content
- Error Handling: Graceful fallbacks for edge cases
Future Enhancements
1. Adaptive Thresholds
- Dynamic Jitter Threshold: Adjust based on video quality
- Network-Aware Throttling: Adapt to connection speed
- Quality-Based Settings: Different thresholds for different scenarios
2. Advanced Buffer Management
- Predictive Buffering: Anticipate buffer needs
- Quality Adaptation: Switch quality based on buffer state
- Network Monitoring: Track connection health
3. Enhanced Visualization
- Buffer Prediction: Show predicted buffer state
- Quality Indicators: Visual quality level indicators
- Network Status: Connection health indicators
Troubleshooting
Common Issues
1. Progress Bar Not Moving
- Check if
handleTimeUpdateis being called - Verify
jitterThresholdisn't too restrictive - Ensure video element has valid duration
2. Excessive Throttling
- Increase
updateThrottlevalue - Check for rapid timeupdate events
- Verify video source stability
3. Buffer Not Showing
- Ensure
handleProgressis attached toprogressevent - Check if video has buffered ranges
- Verify buffer state updates
Debug Logging
The system provides comprehensive logging:
[ANTI-JITTER] Blocked backward jump: 2s -> 5s
[THROTTLE] Skipped update: 50ms < 100ms
[BUFFER] Buffered to 10s
[SEEK] Seeking to 15s, updated progress reference
[PROGRESS] Forward progress: 16s
Conclusion
The anti-jitter progress system successfully implements Stash's approach to solve streaming buffer jitter. By preventing backward jumps, throttling updates, and providing visual feedback, it creates a smooth, professional video playback experience.
The system is:
- Configurable: Easy to adjust thresholds and behavior
- Reusable: Shared hook for multiple components
- Efficient: Minimal performance impact
- Reliable: Handles edge cases gracefully
This implementation provides the foundation for professional-grade video streaming without the jittery behavior common in basic implementations.