359 lines
11 KiB
Markdown
359 lines
11 KiB
Markdown
# 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**:
|
|
```typescript
|
|
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**:
|
|
```tsx
|
|
<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 `progress` events
|
|
- **Visual Feedback**: Shows buffered content in progress bar
|
|
- **Status Display**: Shows buffered duration below progress bar
|
|
|
|
**Implementation**:
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```tsx
|
|
{/* 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
|
|
```typescript
|
|
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
|
|
```typescript
|
|
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
|
|
```typescript
|
|
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:
|
|
```bash
|
|
node test-anti-jitter.mjs
|
|
```
|
|
|
|
### Test Scenarios
|
|
1. **Normal Forward Progress**: All forward updates should pass
|
|
2. **Small Backward Adjustment**: Small adjustments should pass
|
|
3. **Large Backward Jump**: Large backward jumps should be blocked
|
|
4. **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 `handleTimeUpdate` is being called
|
|
- Verify `jitterThreshold` isn't too restrictive
|
|
- Ensure video element has valid duration
|
|
|
|
#### 2. Excessive Throttling
|
|
- Increase `updateThrottle` value
|
|
- Check for rapid timeupdate events
|
|
- Verify video source stability
|
|
|
|
#### 3. Buffer Not Showing
|
|
- Ensure `handleProgress` is attached to `progress` event
|
|
- 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.
|