Merge pull request #68 from R0m1k3/antigravity

feat: add IPTV player screen supporting live, VOD, and series streams…
This commit is contained in:
LogiFlow authored and GitHub committed 2025-12-07 10:15:52 +01:00
commit 2a0d8892b0
3 files changed
+201 -22

No files matched your search

+137 -22
View File
@@ -11,7 +11,9 @@ import '../providers/settings_provider.dart';
import '../widgets/epg_overlay.dart';
import '../../../core/models/playlist_config.dart';
import '../../../core/models/iptv_models.dart';
import '../../../core/widgets/themed_loading_screen.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';
/// Stream type enum for player
enum StreamType { live, vod, series }
@@ -53,6 +55,10 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
bool _showControls = false;
Timer? _controlsTimer;
StreamSubscription? _messageSubscription;
// Premium Features State
List<Map<String, dynamic>> _audioTracks = [];
String _aspectRatio = 'contain';
@override
void initState() {
@@ -78,9 +84,13 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
_contentId, currentTime, duration
);
} else if (type == 'playback_ended' && _contentId.isNotEmpty) {
ref.read(playbackPositionsProvider.notifier).clearPosition(_contentId);
} else if (type == 'user_activity') {
_onHover();
} else if (type == 'audio_tracks') {
final tracks = List<Map<String, dynamic>>.from(data['tracks']);
setState(() {
_audioTracks = tracks;
});
}
}
} catch (e) {
@@ -89,6 +99,24 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
});
}
void _sendMessage(Map<String, dynamic> message) {
// Helper to send message to iframe
final iframe = html.document.getElementById(_viewId) as html.IFrameElement?;
iframe?.contentWindow?.postMessage(message, '*');
}
void _setAspectRatio(String mode) {
setState(() {
_aspectRatio = mode;
});
_sendMessage({'type': 'set_aspect_ratio', 'value': mode});
}
void _setAudioTrack(int index) {
_sendMessage({'type': 'set_audio_track', 'index': index});
Navigator.pop(context); // Close settings
}
@override
void dispose() {
_messageSubscription?.cancel();
@@ -262,6 +290,75 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
}
}
void _showSettingsDialog() {
showDialog(
context: context,
builder: (context) => PointerInterceptor(
child: AlertDialog(
backgroundColor: const Color(0xFF1E1E1E),
title: const Text('Réglages du Lecteur', style: TextStyle(color: Colors.white)),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Format d\'image', style: TextStyle(color: Colors.white70, fontSize: 13, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: [
_buildAspectRatioChip('Original', 'contain'),
_buildAspectRatioChip('Remplir', 'cover'),
_buildAspectRatioChip('Étirer', 'fill'),
],
),
if (_audioTracks.isNotEmpty) ...[
const SizedBox(height: 24),
const Text('Pistes Audio', style: TextStyle(color: Colors.white70, fontSize: 13, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Container(
constraints: const BoxConstraints(maxHeight: 200),
child: SingleChildScrollView(
child: Column(
children: _audioTracks.map((track) {
return ListTile(
title: Text(track['label'] ?? 'Piste ${track['id']}', style: const TextStyle(color: Colors.white)),
subtitle: Text(track['lang'] ?? '', style: const TextStyle(color: Colors.white54)),
onTap: () => _setAudioTrack(track['id']),
dense: true,
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.audiotrack, color: Colors.white54),
);
}).toList(),
),
),
),
],
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Fermer'),
),
],
),
),
);
}
Widget _buildAspectRatioChip(String label, String value) {
final isSelected = _aspectRatio == value;
return ChoiceChip(
label: Text(label),
selected: isSelected,
onSelected: (_) => _setAspectRatio(value),
backgroundColor: Colors.black26,
selectedColor: Theme.of(context).primaryColor,
labelStyle: TextStyle(color: isSelected ? Colors.white : Colors.white70),
side: BorderSide.none,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -272,6 +369,16 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
elevation: 0,
actions: [
PointerInterceptor(
child: IconButton(
icon: const Icon(Icons.settings),
onPressed: _showSettingsDialog,
tooltip: 'Réglages',
),
),
const SizedBox(width: 16),
],
) : null,
extendBodyBehindAppBar: true,
backgroundColor: Colors.black,
@@ -312,34 +419,40 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
// Hover Controls Layer
if (_showControls) ...[
// Previous Channel (Left Zone)
// Previous Channel (Left Top Zone)
if (widget.channels != null && widget.channels!.isNotEmpty)
Positioned(
left: 0,
top: 0,
bottom: 0,
top: 80, // Moved to top
bottom: null,
height: 200,
width: 100,
child: InkWell(
onTap: _playPrevious,
hoverColor: Colors.black12,
child: const Center(
child: Icon(Icons.arrow_back_ios, color: Colors.white, size: 48),
child: PointerInterceptor(
child: InkWell(
onTap: _playPrevious,
hoverColor: Colors.black12,
child: const Center(
child: Icon(Icons.arrow_back_ios, color: Colors.white, size: 48),
),
),
),
),
// Next Channel (Right Zone)
// Next Channel (Right Top Zone)
if (widget.channels != null && widget.channels!.isNotEmpty)
Positioned(
right: 0,
top: 0,
bottom: 0,
top: 80, // Moved to top
bottom: null,
height: 200,
width: 100,
child: InkWell(
onTap: _playNext,
hoverColor: Colors.black12,
child: const Center(
child: Icon(Icons.arrow_forward_ios, color: Colors.white, size: 48),
child: PointerInterceptor(
child: InkWell(
onTap: _playNext,
hoverColor: Colors.black12,
child: const Center(
child: Icon(Icons.arrow_forward_ios, color: Colors.white, size: 48),
),
),
),
),
@@ -359,11 +472,13 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
colors: [Colors.black87, Colors.transparent],
),
),
child: EpgOverlay(
streamId: widget.channels != null
? widget.channels![_currentIndex].streamId
: widget.streamId,
playlist: widget.playlist,
child: PointerInterceptor(
child: EpgOverlay(
streamId: widget.channels != null
? widget.channels![_currentIndex].streamId
: widget.streamId,
playlist: widget.playlist,
),
),
),
),
+1
View File
@@ -38,6 +38,7 @@ dependencies:
# UI Components
google_fonts: ^6.2.1
cached_network_image: ^3.3.1
pointer_interceptor: ^0.10.1
# Security
crypto: ^3.0.3
+63
View File
@@ -166,6 +166,67 @@
}, '*');
});
// Listen for commands from parent (Flutter)
window.addEventListener('message', (event) => {
const data = event.data;
if (!data) return;
if (data.type === 'set_aspect_ratio') {
log('Setting aspect ratio: ' + data.value);
// value: 'contain', 'cover', 'fill' (fill = 100% 100%)
if (data.value === 'fill') {
video.style.objectFit = 'fill';
} else if (data.value === 'cover') {
video.style.objectFit = 'cover';
} else {
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;
}
}
});
// Report available audio tracks
function reportAudioTracks() {
let tracks = [];
if (window.hlsInstance && window.hlsInstance.audioTracks) {
tracks = window.hlsInstance.audioTracks.map((t, i) => ({
id: i,
label: t.name || t.lang || ('Track ' + (i + 1)),
lang: t.lang || ''
}));
} else if (video.audioTracks) {
// Standard HTML5 (Safari/experimental)
for (let i = 0; i < video.audioTracks.length; i++) {
const t = video.audioTracks[i];
tracks.push({
id: i,
label: t.label || t.language || ('Track ' + (i + 1)),
lang: t.language || ''
});
}
}
if (tracks.length > 0) {
window.parent.postMessage({
type: 'audio_tracks',
tracks: tracks
}, '*');
}
}
// Hook reporting into playback start
video.addEventListener('loadedmetadata', () => {
setTimeout(reportAudioTracks, 1000); // Wait a bit for tracks to parse
});
// Report user activity (mouse movement) to parent
let lastActivityTime = 0;
document.addEventListener('mousemove', () => {
@@ -259,6 +320,8 @@
xhrSetup: function (xhr, url) { }
});
window.hlsInstance = hls; // Expose for external control (audio tracks)
hls.loadSource(url);
hls.attachMedia(video);