feat: Add IPTV player screen with FFmpeg transcoding, resume playback, and dedicated movie/series tabs.

This commit is contained in:
Michael committed 2025-12-06 14:10:52 +01:00
1 parent 5c4488864b
commit 5671afbd26
6 files changed
+475 -87

No files matched your search

+52 -1
View File
@@ -3,8 +3,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:html' as html;
import 'dart:ui_web' as ui_web;
import 'dart:convert';
import 'dart:async';
import 'package:http/http.dart' as http;
import '../providers/xtream_provider.dart';
import '../providers/playback_positions_provider.dart';
import '../widgets/epg_overlay.dart';
import '../../../core/models/playlist_config.dart';
@@ -37,14 +39,51 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
String? _errorMessage;
String? _statusMessage;
late String _viewId;
late String _contentId;
bool _isInitialized = false;
bool _isLoading = true;
StreamSubscription? _messageSubscription;
@override
void initState() {
super.initState();
_viewId = 'iptv-player-${widget.streamId}-${DateTime.now().millisecondsSinceEpoch}';
// Content ID for resume feature
_contentId = widget.streamType == StreamType.live
? '' // Live streams don't need resume
: '${widget.streamType.name}_${widget.streamId}';
_initializePlayer();
_setupMessageListener();
}
void _setupMessageListener() {
// Listen for postMessage from player.html iframe
_messageSubscription = html.window.onMessage.listen((event) {
try {
final data = event.data;
if (data is Map) {
final type = data['type'];
if (type == 'playback_position' && _contentId.isNotEmpty) {
final currentTime = (data['currentTime'] as num).toDouble();
final duration = (data['duration'] as num).toDouble();
ref.read(playbackPositionsProvider.notifier).savePosition(
_contentId, currentTime, duration
);
} else if (type == 'playback_ended' && _contentId.isNotEmpty) {
// Clear position when video ends
ref.read(playbackPositionsProvider.notifier).clearPosition(_contentId);
}
}
} catch (e) {
debugPrint('Error handling postMessage: $e');
}
});
}
@override
void dispose() {
_messageSubscription?.cancel();
super.dispose();
}
Future<void> _initializePlayer() async {
@@ -145,9 +184,21 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
_statusMessage = 'Loading player...';
});
// Get resume position for VOD/Series
double startTime = 0;
if (widget.streamType != StreamType.live && _contentId.isNotEmpty) {
final positions = ref.read(playbackPositionsProvider);
startTime = positions.getPosition(_contentId);
if (startTime > 0) {
debugPrint('PlayerScreen: Resuming from ${startTime.toStringAsFixed(0)}s');
}
}
// Register the HTML view with an iframe pointing to our player.html
final encodedHlsUrl = Uri.encodeComponent(hlsUrl);
final playerUrl = 'player.html?url=$encodedHlsUrl';
final playerUrl = startTime > 0
? 'player.html?url=$encodedHlsUrl&startTime=${startTime.toStringAsFixed(0)}'
: 'player.html?url=$encodedHlsUrl';
debugPrint('PlayerScreen: Player URL: $playerUrl');