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',
];
// 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/');
// 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) {
// LIVE TV: Copy video stream for best quality
// 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
// LIVE TV: Fast transcoding with ultrafast preset
ffmpegArgs.addAll([
'-c:v', 'libx264',
'-preset', 'fast', // Better quality than ultrafast
'-tune', 'film', // Better for movies/series
'-profile:v', 'high', // High profile for better compression
'-level', '4.1', // Supports 1080p60
'-preset', 'ultrafast',
'-tune', 'zerolatency',
'-profile:v', 'main', // Main profile for better quality than baseline
'-level', '4.0',
'-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') {
@@ -179,26 +185,24 @@ Handler createStreamInitHandler() {
'-bufsize', '3000k',
]);
} else {
// High quality (default) - 6Mbps for 1080p
ffmpegArgs.addAll([
'-b:v', '6000k',
'-maxrate', '8000k',
'-bufsize', '12000k',
'-crf', '18', // High quality CRF
'-crf', '18',
]);
}
}
// Audio: Always transcode to AAC (browsers don't support AC3/DTS)
// Higher bitrate for better audio quality
// Audio: Always transcode to AAC
ffmpegArgs.addAll([
'-c:a', 'aac',
'-b:a', '192k', // Higher audio bitrate
'-ar', '48000', // Standard sample rate
'-b:a', '192k',
'-ar', '48000',
'-ac', '2',
playlistPath,
]);
ffmpegArgs.add(playlistPath);
// Start FFmpeg process
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 streamTypeParam = widget.streamType == StreamType.live ? 'live' : 'vod';
var playerSrc = playerSettings.playerType == PlayerType.lite
? 'player_lite.html?url=$encodedUrl&type=$streamTypeParam'
: 'player.html?url=$encodedUrl';
// Force player choice based on stream type:
// - 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) {
playerSrc += '&t=$startTimeOverride';
@@ -271,10 +276,13 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
@override
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
if (settings.playerType == PlayerType.lite) {
// LITE PLAYER MODE for Live TV: Simple iframe with native HTML5 controls, minimal Flutter overlay
if (isLiveTV) {
return Scaffold(
backgroundColor: Colors.black,
body: MouseRegion(
@@ -57,20 +57,18 @@ class XtreamService {
_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.
/// The endpoint will start FFmpeg and return the local HLS URL.
/// Returns the proxy URL for direct MPEG-TS streaming.
/// Player will use mpegts.js for playback (no server-side transcoding).
String getLiveStreamUrl(String streamId) {
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';
// Use FFmpeg transcoding endpoint
final baseUrl = html.window.location.origin;
final encodedUrl = Uri.encodeComponent(iptvUrl);
return '$baseUrl/api/stream/$streamId?url=$encodedUrl';
// Use proxy for direct streaming (no FFmpeg transcoding)
return _wrapWithProxy(iptvUrl);
}
/// Generate stream URL for VOD (movies)
@@ -34,26 +34,9 @@ class StreamingSettingsTab extends ConsumerWidget {
),
const SizedBox(height: 24),
// Player Type Section
_buildSectionCard(
context: context,
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),
// Note: Player type is now automatically determined:
// - Live TV uses Player Lite (MPEG-TS)
// - Movies/Series use Player Standard
// Quality Section
_buildSectionCard(
+28 -46
View File
@@ -361,59 +361,41 @@
log('Stream type: ' + streamType);
const lowerUrl = streamUrl.toLowerCase();
// SPECIAL: /api/stream/ endpoint handling - ALWAYS returns JSON with HLS playlist
if (streamUrl.includes('/api/stream/')) {
// Both Live TV and VOD use /api/stream/ which returns JSON with HLS playlist URL
log((streamType === 'live' ? 'Live TV' : 'VOD') + ' via /api/stream/ - fetching HLS...');
showLoading(streamType === 'live' ? 'Connecting to live stream...' : 'Initializing stream...');
// LIVE TV: Simple MPEG-TS playback with mpegts.js
if (streamType === 'live') {
log('Live TV mode: Using MPEG-TS direct playback');
showLoading('Connecting to live stream...');
try {
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
// Check for HLS first (.m3u8)
if (lowerUrl.includes('.m3u8') || lowerUrl.includes('/hls/')) {
log('Live HLS detected');
await playWithHLS(streamUrl);
} else {
log('Live MPEG-TS mode');
await playWithMpegTS(streamUrl);
// Default: MPEG-TS with mpegts.js
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 {
// 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);
} else {
await playDirect(streamUrl);
}
await playDirect(streamUrl);
}
}