mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-23 23:18:06 +02:00
feat: Add initial IPTV support with a dedicated player, series detail screen, Xtream API service, and a streaming API handler.
This commit is contained in:
1 parent
3f9a88c09a
commit
b9d6d715d7
4 files changed
+68
-15
No files matched your search
@@ -149,10 +149,11 @@ Handler createVodStreamHandler(Future<PlaylistConfig?> Function(Request) getPlay
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Build Upstream URL (Try MKV first, then generic)
|
// Build Upstream URL based on content type
|
||||||
// Note: We don't know the extension here easily unless passed.
|
// Check for type parameter in request URL (?type=series or ?type=movie)
|
||||||
// Xtream usually allows access via streamId.mkv or streamId.mp4 regardless of actual file
|
final contentType = request.url.queryParameters['type'] ?? 'movie';
|
||||||
// We'll trust the ID.
|
final basePath = contentType == 'series' ? 'series' : 'movie';
|
||||||
|
|
||||||
// Only start FFmpeg if not already running
|
// Only start FFmpeg if not already running
|
||||||
if (!_vodProcesses.containsKey(streamId)) {
|
if (!_vodProcesses.containsKey(streamId)) {
|
||||||
// Prepare directory (Only for new session)
|
// Prepare directory (Only for new session)
|
||||||
@@ -161,13 +162,23 @@ Handler createVodStreamHandler(Future<PlaylistConfig?> Function(Request) getPlay
|
|||||||
}
|
}
|
||||||
streamDir.createSync(recursive: true);
|
streamDir.createSync(recursive: true);
|
||||||
|
|
||||||
final targetUrl = '${playlist.dns}/movie/${playlist.username}/${playlist.password}/$streamId.mkv';
|
final targetUrl = '${playlist.dns}/$basePath/${playlist.username}/${playlist.password}/$streamId.mkv';
|
||||||
print('[VOD] Starting Transcode: $targetUrl');
|
print('[VOD] Starting Transcode ($contentType): $targetUrl');
|
||||||
|
|
||||||
// Start FFmpeg
|
// Start FFmpeg
|
||||||
final ffmpegArgs = [
|
final ffmpegArgs = [
|
||||||
'-hide_banner', '-loglevel', 'error',
|
'-hide_banner', '-loglevel', 'error',
|
||||||
'-headers', 'User-Agent: VLC/3.0.18 LibVLC/3.0.18\r\n',
|
'-headers', 'User-Agent: VLC/3.0.18 LibVLC/3.0.18\r\n',
|
||||||
|
|
||||||
|
// Robustness for network streams
|
||||||
|
'-reconnect', '1',
|
||||||
|
'-reconnect_at_eof', '1',
|
||||||
|
'-reconnect_streamed', '1',
|
||||||
|
'-reconnect_delay_max', '5',
|
||||||
|
'-rw_timeout', '15000000', // 15s timeout for read/write
|
||||||
|
'-analyzeduration', '10000000', // Increase probe duration
|
||||||
|
'-probesize', '10000000',
|
||||||
|
|
||||||
'-i', targetUrl,
|
'-i', targetUrl,
|
||||||
|
|
||||||
// Video: H.264 Ultrafast (Low CPU)
|
// Video: H.264 Ultrafast (Low CPU)
|
||||||
@@ -182,9 +193,14 @@ Handler createVodStreamHandler(Future<PlaylistConfig?> Function(Request) getPlay
|
|||||||
|
|
||||||
// Audio: AAC (Browser compatible, improved quality)
|
// Audio: AAC (Browser compatible, improved quality)
|
||||||
'-c:a', 'aac',
|
'-c:a', 'aac',
|
||||||
'-b:a', '192k', // Higher bitrate for better quality
|
'-b:a', '192k',
|
||||||
'-ar', '48000', // Standard sample rate
|
'-ar', '48000',
|
||||||
'-ac', '2',
|
|
||||||
|
// Advanced Audio Filtering:
|
||||||
|
// 1. "pan": Downmix 5.1/7.1 to Stereo with center mix (dialogue) boosted.
|
||||||
|
// 2. "dynaudnorm": Dynamic Audio Normalizer to boost quiet dialogue and limit loud explosions (Night Mode effect).
|
||||||
|
'-af', 'pan=stereo|FL=1.0*FL+0.707*FC+0.5*BL+0.5*SL+0.5*LFE|FR=1.0*FR+0.707*FC+0.5*BR+0.5*SR+0.5*LFE,dynaudnorm=f=150:g=15',
|
||||||
|
|
||||||
|
|
||||||
'-f', 'hls',
|
'-f', 'hls',
|
||||||
'-hls_time', '4',
|
'-hls_time', '4',
|
||||||
@@ -215,10 +231,10 @@ Handler createVodStreamHandler(Future<PlaylistConfig?> Function(Request) getPlay
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for playlist to appear (max 10s)
|
// Wait for playlist to appear (max 30s)
|
||||||
final playlistFile = File('${streamDir.path}/playlist.m3u8');
|
final playlistFile = File('${streamDir.path}/playlist.m3u8');
|
||||||
int retries = 0;
|
int retries = 0;
|
||||||
while (!playlistFile.existsSync() && retries < 20) {
|
while (!playlistFile.existsSync() && retries < 60) {
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
retries++;
|
retries++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -345,6 +345,22 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Mute button for Live TV
|
||||||
|
Material(
|
||||||
|
color: Colors.black54,
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: _toggleMute,
|
||||||
|
customBorder: const CircleBorder(),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Icon(
|
||||||
|
_isMuted ? Icons.volume_off_rounded : Icons.volume_up_rounded,
|
||||||
|
color: Colors.white
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -616,6 +632,12 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
|||||||
],
|
],
|
||||||
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
// Volume/Mute button
|
||||||
|
_buildTvControl(
|
||||||
|
_isMuted ? Icons.volume_off_rounded : Icons.volume_up_rounded,
|
||||||
|
_toggleMute
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
_buildTvControl(Icons.subtitles_rounded, () {}),
|
_buildTvControl(Icons.subtitles_rounded, () {}),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
_buildTvControl(Icons.aspect_ratio_rounded, _toggleFullscreen),
|
_buildTvControl(Icons.aspect_ratio_rounded, _toggleFullscreen),
|
||||||
|
|||||||
@@ -267,10 +267,25 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
|
|||||||
color: Colors.grey.shade800,
|
color: Colors.grey.shade800,
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
// Mark as watched when playing
|
// Mark as watched when playing
|
||||||
ref.read(watchHistoryProvider.notifier).markEpisodeWatched(episodeKey);
|
ref.read(watchHistoryProvider.notifier).markEpisodeWatched(episodeKey);
|
||||||
|
|
||||||
|
// Fetch actual duration from API if not available
|
||||||
|
Duration? episodeDuration;
|
||||||
|
if (episode.durationSecs != null && episode.durationSecs! > 0) {
|
||||||
|
episodeDuration = Duration(seconds: episode.durationSecs!);
|
||||||
|
} else {
|
||||||
|
// Try to fetch from API using getVodDuration (episodes are often queryable as VODs)
|
||||||
|
final service = ref.read(xtreamServiceProvider(widget.playlist));
|
||||||
|
final durationSecs = await service.getVodDuration(episode.id);
|
||||||
|
if (durationSecs != null && durationSecs > 0) {
|
||||||
|
episodeDuration = Duration(seconds: durationSecs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -280,7 +295,7 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
|
|||||||
playlist: widget.playlist,
|
playlist: widget.playlist,
|
||||||
streamType: StreamType.series,
|
streamType: StreamType.series,
|
||||||
containerExtension: episode.containerExtension ?? 'mkv',
|
containerExtension: episode.containerExtension ?? 'mkv',
|
||||||
duration: episode.durationSecs != null ? Duration(seconds: episode.durationSecs!) : null,
|
duration: episodeDuration,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -79,9 +79,9 @@ class XtreamService {
|
|||||||
String getSeriesStreamUrl(String streamId, String containerExtension) {
|
String getSeriesStreamUrl(String streamId, String containerExtension) {
|
||||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||||
|
|
||||||
// Point to new dedicated VOD API (HLS playlist)
|
// Point to new dedicated VOD API (HLS playlist) with series type
|
||||||
final baseUrl = html.window.location.origin;
|
final baseUrl = html.window.location.origin;
|
||||||
return '$baseUrl/api/vod/$streamId/playlist.m3u8';
|
return '$baseUrl/api/vod/$streamId/playlist.m3u8?type=series';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Authenticate and get server info
|
/// Authenticate and get server info
|
||||||
|
|||||||
Reference in new issue
Block a user