mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-21 14:08:04 +02:00
feat: security hardening, streaming overhaul, design polish, tests
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>
This commit is contained in:
1 parent
cb5eca7547
commit
60d3f42901
107 files changed
+4525
-3815
No files matched your search
@@ -1,253 +1,107 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/services/adaptive_bitrate_service.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
|
||||
/// Widget for displaying and selecting video quality during playback
|
||||
class QualitySelectorWidget extends ConsumerStatefulWidget {
|
||||
final QualitySelector qualitySelector;
|
||||
final VoidCallback onClose;
|
||||
/// Stream quality presets handled server-side by FFmpeg
|
||||
/// (see bin/api/streaming_handler.dart).
|
||||
enum StreamQuality {
|
||||
source('source', 'Source', 'Flux direct, zéro transcodage'),
|
||||
high('high', 'Haute', '1080p · 6-8 Mbps'),
|
||||
medium('medium', 'Moyenne', '3 Mbps'),
|
||||
low('low', 'Basse', '720p · 1.5 Mbps');
|
||||
|
||||
const QualitySelectorWidget({
|
||||
super.key,
|
||||
required this.qualitySelector,
|
||||
required this.onClose,
|
||||
});
|
||||
const StreamQuality(this.value, this.label, this.description);
|
||||
|
||||
@override
|
||||
ConsumerState<QualitySelectorWidget> createState() =>
|
||||
_QualitySelectorWidgetState();
|
||||
final String value;
|
||||
final String label;
|
||||
final String description;
|
||||
}
|
||||
|
||||
class _QualitySelectorWidgetState extends ConsumerState<QualitySelectorWidget> {
|
||||
late List<QualityLevel> _availableQualities;
|
||||
late QualityLevel _selectedQuality;
|
||||
/// Popup button to switch the transcoding quality of the current stream.
|
||||
class QualitySelectorButton extends StatelessWidget {
|
||||
final StreamQuality current;
|
||||
final ValueChanged<StreamQuality> onSelected;
|
||||
final double size;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_availableQualities = QualityProfiles.getBalancedProfiles();
|
||||
_selectedQuality = widget.qualitySelector.currentQuality;
|
||||
}
|
||||
const QualitySelectorButton({
|
||||
super.key,
|
||||
required this.current,
|
||||
required this.onSelected,
|
||||
this.size = 44,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentQuality = widget.qualitySelector.currentQuality;
|
||||
final bandwidthMbps = widget.qualitySelector.getEstimatedBandwidthMbps();
|
||||
|
||||
return Dialog(
|
||||
backgroundColor: Colors.black87,
|
||||
insetPadding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Colors.white24),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Video Quality',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
onPressed: widget.onClose,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Network Info
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white10,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Current Bandwidth:',
|
||||
style: TextStyle(color: Colors.white70, fontSize: 14),
|
||||
),
|
||||
Text(
|
||||
'${bandwidthMbps.toStringAsFixed(1)} Mbps',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
return Tooltip(
|
||||
message: 'Qualité : ${current.label}',
|
||||
child: PopupMenuButton<StreamQuality>(
|
||||
initialValue: current,
|
||||
color: AppColors.surfaceContainerHigh,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
onSelected: onSelected,
|
||||
itemBuilder: (context) => StreamQuality.values
|
||||
.map(
|
||||
(q) => PopupMenuItem<StreamQuality>(
|
||||
value: q,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
q == current
|
||||
? Icons.radio_button_checked_rounded
|
||||
: Icons.radio_button_off_rounded,
|
||||
size: 18,
|
||||
color: q == current
|
||||
? AppColors.primary
|
||||
: AppColors.textTertiary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Quality Options
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: _availableQualities.length,
|
||||
itemBuilder: (context, index) {
|
||||
final quality = _availableQualities[index];
|
||||
final isSelected = quality == currentQuality;
|
||||
|
||||
return ListTile(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||
leading: isSelected
|
||||
? const Icon(Icons.check_circle, color: Colors.blue)
|
||||
: const Icon(
|
||||
Icons.radio_button_unchecked,
|
||||
color: Colors.white30,
|
||||
const SizedBox(width: 12),
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
q.label,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: GoogleFonts.inter(
|
||||
color: AppColors.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
q.description,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: GoogleFonts.inter(
|
||||
color: AppColors.textTertiary,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: Text(
|
||||
quality.label,
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.blue : Colors.white,
|
||||
fontWeight:
|
||||
isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Text(
|
||||
'${quality.bitrateBps ~/ 1000000} Mbps',
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 12),
|
||||
),
|
||||
onTap: () {
|
||||
widget.qualitySelector.setQuality(quality);
|
||||
setState(() {
|
||||
_selectedQuality = quality;
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
child: Container(
|
||||
width: size,
|
||||
height: size,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white.withOpacity(0.2)),
|
||||
),
|
||||
|
||||
// Auto Quality Option
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(color: Colors.white24),
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
leading: const Icon(Icons.auto_awesome, color: Colors.amber),
|
||||
title: const Text(
|
||||
'Auto Quality',
|
||||
style: TextStyle(
|
||||
color: Colors.amber,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
subtitle: const Text(
|
||||
'Adjusts based on network speed',
|
||||
style: TextStyle(color: Colors.white70, fontSize: 12),
|
||||
),
|
||||
onTap: () {
|
||||
// Auto mode would be handled by the player
|
||||
widget.onClose();
|
||||
},
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.high_quality_rounded,
|
||||
color: Colors.white,
|
||||
size: 22,
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Floating quality indicator (minimal UI during playback)
|
||||
class QualityIndicator extends ConsumerWidget {
|
||||
final QualitySelector qualitySelector;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const QualityIndicator({
|
||||
super.key,
|
||||
required this.qualitySelector,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final quality = qualitySelector.currentQuality;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.high_quality, color: Colors.white, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
quality.resolution.split('x')[1], // Show just the height
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Bandwidth monitor widget (for debugging/info)
|
||||
class BandwidthMonitor extends ConsumerWidget {
|
||||
final QualitySelector qualitySelector;
|
||||
|
||||
const BandwidthMonitor({
|
||||
super.key,
|
||||
required this.qualitySelector,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.speed, color: Colors.green, size: 14),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${qualitySelector.getEstimatedBandwidthMbps().toStringAsFixed(1)} Mbps',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user