From 3b648347cd10e717b72e77abbdc6dd543d7fb42f Mon Sep 17 00:00:00 2001 From: sebastjanartic <45803536-sebastjanartic@users.noreply.replit.com> Date: Thu, 7 Aug 2025 09:09:02 +0000 Subject: [PATCH] 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 --- client/src/components/quality-indicator.tsx | 85 +++++++++++++++++++ client/src/components/thumbnail-generator.tsx | 7 +- client/src/components/video-modal.tsx | 83 ++++++++++++++++-- 3 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 client/src/components/quality-indicator.tsx diff --git a/client/src/components/quality-indicator.tsx b/client/src/components/quality-indicator.tsx new file mode 100644 index 0000000..a05834d --- /dev/null +++ b/client/src/components/quality-indicator.tsx @@ -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(""); + const [networkType, setNetworkType] = useState(""); + 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 ( +
+ + {currentQuality} + {networkType && ( + ({networkType}) + )} +
+ ); +} \ No newline at end of file diff --git a/client/src/components/thumbnail-generator.tsx b/client/src/components/thumbnail-generator.tsx index 05b9713..a324d0d 100644 --- a/client/src/components/thumbnail-generator.tsx +++ b/client/src/components/thumbnail-generator.tsx @@ -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); diff --git a/client/src/components/video-modal.tsx b/client/src/components/video-modal.tsx index 2aa48c6..9393d1d 100644 --- a/client/src/components/video-modal.tsx +++ b/client/src/components/video-modal.tsx @@ -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. + {/* Quality Indicator */} + {hlsRef.current && ( + + )} + {/* Video Controls and Share Menu */}
{/* Edit Button */}