feat: Add initial IPTV player with HLS/MPEG-TS support, query parameter parsing, and parent window communication.

This commit is contained in:
Michael committed 2025-12-07 14:57:32 +01:00
1 parent 8c53cd2bb0
commit 38f3f9c96b
5 files changed
+138 -11

No files matched your search

@@ -165,7 +165,7 @@ class _HeroCarouselState extends State<HeroCarousel> {
children: [
_HeroButton(
icon: Icons.play_arrow,
label: 'Play',
label: 'Lecture',
onPressed: () => widget.onTap(item),
isPrimary: true,
),
@@ -173,7 +173,7 @@ class _HeroCarouselState extends State<HeroCarousel> {
if (item.onMoreInfo != null)
_HeroButton(
icon: Icons.info_outline,
label: 'More Info',
label: 'Plus d\'infos',
onPressed: item.onMoreInfo!,
isPrimary: false,
),
+113 -2
View File
@@ -56,8 +56,18 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
bool _isInitialized = false;
bool _isLoading = true;
bool _showControls = false;
bool _isPlaying = true;
double _currentPosition = 0;
double _totalDuration = 1;
Timer? _controlsTimer;
StreamSubscription? _messageSubscription;
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
return "${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds";
}
// Premium Features State
List<Map<String, dynamic>> _audioTracks = [];
@@ -75,9 +85,18 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
if (type == 'playback_position' && _contentId.isNotEmpty) {
final currentTime = (data['currentTime'] as num).toDouble();
final duration = (data['duration'] as num).toDouble();
setState(() {
_currentPosition = currentTime;
_totalDuration = duration > 0 ? duration : 1;
});
ref.read(playbackPositionsProvider.notifier).savePosition(
_contentId, currentTime, duration
);
} else if (type == 'playback_status') {
final status = data['status'];
setState(() {
_isPlaying = status == 'playing';
});
} else if (type == 'playback_ended' && _contentId.isNotEmpty) {
} else if (type == 'user_activity') {
_onHover();
@@ -138,7 +157,23 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
});
}
_controlsTimer?.cancel();
_controlsTimer = Timer(const Duration(seconds: 3), _hideControls);
_controlsTimer = Timer(const Duration(seconds: 4), _hideControls);
}
void _togglePlayPause() {
setState(() {
_isPlaying = !_isPlaying;
});
_sendMessage({'type': _isPlaying ? 'play' : 'pause'});
_onHover(); // Keep controls visible
}
void _seekTo(double value) {
setState(() {
_currentPosition = value;
});
_sendMessage({'type': 'seek', 'value': value});
_onHover();
}
void _playNext() {
@@ -583,11 +618,87 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
// Hover Controls Layer
if (_showControls) ...[
// Play/Pause Button (Centered)
Positioned(
top: 0,
bottom: 0,
left: 0,
right: 0,
child: Center(
child: PointerInterceptor(
child: InkWell(
onTap: _togglePlayPause,
borderRadius: BorderRadius.circular(50),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
child: Icon(
_isPlaying ? Icons.pause : Icons.play_arrow,
color: Colors.white,
size: 64,
),
),
),
),
),
),
// Progress Bar (Bottom) - Only for VOD/Series
if (widget.streamType != StreamType.live)
Positioned(
bottom: 80,
left: 24,
right: 24,
child: PointerInterceptor(
child: Column(
children: [
SliderTheme(
data: SliderThemeData(
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8),
overlayShape: const RoundSliderOverlayShape(overlayRadius: 16),
activeTrackColor: AppColors.primary,
inactiveTrackColor: Colors.white24,
thumbColor: AppColors.primary,
overlayColor: AppColors.primary.withOpacity(0.2),
),
child: Slider(
value: _currentPosition,
max: _totalDuration,
onChanged: (value) {
_seekTo(value);
},
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_formatDuration(Duration(seconds: _currentPosition.toInt())),
style: const TextStyle(color: Colors.white, fontSize: 12),
),
Text(
_formatDuration(Duration(seconds: _totalDuration.toInt())),
style: const TextStyle(color: Colors.white, fontSize: 12),
),
],
),
),
],
),
),
),
// Previous Channel (Left Top Zone)
if (widget.channels != null && widget.channels!.isNotEmpty)
Positioned(
left: 0,
top: 80, // Moved to top
top: 80,
bottom: null,
height: 200,
width: 100,
+2 -2
View File
@@ -162,8 +162,8 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
: _movies.where((m) => settings.matchesMoviesFilter(m.categoryName)).toList();
}
// Hero Items (Take 5 random or first 5)
final heroItems = _movies.take(5).map((m) => HeroItem(
// Hero Items (Take 5 random or first 5 from filtered list)
final heroItems = displayMovies.take(5).map((m) => HeroItem(
id: m.streamId,
title: m.name,
imageUrl: m.streamIcon ?? '',
+1 -1
View File
@@ -161,7 +161,7 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
}
// Hero Items
final heroItems = _series.take(5).map((s) => HeroItem(
final heroItems = displaySeries.take(5).map((s) => HeroItem(
id: s.seriesId.toString(), // ID logic might differ for Series
title: s.name,
imageUrl: s.cover ?? '',
+20 -4
View File
@@ -182,17 +182,33 @@
video.style.objectFit = 'contain';
}
} else if (data.type === 'set_audio_track') {
// Not standard in HTML5 Video API across all browsers universally for MP4
// But HLS.js and mpegts.js have specific APIs.
// For native "video.audioTracks", it is experimental.
// We will try to handle HLS.js switching if active.
log('Switching audio track index: ' + data.index);
if (window.hlsInstance) {
window.hlsInstance.audioTrack = data.index;
}
} else if (data.type === 'play') {
log('Received play command');
video.play().catch(e => log('Play failed: ' + e));
} else if (data.type === 'pause') {
log('Received pause command');
video.pause();
} else if (data.type === 'seek') {
log('Seeking to: ' + data.value);
if (isFinite(data.value)) {
video.currentTime = data.value;
}
}
});
// Notify parent about playback state changes
video.addEventListener('play', () => {
window.parent.postMessage({ type: 'playback_status', status: 'playing' }, '*');
});
video.addEventListener('pause', () => {
window.parent.postMessage({ type: 'playback_status', status: 'paused' }, '*');
});
// Report available audio tracks
function reportAudioTracks() {
let tracks = [];