mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +02:00
feat: add mobile-optimized IPTV player with HLS and MpegTS support
This commit is contained in:
1 parent
3c9a9c7be7
commit
fa6ae90d8d
38 files changed
+1280
-895
No files matched your search
@@ -1,10 +1,8 @@
|
||||
/// Adaptive Bitrate (ABR) Streaming Handler for HLS
|
||||
///
|
||||
///
|
||||
/// Supports multi-bitrate HLS playlists for adaptive streaming
|
||||
/// with fallback options for poor network conditions
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
library;
|
||||
|
||||
/// Represents a video quality level for adaptive bitrate streaming
|
||||
class QualityLevel {
|
||||
@@ -108,23 +106,23 @@ class QualityProfiles {
|
||||
|
||||
/// Get default profiles for balanced streaming
|
||||
static List<QualityLevel> getBalancedProfiles() => [
|
||||
mobile240p,
|
||||
mobile360p,
|
||||
hd480p,
|
||||
hd720p,
|
||||
fhd1080p,
|
||||
];
|
||||
mobile240p,
|
||||
mobile360p,
|
||||
hd480p,
|
||||
hd720p,
|
||||
fhd1080p,
|
||||
];
|
||||
|
||||
/// Get all available profiles
|
||||
static List<QualityLevel> getAllProfiles() => [
|
||||
mobile240p,
|
||||
mobile360p,
|
||||
hd480p,
|
||||
hd720p,
|
||||
fhd1080p,
|
||||
uhd2k,
|
||||
uhd4k,
|
||||
];
|
||||
mobile240p,
|
||||
mobile360p,
|
||||
hd480p,
|
||||
hd720p,
|
||||
fhd1080p,
|
||||
uhd2k,
|
||||
uhd4k,
|
||||
];
|
||||
|
||||
/// Get profiles suitable for streaming type
|
||||
static List<QualityLevel> getProfiles({
|
||||
@@ -159,8 +157,8 @@ class QualityProfiles {
|
||||
/// Bandwidth detector for adaptive bitrate selection
|
||||
class BandwidthDetector {
|
||||
final _samples = <DateTime, int>{}; // timestamp -> bytes downloaded
|
||||
int _totalBytesLastPeriod = 0;
|
||||
Duration _sampleWindow = const Duration(seconds: 10);
|
||||
final int _totalBytesLastPeriod = 0;
|
||||
final Duration _sampleWindow = const Duration(seconds: 10);
|
||||
|
||||
/// Record bytes downloaded at current time
|
||||
void recordBytes(int bytes) {
|
||||
@@ -177,7 +175,9 @@ class BandwidthDetector {
|
||||
if (_samples.isEmpty) return 0;
|
||||
|
||||
int totalBytes = 0;
|
||||
_samples.values.forEach((bytes) => totalBytes += bytes);
|
||||
for (var bytes in _samples.values) {
|
||||
totalBytes += bytes;
|
||||
}
|
||||
|
||||
final timeSpan = DateTime.now()
|
||||
.difference(
|
||||
@@ -236,10 +236,10 @@ class HlsMasterPlaylistGenerator {
|
||||
|
||||
// Variant streams (quality levels)
|
||||
for (final quality in qualityLevels) {
|
||||
buffer.writeln('#EXT-X-STREAM-INF:BANDWIDTH=${quality.bitrateBps},'
|
||||
'RESOLUTION=${quality.width}x${quality.height}');
|
||||
buffer
|
||||
.writeln('#EXT-X-STREAM-INF:BANDWIDTH=${quality.bitrateBps},'
|
||||
'RESOLUTION=${quality.width}x${quality.height}');
|
||||
buffer.writeln('$baseUrl/variant_${quality.width}x${quality.height}.m3u8');
|
||||
.writeln('$baseUrl/variant_${quality.width}x${quality.height}.m3u8');
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
@@ -317,7 +317,8 @@ class QualitySelector {
|
||||
|
||||
// Only switch if significantly different from current
|
||||
final overhead = _currentQuality.bitrateBps * 1.2;
|
||||
if (bandwidthBps > overhead || bandwidthBps < _currentQuality.bitrateBps * 0.5) {
|
||||
if (bandwidthBps > overhead ||
|
||||
bandwidthBps < _currentQuality.bitrateBps * 0.5) {
|
||||
final bestQuality =
|
||||
BandwidthDetector.selectBestQuality(bandwidthBps, availableProfiles);
|
||||
setQuality(bestQuality);
|
||||
|
||||
@@ -12,8 +12,8 @@ class CacheEntry<T> {
|
||||
required this.data,
|
||||
required this.sizeEstimate,
|
||||
}) : timestamp = DateTime.now(),
|
||||
lastAccess = DateTime.now(),
|
||||
accessCount = 0;
|
||||
lastAccess = DateTime.now(),
|
||||
accessCount = 0;
|
||||
|
||||
bool isExpired({Duration ttl = const Duration(hours: 24)}) {
|
||||
return DateTime.now().difference(timestamp) > ttl;
|
||||
@@ -52,7 +52,7 @@ class CacheService {
|
||||
|
||||
/// Get cached data
|
||||
T? get<T>(String key) {
|
||||
final entry = _cache[key] as CacheEntry?;
|
||||
final entry = _cache[key];
|
||||
if (entry != null && !entry.isExpired()) {
|
||||
entry.markAccess();
|
||||
return entry.data as T;
|
||||
@@ -68,11 +68,12 @@ class CacheService {
|
||||
|
||||
/// Get cache stats
|
||||
Map<String, dynamic> getStats() => {
|
||||
'totalSize': _totalSizeBytes,
|
||||
'totalSizeMb': _totalSizeBytes / (1024 * 1024),
|
||||
'itemCount': _cache.length,
|
||||
'averageSizePerItem': _cache.isEmpty ? 0 : _totalSizeBytes ~/ _cache.length,
|
||||
};
|
||||
'totalSize': _totalSizeBytes,
|
||||
'totalSizeMb': _totalSizeBytes / (1024 * 1024),
|
||||
'itemCount': _cache.length,
|
||||
'averageSizePerItem':
|
||||
_cache.isEmpty ? 0 : _totalSizeBytes ~/ _cache.length,
|
||||
};
|
||||
|
||||
/// Clear all cache
|
||||
void clear() {
|
||||
@@ -96,7 +97,7 @@ class CacheService {
|
||||
|
||||
/// Trim cache by removing least recently used entries
|
||||
void _trimCacheIfNeeded() {
|
||||
final maxSize = _maxMemoryCacheSizeMb * 1024 * 1024;
|
||||
const maxSize = _maxMemoryCacheSizeMb * 1024 * 1024;
|
||||
if (_totalSizeBytes > maxSize) {
|
||||
// Remove least recently accessed entries
|
||||
final sorted = _cache.entries.toList()
|
||||
@@ -173,10 +174,10 @@ class ImageCacheManager {
|
||||
|
||||
/// Get cache stats
|
||||
Map<String, dynamic> getStats() => {
|
||||
'totalMemoryMb': _totalMemoryBytes / (1024 * 1024),
|
||||
'cachedImages': _imageCache.length,
|
||||
'maxImages': _maxImages,
|
||||
};
|
||||
'totalMemoryMb': _totalMemoryBytes / (1024 * 1024),
|
||||
'cachedImages': _imageCache.length,
|
||||
'maxImages': _maxImages,
|
||||
};
|
||||
|
||||
/// Clear all image cache
|
||||
void clear() {
|
||||
@@ -196,9 +197,8 @@ final imageCacheManagerProvider = Provider<ImageCacheManager>((ref) {
|
||||
});
|
||||
|
||||
/// Provider for cache statistics
|
||||
final cacheStatsProvider = StateNotifierProvider<
|
||||
CacheStatsNotifier,
|
||||
Map<String, dynamic>>((ref) {
|
||||
final cacheStatsProvider =
|
||||
StateNotifierProvider<CacheStatsNotifier, Map<String, dynamic>>((ref) {
|
||||
final cacheService = ref.watch(cacheServiceProvider);
|
||||
return CacheStatsNotifier(cacheService);
|
||||
});
|
||||
@@ -206,8 +206,7 @@ final cacheStatsProvider = StateNotifierProvider<
|
||||
class CacheStatsNotifier extends StateNotifier<Map<String, dynamic>> {
|
||||
final CacheService _cacheService;
|
||||
|
||||
CacheStatsNotifier(this._cacheService)
|
||||
: super(_cacheService.getStats());
|
||||
CacheStatsNotifier(this._cacheService) : super(_cacheService.getStats());
|
||||
|
||||
void refreshStats() {
|
||||
state = _cacheService.getStats();
|
||||
|
||||
@@ -203,7 +203,9 @@ class _RetryInterceptor extends Interceptor {
|
||||
|
||||
@override
|
||||
Future<void> onError(
|
||||
DioException err, ErrorInterceptorHandler handler) async {
|
||||
DioException err,
|
||||
ErrorInterceptorHandler handler,
|
||||
) async {
|
||||
// Only retry on timeout or connection errors
|
||||
if (err.type == DioExceptionType.connectionTimeout ||
|
||||
err.type == DioExceptionType.receiveTimeout) {
|
||||
@@ -248,9 +250,11 @@ final networkConfigProvider =
|
||||
|
||||
class NetworkConfigNotifier extends StateNotifier<NetworkConfig> {
|
||||
NetworkConfigNotifier()
|
||||
: super(const NetworkConfig(
|
||||
userAgent: 'VLC/3.0.18 LibVLC/3.0.18',
|
||||
));
|
||||
: super(
|
||||
const NetworkConfig(
|
||||
userAgent: 'VLC/3.0.18 LibVLC/3.0.18',
|
||||
),
|
||||
);
|
||||
|
||||
void setProxy(String? proxyUrl) {
|
||||
state = state.copyWith(proxyUrl: proxyUrl);
|
||||
|
||||
@@ -23,10 +23,7 @@ class StreamingMetrics {
|
||||
});
|
||||
|
||||
double get totalPlaybackSeconds =>
|
||||
(endTime ?? DateTime.now())
|
||||
.difference(startTime)
|
||||
.inSeconds
|
||||
.toDouble();
|
||||
(endTime ?? DateTime.now()).difference(startTime).inSeconds.toDouble();
|
||||
|
||||
double get qualityScore {
|
||||
// Score based on metrics
|
||||
@@ -67,7 +64,7 @@ class StreamingOptimizer {
|
||||
StreamController.broadcast();
|
||||
late Timer _metricsTimer;
|
||||
|
||||
int _segmentStartTime = 0;
|
||||
final int _segmentStartTime = 0;
|
||||
int _totalBytesDownloaded = 0;
|
||||
int _rebufferCount = 0;
|
||||
int _lastBufferNotificationTime = 0;
|
||||
@@ -98,8 +95,7 @@ class StreamingOptimizer {
|
||||
final bandwidthBps = qualitySelector.getEstimatedBandwidthMbps() * 1000000;
|
||||
_currentMetrics = StreamingMetrics(
|
||||
averageNetworkBitrate: _totalBytesDownloaded > 0
|
||||
? (_totalBytesDownloaded * 8 /
|
||||
_currentMetrics.totalPlaybackSeconds)
|
||||
? (_totalBytesDownloaded * 8 / _currentMetrics.totalPlaybackSeconds)
|
||||
: 0,
|
||||
currentNetworkBitrate: bandwidthBps,
|
||||
bufferDurationMs: _currentMetrics.bufferDurationMs,
|
||||
@@ -235,14 +231,16 @@ class StreamingOptimizerNotifier extends StateNotifier<StreamingMetrics> {
|
||||
late StreamingOptimizer _optimizer;
|
||||
|
||||
StreamingOptimizerNotifier(this._qualitySelector)
|
||||
: super(StreamingMetrics(
|
||||
averageNetworkBitrate: 0,
|
||||
currentNetworkBitrate: 0,
|
||||
bufferDurationMs: 0,
|
||||
segmentDownloadTimeMs: 0,
|
||||
rebufferCount: 0,
|
||||
startTime: DateTime.now(),
|
||||
)) {
|
||||
: super(
|
||||
StreamingMetrics(
|
||||
averageNetworkBitrate: 0,
|
||||
currentNetworkBitrate: 0,
|
||||
bufferDurationMs: 0,
|
||||
segmentDownloadTimeMs: 0,
|
||||
rebufferCount: 0,
|
||||
startTime: DateTime.now(),
|
||||
),
|
||||
) {
|
||||
_optimizer = StreamingOptimizer(qualitySelector: _qualitySelector);
|
||||
|
||||
// Listen to metrics updates
|
||||
|
||||
Reference in new issue
Block a user