Improve video streaming with dynamic quality adjustments
Integrate adaptive bitrate streaming using HLS.js, optimizing playback based on network conditions and buffer levels for a smoother user experience. Add a quality indicator component. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 11420304-80a9-4ef2-adff-cbdaa418ffa8 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/11420304-80a9-4ef2-adff-cbdaa418ffa8/xcLcvwj
This commit is contained in:
parent
9d55c83811
commit
3b648347cd
85
client/src/components/quality-indicator.tsx
Normal file
85
client/src/components/quality-indicator.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Signal } from "lucide-react";
|
||||
|
||||
interface QualityIndicatorProps {
|
||||
hlsInstance: any;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function QualityIndicator({ hlsInstance, className = "" }: QualityIndicatorProps) {
|
||||
const [currentQuality, setCurrentQuality] = useState<string>("");
|
||||
const [networkType, setNetworkType] = useState<string>("");
|
||||
const [bufferHealth, setBufferHealth] = useState<"good" | "medium" | "poor">("good");
|
||||
|
||||
useEffect(() => {
|
||||
if (!hlsInstance) return;
|
||||
|
||||
// Monitor quality changes
|
||||
const handleLevelSwitch = (event: any, data: any) => {
|
||||
const level = hlsInstance.levels[data.level];
|
||||
setCurrentQuality(`${level.height}p`);
|
||||
};
|
||||
|
||||
// Monitor buffer health
|
||||
const handleBufferAppending = () => {
|
||||
const video = hlsInstance.media;
|
||||
if (video && video.buffered.length > 0) {
|
||||
const bufferLevel = video.buffered.end(video.buffered.length - 1) - video.currentTime;
|
||||
if (bufferLevel > 10) {
|
||||
setBufferHealth("good");
|
||||
} else if (bufferLevel > 3) {
|
||||
setBufferHealth("medium");
|
||||
} else {
|
||||
setBufferHealth("poor");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
hlsInstance.on('hlsLevelSwitched', handleLevelSwitch);
|
||||
hlsInstance.on('hlsBufferAppending', handleBufferAppending);
|
||||
|
||||
// Detect network connection
|
||||
const connection = (navigator as any).connection;
|
||||
if (connection) {
|
||||
setNetworkType(connection.effectiveType || "unknown");
|
||||
|
||||
const handleConnectionChange = () => {
|
||||
setNetworkType(connection.effectiveType || "unknown");
|
||||
};
|
||||
|
||||
connection.addEventListener('change', handleConnectionChange);
|
||||
|
||||
return () => {
|
||||
connection.removeEventListener('change', handleConnectionChange);
|
||||
};
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (hlsInstance) {
|
||||
hlsInstance.off('hlsLevelSwitched', handleLevelSwitch);
|
||||
hlsInstance.off('hlsBufferAppending', handleBufferAppending);
|
||||
}
|
||||
};
|
||||
}, [hlsInstance]);
|
||||
|
||||
const getSignalColor = () => {
|
||||
switch (bufferHealth) {
|
||||
case "good": return "text-green-500";
|
||||
case "medium": return "text-yellow-500";
|
||||
case "poor": return "text-red-500";
|
||||
default: return "text-gray-500";
|
||||
}
|
||||
};
|
||||
|
||||
if (!currentQuality) return null;
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-2 text-sm text-white bg-black/50 px-2 py-1 rounded ${className}`}>
|
||||
<Signal className={`w-4 h-4 ${getSignalColor()}`} />
|
||||
<span>{currentQuality}</span>
|
||||
{networkType && (
|
||||
<span className="text-xs opacity-75">({networkType})</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -60,7 +60,12 @@ export default function ThumbnailGenerator({
|
||||
const hls = new Hls({
|
||||
debug: false,
|
||||
enableWorker: false,
|
||||
lowLatencyMode: false
|
||||
lowLatencyMode: false,
|
||||
// Optimized for thumbnail generation - prioritize speed
|
||||
startLevel: 0, // Start with lowest quality for faster loading
|
||||
maxBufferLength: 10, // Smaller buffer for thumbnail generation
|
||||
fragLoadingTimeOut: 10000,
|
||||
manifestLoadingTimeOut: 5000
|
||||
});
|
||||
|
||||
hls.loadSource(videoUrl);
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
WhatsappIcon
|
||||
} from "react-share";
|
||||
import VideoEditModal from "./video-edit-modal";
|
||||
import QualityIndicator from "./quality-indicator";
|
||||
|
||||
// HLS.js types for video streaming
|
||||
|
||||
@ -100,31 +101,89 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
|
||||
const hls = new Hls({
|
||||
debug: false,
|
||||
enableWorker: false,
|
||||
lowLatencyMode: true,
|
||||
backBufferLength: 90
|
||||
lowLatencyMode: false,
|
||||
// Adaptive bitrate settings for optimal streaming
|
||||
startLevel: -1, // Auto-select starting quality based on bandwidth
|
||||
capLevelToPlayerSize: true, // Limit quality to actual player size
|
||||
maxLoadingDelay: 4,
|
||||
maxBufferLength: 30, // Keep 30 seconds buffered
|
||||
maxBufferSize: 60 * 1000 * 1000, // 60MB buffer
|
||||
maxBufferHole: 0.5,
|
||||
// Network adaptive settings
|
||||
abrEwmaFastLive: 3,
|
||||
abrEwmaSlowLive: 9,
|
||||
abrEwmaFastVoD: 3,
|
||||
abrEwmaSlowVoD: 9,
|
||||
abrMaxWithRealBitrate: false,
|
||||
abrBandWidthFactor: 0.95, // Conservative bandwidth usage
|
||||
abrBandWidthUpFactor: 0.7, // Slower quality upgrades
|
||||
// Fragment loading settings
|
||||
fragLoadingTimeOut: 20000,
|
||||
manifestLoadingTimeOut: 10000,
|
||||
levelLoadingTimeOut: 10000,
|
||||
// Start with lower quality for faster initial load
|
||||
testBandwidth: false
|
||||
});
|
||||
|
||||
hls.loadSource(videoUrl);
|
||||
hls.attachMedia(videoElement);
|
||||
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
console.log('HLS manifest loaded successfully');
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, (event, data) => {
|
||||
console.log('HLS manifest loaded - Available qualities:',
|
||||
data.levels.map(l => `${l.height}p @ ${Math.round(l.bitrate/1000)}kbps`));
|
||||
|
||||
// Set initial quality based on connection
|
||||
const connection = (navigator as any).connection;
|
||||
if (connection) {
|
||||
const effectiveType = connection.effectiveType;
|
||||
console.log('Network type detected:', effectiveType);
|
||||
|
||||
// Adjust starting level based on network
|
||||
if (effectiveType === 'slow-2g' || effectiveType === '2g') {
|
||||
hls.startLevel = 0; // Start with lowest quality
|
||||
} else if (effectiveType === '3g') {
|
||||
hls.startLevel = Math.min(1, data.levels.length - 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Quality level monitoring
|
||||
hls.on(Hls.Events.LEVEL_SWITCHED, (event, data) => {
|
||||
const level = hls.levels[data.level];
|
||||
console.log(`Kakovost preklopljena na: ${level.height}p @ ${Math.round(level.bitrate/1000)}kbps`);
|
||||
});
|
||||
|
||||
// Buffer monitoring for dynamic adjustment
|
||||
hls.on(Hls.Events.BUFFER_APPENDING, () => {
|
||||
const buffered = videoElement.buffered;
|
||||
if (buffered.length > 0) {
|
||||
const bufferLevel = buffered.end(buffered.length - 1) - videoElement.currentTime;
|
||||
if (bufferLevel < 2) {
|
||||
console.log('Nizek buffer zaznan, lahko zmanjšam kakovost');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Network error handling with retries
|
||||
hls.on(Hls.Events.ERROR, (event, data) => {
|
||||
console.error('HLS error:', data);
|
||||
console.error('HLS napaka:', data);
|
||||
if (data.fatal) {
|
||||
switch (data.type) {
|
||||
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||
console.log('Network error, trying to recover...');
|
||||
console.log('Omrežna napaka, poskušam obnoviti...');
|
||||
// Try to downgrade quality first
|
||||
if (hls.currentLevel > 0) {
|
||||
hls.currentLevel = hls.currentLevel - 1;
|
||||
console.log('Zmanjšujem kakovost zaradi omrežnih težav');
|
||||
}
|
||||
hls.startLoad();
|
||||
break;
|
||||
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||
console.log('Media error, trying to recover...');
|
||||
console.log('Medijska napaka, poskušam obnoviti...');
|
||||
hls.recoverMediaError();
|
||||
break;
|
||||
default:
|
||||
console.log('Fatal error, destroying HLS instance...');
|
||||
console.log('Kritična napaka, uničujem HLS instanco...');
|
||||
hls.destroy();
|
||||
break;
|
||||
}
|
||||
@ -274,6 +333,14 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
|
||||
{/* Quality Indicator */}
|
||||
{hlsRef.current && (
|
||||
<QualityIndicator
|
||||
hlsInstance={hlsRef.current}
|
||||
className="absolute top-4 left-4"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Video Controls and Share Menu */}
|
||||
<div className="absolute top-4 right-4 flex gap-2">
|
||||
{/* Edit Button */}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user