feat: implement IPTV player screen for live, VOD, and series streams with integrated controls and state management.

This commit is contained in:
Michael committed 2025-12-14 18:47:00 +01:00
1 parent bffef49554
commit ee707d3cf9
5 files changed
+79 -104

No files matched your search

+28 -24
View File
@@ -147,28 +147,34 @@ Handler createStreamInitHandler() {
'-hls_segment_filename', '${tempDir.path}/segment_%03d.ts', '-hls_segment_filename', '${tempDir.path}/segment_%03d.ts',
]; ];
// Detect if this is likely a Live TV stream (TS format) vs VOD (MKV/MP4) // Video encoding: Always transcode for HLS timestamp compatibility
// Using ultrafast preset for Live TV to minimize latency
final isLiveStream = iptvUrl.endsWith('.ts') || iptvUrl.contains('/live/'); final isLiveStream = iptvUrl.endsWith('.ts') || iptvUrl.contains('/live/');
// Video encoding strategy:
// - Live TV (.ts): Try to COPY video stream for maximum quality (no re-encoding loss)
// - VOD (MKV/MP4): Transcode to ensure timestamp compatibility
if (isLiveStream) { if (isLiveStream) {
// LIVE TV: Copy video stream for best quality // LIVE TV: Fast transcoding with ultrafast preset
// Most IPTV live streams are already H.264 and browser-compatible
ffmpegArgs.addAll([
'-c:v', 'copy', // No re-encoding = best quality
]);
} else {
// VOD: Transcode to fix timestamp issues in MKV containers
ffmpegArgs.addAll([ ffmpegArgs.addAll([
'-c:v', 'libx264', '-c:v', 'libx264',
'-preset', 'fast', // Better quality than ultrafast '-preset', 'ultrafast',
'-tune', 'film', // Better for movies/series '-tune', 'zerolatency',
'-profile:v', 'high', // High profile for better compression '-profile:v', 'main', // Main profile for better quality than baseline
'-level', '4.1', // Supports 1080p60 '-level', '4.0',
'-pix_fmt', 'yuv420p', '-pix_fmt', 'yuv420p',
'-g', '48', // 2s GOP '-g', '48', // 2s GOP at 24fps
'-b:v', '4000k', // 4Mbps for good quality live
'-maxrate', '4500k',
'-bufsize', '8000k',
]);
} else {
// VOD: Higher quality transcoding
ffmpegArgs.addAll([
'-c:v', 'libx264',
'-preset', 'fast',
'-tune', 'film',
'-profile:v', 'high',
'-level', '4.1',
'-pix_fmt', 'yuv420p',
'-g', '48',
]); ]);
if (quality == 'low') { if (quality == 'low') {
@@ -179,26 +185,24 @@ Handler createStreamInitHandler() {
'-bufsize', '3000k', '-bufsize', '3000k',
]); ]);
} else { } else {
// High quality (default) - 6Mbps for 1080p
ffmpegArgs.addAll([ ffmpegArgs.addAll([
'-b:v', '6000k', '-b:v', '6000k',
'-maxrate', '8000k', '-maxrate', '8000k',
'-bufsize', '12000k', '-bufsize', '12000k',
'-crf', '18', // High quality CRF '-crf', '18',
]); ]);
} }
} }
// Audio: Always transcode to AAC (browsers don't support AC3/DTS) // Audio: Always transcode to AAC
// Higher bitrate for better audio quality
ffmpegArgs.addAll([ ffmpegArgs.addAll([
'-c:a', 'aac', '-c:a', 'aac',
'-b:a', '192k', // Higher audio bitrate '-b:a', '192k',
'-ar', '48000', // Standard sample rate '-ar', '48000',
'-ac', '2', '-ac', '2',
playlistPath,
]); ]);
ffmpegArgs.add(playlistPath);
// Start FFmpeg process // Start FFmpeg process
final process = await Process.start('ffmpeg', ffmpegArgs); final process = await Process.start('ffmpeg', ffmpegArgs);
+14 -6
View File
@@ -120,9 +120,14 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
final encodedUrl = Uri.encodeComponent(streamUrl); final encodedUrl = Uri.encodeComponent(streamUrl);
final streamTypeParam = widget.streamType == StreamType.live ? 'live' : 'vod'; final streamTypeParam = widget.streamType == StreamType.live ? 'live' : 'vod';
var playerSrc = playerSettings.playerType == PlayerType.lite
? 'player_lite.html?url=$encodedUrl&type=$streamTypeParam' // Force player choice based on stream type:
: 'player.html?url=$encodedUrl'; // - Live TV: Player Lite (simple TS playback with mpegts.js)
// - VOD/Series: Player Standard (fuller controls, HLS support)
final isLiveTV = widget.streamType == StreamType.live;
var playerSrc = isLiveTV
? 'player_lite.html?url=$encodedUrl&type=live'
: 'player.html?url=$encodedUrl&type=vod';
if (startTimeOverride != null) { if (startTimeOverride != null) {
playerSrc += '&t=$startTimeOverride'; playerSrc += '&t=$startTimeOverride';
@@ -271,10 +276,13 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final settings = ref.watch(iptvSettingsProvider); // Use stream type to determine player UI:
// - Live TV: Lite player UI (minimal overlay)
// - VOD/Series: Standard player UI (full controls)
final isLiveTV = widget.streamType == StreamType.live;
// LITE PLAYER MODE: Simple iframe with native HTML5 controls, minimal Flutter overlay // LITE PLAYER MODE for Live TV: Simple iframe with native HTML5 controls, minimal Flutter overlay
if (settings.playerType == PlayerType.lite) { if (isLiveTV) {
return Scaffold( return Scaffold(
backgroundColor: Colors.black, backgroundColor: Colors.black,
body: MouseRegion( body: MouseRegion(
@@ -57,20 +57,18 @@ class XtreamService {
_currentPlaylist = playlist; _currentPlaylist = playlist;
} }
/// Generate stream URL for live TV via FFmpeg transcoding /// Generate stream URL for live TV
/// ///
/// Returns the start URL for FFmpeg transcoding endpoint. /// Returns the proxy URL for direct MPEG-TS streaming.
/// The endpoint will start FFmpeg and return the local HLS URL. /// Player will use mpegts.js for playback (no server-side transcoding).
String getLiveStreamUrl(String streamId) { String getLiveStreamUrl(String streamId) {
if (_currentPlaylist == null) throw Exception('No playlist configured'); if (_currentPlaylist == null) throw Exception('No playlist configured');
// Use .ts format for direct MPEG-TS streaming (FFmpeg handles this better) // Direct .ts URL for MPEG-TS streaming
final iptvUrl = '${_currentPlaylist!.dns}/live/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.ts'; final iptvUrl = '${_currentPlaylist!.dns}/live/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.ts';
// Use FFmpeg transcoding endpoint // Use proxy for direct streaming (no FFmpeg transcoding)
final baseUrl = html.window.location.origin; return _wrapWithProxy(iptvUrl);
final encodedUrl = Uri.encodeComponent(iptvUrl);
return '$baseUrl/api/stream/$streamId?url=$encodedUrl';
} }
/// Generate stream URL for VOD (movies) /// Generate stream URL for VOD (movies)
@@ -34,26 +34,9 @@ class StreamingSettingsTab extends ConsumerWidget {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// Player Type Section // Note: Player type is now automatically determined:
_buildSectionCard( // - Live TV uses Player Lite (MPEG-TS)
context: context, // - Movies/Series use Player Standard
title: 'Lecteur Vidéo',
icon: Icons.play_circle_outline,
children: [
_buildDropdownTile<PlayerType>(
title: 'Type de lecteur',
subtitle: 'Changer si vous rencontrez des problèmes de lecture',
value: settings.playerType,
items: PlayerType.values,
onChanged: (v) => notifier.setPlayerType(v!),
labelBuilder: (v) => switch (v) {
PlayerType.standard => 'Standard (Avancé)',
PlayerType.lite => 'Lite (Compatibilité max)',
},
),
],
),
const SizedBox(height: 16),
// Quality Section // Quality Section
_buildSectionCard( _buildSectionCard(
+28 -46
View File
@@ -361,59 +361,41 @@
log('Stream type: ' + streamType); log('Stream type: ' + streamType);
const lowerUrl = streamUrl.toLowerCase(); const lowerUrl = streamUrl.toLowerCase();
// SPECIAL: /api/stream/ endpoint handling - ALWAYS returns JSON with HLS playlist // LIVE TV: Simple MPEG-TS playback with mpegts.js
if (streamUrl.includes('/api/stream/')) { if (streamType === 'live') {
// Both Live TV and VOD use /api/stream/ which returns JSON with HLS playlist URL log('Live TV mode: Using MPEG-TS direct playback');
log((streamType === 'live' ? 'Live TV' : 'VOD') + ' via /api/stream/ - fetching HLS...'); showLoading('Connecting to live stream...');
showLoading(streamType === 'live' ? 'Connecting to live stream...' : 'Initializing stream...');
try { // Check for HLS first (.m3u8)
const response = await fetch(streamUrl);
const data = await response.json();
if (data.playlist) {
const playlistUrl = window.location.origin + data.playlist;
log('Got HLS playlist: ' + playlistUrl);
showLoading('Loading HLS stream...');
await playWithHLS(playlistUrl);
return;
} else {
showError('Invalid stream response');
return;
}
} catch (err) {
log('Error fetching playlist: ' + err);
showError('Failed to initialize stream');
return;
}
}
// Use type parameter if available, otherwise detect from URL
if (streamType === 'vod') {
// VOD content - use direct HTML5 playback
log('VOD mode: Using direct playback');
await playDirect(streamUrl);
} else if (streamType === 'live') {
// Live TV - check for HLS first, then MPEG-TS
if (lowerUrl.includes('.m3u8') || lowerUrl.includes('/hls/')) { if (lowerUrl.includes('.m3u8') || lowerUrl.includes('/hls/')) {
log('Live HLS detected'); log('Live HLS detected');
await playWithHLS(streamUrl); await playWithHLS(streamUrl);
} else { } else {
log('Live MPEG-TS mode'); // Default: MPEG-TS with mpegts.js
await playWithMpegTS(streamUrl); log('Using mpegts.js for MPEG-TS');
await playWithMpegTS(streamUrl, true); // isLive=true
} }
return;
}
// VOD: Direct HTML5 playback with proxy
if (streamType === 'vod') {
log('VOD mode: Using direct playback');
showLoading('Loading video...');
await playDirect(streamUrl);
return;
}
// Fallback: detect from URL pattern
if (lowerUrl.includes('.m3u8') || lowerUrl.includes('/hls/')) {
await playWithHLS(streamUrl);
} else if (lowerUrl.includes('.mp4') || lowerUrl.includes('.mkv') ||
lowerUrl.includes('/movie/') || lowerUrl.includes('/series/')) {
await playDirect(streamUrl);
} else if (lowerUrl.includes('.ts') || lowerUrl.includes('/live/')) {
await playWithMpegTS(streamUrl, true);
} else { } else {
// Fallback: detect from URL pattern await playDirect(streamUrl);
if (lowerUrl.includes('.m3u8') || lowerUrl.includes('/hls/')) {
await playWithHLS(streamUrl);
} else if (lowerUrl.includes('.mp4') || lowerUrl.includes('.mkv') ||
lowerUrl.includes('/movie/') || lowerUrl.includes('/series/')) {
await playDirect(streamUrl);
} else if (lowerUrl.includes('.ts') || lowerUrl.includes('/live/')) {
await playWithMpegTS(streamUrl);
} else {
await playDirect(streamUrl);
}
} }
} }