mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +02:00
Security: - Replace unsalted SHA-256 password hashing with bcrypt (lazy rehash on login) - Add authenticated /api/xtream-api gateway: Xtream credentials are injected server-side and never sent to the frontend; /api/playlists no longer returns passwords - Redact credentials from all logs (login body, proxy/FFmpeg/scheduler URLs) - Add auth to recordings, EPG, season-passes and streaming routes (HttpOnly session cookie for hls.js; loopback bypass for local FFmpeg) - Lock player postMessage to same-origin in both directions - Vendor and pin hls.js 1.6.7 / mpegts.js 1.7.3 (drop CDN @latest) - Fix rate limiter (client IP was never resolved), add login rate limit, restrict CORS, add CSP Report-Only, block private-IP SSRF targets, fix path traversal in recording log retrieval, chmod 777 -> 770 - Remove dead HiveService (seeded admin/admin into IndexedDB with SHA-256) - Fix authMiddleware not populating 'user' context (getPlaylist ignored the logged-in user; admin purge always returned 403) Streaming: - New FfmpegSessionManager: process registry, idle reaper (4 min live / 15 min VOD), orphan cleanup at startup, clean SIGTERM shutdown, fast-fail with stderr instead of 30 s timeout - Quality selection (source/high/medium/low) for live and VOD; source mode streams with -c:v copy (zero transcoding); selector wired into the player - Concurrent recordings (MAX_CONCURRENT_RECORDINGS, default 2); conflicts retry on the next tick instead of silently failing - Lower live latency (HLS window 20 -> 10 segments, liveSync 10 -> 3) - Fix recording log lookup (.mp4 vs .mkv mismatch) Design: - Replace hardcoded colors with AppColors tokens (12 files) - web/theme.css syncs HTML players with the Flutter palette - DPAD/keyboard navigation (arrow-key focus, player shortcuts) - Tooltips on player icon buttons, Semantics on content cards - Remove 7 dead widgets broken since the Stitch merge Quality: - bin/test/: 21 unit tests (bcrypt, redaction, traversal, SSRF, recording conflicts) plus a quality-selector widget test - GitHub Actions CI (analyze + test + build web) - Archive stale status docs into docs/archive/ Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
558 lines
15 KiB
Markdown
558 lines
15 KiB
Markdown
/// INTEGRATION GUIDE - XtremFlow Optimizations
|
|
///
|
|
/// This guide shows how to integrate and use all the new optimization features
|
|
|
|
// ============================================
|
|
// 1. ADAPTIVE BITRATE STREAMING
|
|
// ============================================
|
|
|
|
// In your player_screen.dart, use this:
|
|
/*
|
|
import 'package:xtremflow/core/services/adaptive_bitrate_service.dart';
|
|
|
|
class PlayerScreen extends ConsumerStatefulWidget {
|
|
@override
|
|
ConsumerState<PlayerScreen> createState() => _PlayerScreenState();
|
|
}
|
|
|
|
class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
|
late QualitySelector _qualitySelector;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_qualitySelector = QualitySelector(
|
|
initialQuality: QualityProfiles.hd720p,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final currentQuality = _qualitySelector.currentQuality;
|
|
|
|
return Stack(
|
|
children: [
|
|
// Your video player widget
|
|
VideoPlayer(url: _getStreamUrl(currentQuality)),
|
|
|
|
// Quality indicator
|
|
QualityIndicator(
|
|
qualitySelector: _qualitySelector,
|
|
onTap: () {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => QualitySelectorWidget(
|
|
qualitySelector: _qualitySelector,
|
|
onClose: () => Navigator.pop(context),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
String _getStreamUrl(QualityLevel quality) {
|
|
// Return URL based on selected quality
|
|
// Example: https://stream.example.com/video_${quality.width}x${quality.height}.m3u8
|
|
return '';
|
|
}
|
|
}
|
|
*/
|
|
|
|
// ============================================
|
|
// 2. SUBTITLES SUPPORT
|
|
// ============================================
|
|
|
|
// Import and use:
|
|
/*
|
|
import 'package:xtremflow/features/iptv/services/subtitle_service.dart';
|
|
|
|
// Parse SRT file
|
|
final srtContent = await rootBundle.loadString('subtitles/movie.srt');
|
|
final subtitleEntries = SubtitleService.parseSrt(srtContent);
|
|
|
|
// Get subtitle at specific time
|
|
final currentSubtitle = SubtitleService.getSubtitleAtTime(
|
|
subtitleEntries,
|
|
Duration(seconds: currentPosition),
|
|
);
|
|
|
|
// Download from URL
|
|
final content = await SubtitleService.downloadSubtitle(
|
|
'https://example.com/subtitles.srt',
|
|
);
|
|
final entries = SubtitleService.parseSrt(content);
|
|
|
|
// Display in player
|
|
SubtitleOverlay(
|
|
subtitle: currentSubtitle,
|
|
position: Offset(0, size.height * 0.85),
|
|
)
|
|
*/
|
|
|
|
// ============================================
|
|
// 3. RECOMMEND SYSTEM (CONTINUE WATCHING, TRENDING)
|
|
// ============================================
|
|
|
|
// In your dashboard_screen.dart:
|
|
/*
|
|
import 'package:xtremflow/features/iptv/providers/recommendations_provider.dart';
|
|
import 'package:xtremflow/features/iptv/widgets/continue_watching_widget.dart';
|
|
|
|
class DashboardScreen extends ConsumerStatefulWidget {
|
|
@override
|
|
ConsumerState<DashboardScreen> createState() => _DashboardScreenState();
|
|
}
|
|
|
|
class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final watchHistory = ref.watch(watchHistoryProvider);
|
|
final trending = ref.watch(trendingProvider);
|
|
|
|
return ListView(
|
|
children: [
|
|
// Continue Watching Section
|
|
ContinueWatchingWidget(
|
|
playlist: widget.playlist,
|
|
content: widget.content,
|
|
onItemTap: () {
|
|
// Navigate to player
|
|
},
|
|
),
|
|
|
|
// Trending Section
|
|
TrendingWidget(
|
|
playlist: widget.playlist,
|
|
content: widget.content,
|
|
onItemTap: () {
|
|
// Navigate to player
|
|
},
|
|
),
|
|
|
|
// Recently Added Section
|
|
RecentlyAddedWidget(
|
|
playlist: widget.playlist,
|
|
content: widget.content,
|
|
onItemTap: () {
|
|
// Navigate to player
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
// Track watch position:
|
|
void _onVideoProgress(Duration position, Duration duration) {
|
|
final percentage = (position.inSeconds / duration.inSeconds) * 100;
|
|
ref.read(watchHistoryProvider.notifier)
|
|
.updateWatchTime(streamId, percentage);
|
|
}
|
|
|
|
// Track trending:
|
|
void _onVideoStart() {
|
|
ref.read(trendingProvider.notifier).incrementViewCount(streamId);
|
|
}
|
|
*/
|
|
|
|
// ============================================
|
|
// 4. OFFLINE DOWNLOADS
|
|
// ============================================
|
|
|
|
// Download a video:
|
|
/*
|
|
import 'package:xtremflow/features/iptv/services/download_service.dart';
|
|
|
|
final downloadService = ref.read(downloadServiceProvider);
|
|
|
|
// Start download
|
|
final task = await downloadService.startDownload(
|
|
id: 'movie_12345',
|
|
title: 'Awesome Movie',
|
|
url: 'https://stream.example.com/movie.mp4',
|
|
);
|
|
|
|
// Monitor progress
|
|
_downloadUpdateTimer = Timer.periodic(Duration(milliseconds: 200), (timer) {
|
|
final progress = downloadService.getDownloadProgress('movie_12345');
|
|
setState(() => _downloadProgress = progress);
|
|
});
|
|
|
|
// Pause/Resume
|
|
downloadService.pauseDownload('movie_12345');
|
|
downloadService.resumeDownload('movie_12345');
|
|
|
|
// Check if available offline
|
|
if (downloadService.isAvailableOffline('movie_12345')) {
|
|
final filePath = downloadService.getOfflineFilePath('movie_12345');
|
|
// Play from file path
|
|
}
|
|
*/
|
|
|
|
// ============================================
|
|
// 5. NETWORK OPTIMIZATION & PROXY
|
|
// ============================================
|
|
|
|
// Configure network with proxy:
|
|
/*
|
|
import 'package:xtremflow/core/services/network_service.dart';
|
|
|
|
final networkConfig = NetworkConfig(
|
|
proxyUrl: 'http://proxy.example.com:8080',
|
|
userAgent: 'CustomUser-Agent/1.0',
|
|
customHeaders: {
|
|
'Authorization': 'Bearer token123',
|
|
'X-Custom-Header': 'value',
|
|
},
|
|
connectTimeout: Duration(seconds: 30),
|
|
receiveTimeout: Duration(seconds: 60),
|
|
);
|
|
|
|
final networkService = OptimizedNetworkService(config: networkConfig);
|
|
|
|
// Update proxy at runtime
|
|
ref.read(networkConfigProvider.notifier)
|
|
.setProxy('http://new-proxy:8080');
|
|
*/
|
|
|
|
// ============================================
|
|
// 6. CACHE MANAGEMENT
|
|
// ============================================
|
|
|
|
// Use cache service:
|
|
/*
|
|
import 'package:xtremflow/core/services/cache_service.dart';
|
|
|
|
final cacheService = ref.read(cacheServiceProvider);
|
|
|
|
// Cache data with automatic size management
|
|
cacheService.cache<String>('channel_list_key', jsonEncodedList,
|
|
estimatedSize: 50000);
|
|
|
|
// Retrieve cached data
|
|
final cachedList = cacheService.get<String>('channel_list_key');
|
|
|
|
// Check if valid
|
|
if (cacheService.has('channel_list_key')) {
|
|
// Use cached value
|
|
}
|
|
|
|
// Get cache statistics
|
|
final stats = cacheService.getStats();
|
|
print('Cache size: ${stats['totalSizeMb']}MB');
|
|
print('Items: ${stats['itemCount']}');
|
|
|
|
// Clear expired entries
|
|
cacheService.clearExpired();
|
|
|
|
// Clear all cache
|
|
cacheService.clear();
|
|
*/
|
|
|
|
// ============================================
|
|
// 7. STREAMING METRICS & OPTIMIZATION
|
|
// ============================================
|
|
|
|
// Monitor streaming quality:
|
|
/*
|
|
import 'package:xtremflow/core/services/streaming_optimizer.dart';
|
|
|
|
final optimizer = ref.watch(streamingOptimizerProvider);
|
|
|
|
Text('Quality: ${optimizer.quality.label}'),
|
|
Text('Buffer: ${optimizer.bufferDurationMs}ms'),
|
|
Text('Rebuffers: ${optimizer.rebufferCount}'),
|
|
|
|
// Record segment download
|
|
ref.read(streamingOptimizerProvider.notifier)
|
|
.recordSegmentDownload(bytesDownloaded, downloadDuration);
|
|
|
|
// Handle rebuffer
|
|
ref.read(streamingOptimizerProvider.notifier)
|
|
.rebufferDetected(Duration(milliseconds: 2000));
|
|
|
|
// View metrics history
|
|
final history = ref.read(streamingOptimizerProvider.notifier)
|
|
.getHistory();
|
|
*/
|
|
|
|
// ============================================
|
|
// 8. EPG GRID VIEW
|
|
// ============================================
|
|
|
|
// Navigate to EPG:
|
|
/*
|
|
import 'package:xtremflow/features/iptv/screens/epg_grid_screen.dart';
|
|
|
|
// In your navigation:
|
|
context.push('/epg', extra: widget.playlist);
|
|
|
|
// Or open as bottom sheet
|
|
showModalBottomSheet(
|
|
context: context,
|
|
builder: (context) => EpgGridScreen(playlist: widget.playlist),
|
|
);
|
|
*/
|
|
|
|
// ============================================
|
|
// 9. OPTIMIZATION CONFIG
|
|
// ============================================
|
|
|
|
// Print optimization summary:
|
|
/*
|
|
import 'package:xtremflow/core/config/optimization_config.dart';
|
|
|
|
// On app startup
|
|
OptimizationConfig.printSummary();
|
|
|
|
// Calibrate for device
|
|
RuntimeOptimizations.calibrateForDevice(
|
|
totalMemoryMb: 8000,
|
|
freeMemoryMb: 2000,
|
|
storageFreeMb: 50000,
|
|
);
|
|
|
|
// Get dynamic cache sizes based on device
|
|
final cacheSizeMb = RuntimeOptimizations.getDynamicCacheSize();
|
|
final imageCacheMb = RuntimeOptimizations.getDynamicImageCacheSize();
|
|
|
|
// Check device capabilities
|
|
if (RuntimeOptimizations.hasHighPerformanceDevice) {
|
|
// Enable all features
|
|
} else if (RuntimeOptimizations.isLowMemoryMode) {
|
|
// Disable heavy features
|
|
}
|
|
*/
|
|
|
|
// ============================================
|
|
// 10. COMPLETE PLAYER INTEGRATION
|
|
// ============================================
|
|
|
|
// Full player screen with all optimizations:
|
|
/*
|
|
import 'package:xtremflow/core/services/adaptive_bitrate_service.dart';
|
|
import 'package:xtremflow/core/services/streaming_optimizer.dart';
|
|
import 'package:xtremflow/features/iptv/services/subtitle_service.dart';
|
|
|
|
class OptimizedPlayerScreen extends ConsumerStatefulWidget {
|
|
final String streamUrl;
|
|
final int streamId;
|
|
|
|
const OptimizedPlayerScreen({
|
|
required this.streamUrl,
|
|
required this.streamId,
|
|
});
|
|
|
|
@override
|
|
ConsumerState<OptimizedPlayerScreen> createState() =>
|
|
_OptimizedPlayerScreenState();
|
|
}
|
|
|
|
class _OptimizedPlayerScreenState
|
|
extends ConsumerState<OptimizedPlayerScreen> {
|
|
late QualitySelector _qualitySelector;
|
|
late VideoPlayerController _controller;
|
|
List<SubtitleEntry> _subtitles = [];
|
|
String? _currentSubtitle;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_initializePlayer();
|
|
}
|
|
|
|
void _initializePlayer() {
|
|
// Setup quality selector
|
|
_qualitySelector = QualitySelector(
|
|
initialQuality: QualityProfiles.hd720p,
|
|
);
|
|
|
|
// Initialize video player with adaptive quality URL
|
|
_controller = VideoPlayerController.network(
|
|
_getQualityUrl(_qualitySelector.currentQuality),
|
|
);
|
|
|
|
// Load subtitles
|
|
_loadSubtitles();
|
|
|
|
// Start listening to playback events
|
|
_controller.addListener(_onPlayerStateChanged);
|
|
}
|
|
|
|
void _loadSubtitles() async {
|
|
// Try to auto-download subtitles
|
|
// Example: from OpenSubtitles API
|
|
}
|
|
|
|
void _onPlayerStateChanged() {
|
|
final position = _controller.value.position;
|
|
|
|
// Track watch progress
|
|
if (_controller.value.duration.inMilliseconds > 0) {
|
|
final percentage = (position.inMilliseconds /
|
|
_controller.value.duration.inMilliseconds) * 100;
|
|
ref.read(watchHistoryProvider.notifier)
|
|
.updateWatchTime(widget.streamId, percentage);
|
|
}
|
|
|
|
// Update subtitle
|
|
if (_subtitles.isNotEmpty) {
|
|
_currentSubtitle = SubtitleService.getSubtitleAtTime(
|
|
_subtitles,
|
|
position,
|
|
);
|
|
setState(() {});
|
|
}
|
|
|
|
// Record streaming metrics
|
|
final bytesEstimate = 1000; // Calculate from actual download
|
|
ref.read(streamingOptimizerProvider.notifier)
|
|
.recordSegmentDownload(bytesEstimate, Duration(milliseconds: 500));
|
|
}
|
|
|
|
String _getQualityUrl(QualityLevel quality) {
|
|
// Return HLS master playlist or variant playlist
|
|
return '${widget.streamUrl}/variant_${quality.width}x${quality.height}.m3u8';
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final metrics = ref.watch(streamingOptimizerProvider);
|
|
|
|
return Stack(
|
|
children: [
|
|
// Video player
|
|
AspectRatio(
|
|
aspectRatio: _controller.value.aspectRatio,
|
|
child: VideoPlayer(_controller),
|
|
),
|
|
|
|
// Subtitles overlay
|
|
if (_currentSubtitle != null)
|
|
Positioned(
|
|
bottom: 60,
|
|
left: 0,
|
|
right: 0,
|
|
child: Center(
|
|
child: Container(
|
|
padding: EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.black54,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Text(
|
|
_currentSubtitle!,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Quality indicator + metrics
|
|
Positioned(
|
|
top: 16,
|
|
right: 16,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
QualityIndicator(
|
|
qualitySelector: _qualitySelector,
|
|
onTap: () {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => QualitySelectorWidget(
|
|
qualitySelector: _qualitySelector,
|
|
onClose: () => Navigator.pop(context),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
SizedBox(height: 8),
|
|
BandwidthMonitor(qualitySelector: _qualitySelector),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Metrics display (debug)
|
|
if (kDebugMode)
|
|
Positioned(
|
|
bottom: 16,
|
|
left: 16,
|
|
child: Container(
|
|
padding: EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.black54,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Rebuffers: ${metrics.rebufferCount}',
|
|
style: TextStyle(color: Colors.white, fontSize: 10),
|
|
),
|
|
Text(
|
|
'Buffer: ${metrics.bufferDurationMs}ms',
|
|
style: TextStyle(color: Colors.white, fontSize: 10),
|
|
),
|
|
Text(
|
|
'Quality: ${metrics.bufferDurationMs}/100',
|
|
style: TextStyle(color: Colors.white, fontSize: 10),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.removeListener(_onPlayerStateChanged);
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|
|
*/
|
|
|
|
// ============================================
|
|
// BEST PRACTICES
|
|
// ============================================
|
|
|
|
/*
|
|
1. ALWAYS use ref.watch() for providers in build()
|
|
❌ Bad: final service = ServicesService()
|
|
✅ Good: final service = ref.read(serviceProvider)
|
|
|
|
2. DISPOSE resources properly
|
|
❌ Bad: Forget to cancel timers
|
|
✅ Good: Override dispose() and clean up
|
|
|
|
3. HANDLE network errors gracefully
|
|
❌ Bad: Assume network always works
|
|
✅ Good: Use try-catch with retry logic
|
|
|
|
4. OPTIMIZE images
|
|
❌ Bad: Load full resolution images
|
|
✅ Good: Use cached_network_image with cache manager
|
|
|
|
5. MONITOR memory usage
|
|
❌ Bad: Keep unlimited cache
|
|
✅ Good: Use cache service with auto-eviction
|
|
|
|
6. TEST on low-end devices
|
|
❌ Bad: Only test on flagship phones
|
|
✅ Good: Test on Android 6.0 with 1GB RAM
|
|
|
|
7. MEASURE performance
|
|
❌ Bad: "It feels fast"
|
|
✅ Good: Use metrics to measure and improve
|
|
*/
|