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({
|
const hls = new Hls({
|
||||||
debug: false,
|
debug: false,
|
||||||
enableWorker: 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);
|
hls.loadSource(videoUrl);
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
WhatsappIcon
|
WhatsappIcon
|
||||||
} from "react-share";
|
} from "react-share";
|
||||||
import VideoEditModal from "./video-edit-modal";
|
import VideoEditModal from "./video-edit-modal";
|
||||||
|
import QualityIndicator from "./quality-indicator";
|
||||||
|
|
||||||
// HLS.js types for video streaming
|
// HLS.js types for video streaming
|
||||||
|
|
||||||
@ -100,31 +101,89 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
|
|||||||
const hls = new Hls({
|
const hls = new Hls({
|
||||||
debug: false,
|
debug: false,
|
||||||
enableWorker: false,
|
enableWorker: false,
|
||||||
lowLatencyMode: true,
|
lowLatencyMode: false,
|
||||||
backBufferLength: 90
|
// 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.loadSource(videoUrl);
|
||||||
hls.attachMedia(videoElement);
|
hls.attachMedia(videoElement);
|
||||||
|
|
||||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
hls.on(Hls.Events.MANIFEST_PARSED, (event, data) => {
|
||||||
console.log('HLS manifest loaded successfully');
|
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) => {
|
hls.on(Hls.Events.ERROR, (event, data) => {
|
||||||
console.error('HLS error:', data);
|
console.error('HLS napaka:', data);
|
||||||
if (data.fatal) {
|
if (data.fatal) {
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case Hls.ErrorTypes.NETWORK_ERROR:
|
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();
|
hls.startLoad();
|
||||||
break;
|
break;
|
||||||
case Hls.ErrorTypes.MEDIA_ERROR:
|
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||||
console.log('Media error, trying to recover...');
|
console.log('Medijska napaka, poskušam obnoviti...');
|
||||||
hls.recoverMediaError();
|
hls.recoverMediaError();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log('Fatal error, destroying HLS instance...');
|
console.log('Kritična napaka, uničujem HLS instanco...');
|
||||||
hls.destroy();
|
hls.destroy();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -274,6 +333,14 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
|
|||||||
Your browser does not support the video tag.
|
Your browser does not support the video tag.
|
||||||
</video>
|
</video>
|
||||||
|
|
||||||
|
{/* Quality Indicator */}
|
||||||
|
{hlsRef.current && (
|
||||||
|
<QualityIndicator
|
||||||
|
hlsInstance={hlsRef.current}
|
||||||
|
className="absolute top-4 left-4"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Video Controls and Share Menu */}
|
{/* Video Controls and Share Menu */}
|
||||||
<div className="absolute top-4 right-4 flex gap-2">
|
<div className="absolute top-4 right-4 flex gap-2">
|
||||||
{/* Edit Button */}
|
{/* Edit Button */}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user