mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +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
d1401711af
commit
d6405f3471
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)
|
||||
// Note: We don't know the extension here easily unless passed.
|
||||
// Xtream usually allows access via streamId.mkv or streamId.mp4 regardless of actual file
|
||||
// We'll trust the ID.
|
||||
// Build Upstream URL based on content type
|
||||
// Check for type parameter in request URL (?type=series or ?type=movie)
|
||||
final contentType = request.url.queryParameters['type'] ?? 'movie';
|
||||
final basePath = contentType == 'series' ? 'series' : 'movie';
|
||||
|
||||
// Only start FFmpeg if not already running
|
||||
if (!_vodProcesses.containsKey(streamId)) {
|
||||
// Prepare directory (Only for new session)
|
||||
@@ -161,13 +162,23 @@ Handler createVodStreamHandler(Future<PlaylistConfig?> Function(Request) getPlay
|
||||
}
|
||||
streamDir.createSync(recursive: true);
|
||||
|
||||
final targetUrl = '${playlist.dns}/movie/${playlist.username}/${playlist.password}/$streamId.mkv';
|
||||
print('[VOD] Starting Transcode: $targetUrl');
|
||||
final targetUrl = '${playlist.dns}/$basePath/${playlist.username}/${playlist.password}/$streamId.mkv';
|
||||
print('[VOD] Starting Transcode ($contentType): $targetUrl');
|
||||
|
||||
// Start FFmpeg
|
||||
final ffmpegArgs = [
|
||||
'-hide_banner', '-loglevel', 'error',
|
||||
'-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,
|
||||
|
||||
// Video: H.264 Ultrafast (Low CPU)
|
||||
@@ -182,9 +193,14 @@ Handler createVodStreamHandler(Future<PlaylistConfig?> Function(Request) getPlay
|
||||
|
||||
// Audio: AAC (Browser compatible, improved quality)
|
||||
'-c:a', 'aac',
|
||||
'-b:a', '192k', // Higher bitrate for better quality
|
||||
'-ar', '48000', // Standard sample rate
|
||||
'-ac', '2',
|
||||
'-b:a', '192k',
|
||||
'-ar', '48000',
|
||||
|
||||
// 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',
|
||||
'-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');
|
||||
int retries = 0;
|
||||
while (!playlistFile.existsSync() && retries < 20) {
|
||||
while (!playlistFile.existsSync() && retries < 60) {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
retries++;
|
||||
}
|
||||
|
||||
@@ -345,6 +345,22 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
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(),
|
||||
// Volume/Mute button
|
||||
_buildTvControl(
|
||||
_isMuted ? Icons.volume_off_rounded : Icons.volume_up_rounded,
|
||||
_toggleMute
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
_buildTvControl(Icons.subtitles_rounded, () {}),
|
||||
const SizedBox(width: 16),
|
||||
_buildTvControl(Icons.aspect_ratio_rounded, _toggleFullscreen),
|
||||
|
||||
@@ -267,10 +267,25 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
|
||||
color: Colors.grey.shade800,
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
onTap: () async {
|
||||
// Mark as watched when playing
|
||||
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(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
@@ -280,7 +295,7 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
|
||||
playlist: widget.playlist,
|
||||
streamType: StreamType.series,
|
||||
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) {
|
||||
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;
|
||||
return '$baseUrl/api/vod/$streamId/playlist.m3u8';
|
||||
return '$baseUrl/api/vod/$streamId/playlist.m3u8?type=series';
|
||||
}
|
||||
|
||||
/// Authenticate and get server info
|
||||
|
||||
Reference in new issue
Block a user