diff --git a/bin/api/streaming_handler.dart b/bin/api/streaming_handler.dart index 8c20926..93c42a7 100644 --- a/bin/api/streaming_handler.dart +++ b/bin/api/streaming_handler.dart @@ -149,10 +149,11 @@ Handler createVodStreamHandler(Future 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 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 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 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++; } diff --git a/lib/features/iptv/screens/player_screen.dart b/lib/features/iptv/screens/player_screen.dart index 19df898..43189ab 100644 --- a/lib/features/iptv/screens/player_screen.dart +++ b/lib/features/iptv/screens/player_screen.dart @@ -345,6 +345,22 @@ class _PlayerScreenState extends ConsumerState { 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 { ], 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), diff --git a/lib/features/iptv/screens/series_detail_screen.dart b/lib/features/iptv/screens/series_detail_screen.dart index d5f8255..963397c 100644 --- a/lib/features/iptv/screens/series_detail_screen.dart +++ b/lib/features/iptv/screens/series_detail_screen.dart @@ -267,10 +267,25 @@ class _SeriesDetailScreenState extends ConsumerState { 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 { playlist: widget.playlist, streamType: StreamType.series, containerExtension: episode.containerExtension ?? 'mkv', - duration: episode.durationSecs != null ? Duration(seconds: episode.durationSecs!) : null, + duration: episodeDuration, ), ), ); diff --git a/lib/features/iptv/services/xtream_service.dart b/lib/features/iptv/services/xtream_service.dart index 6c6b898..d094137 100644 --- a/lib/features/iptv/services/xtream_service.dart +++ b/lib/features/iptv/services/xtream_service.dart @@ -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