mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-11 17:29:26 +02:00
Merge pull request #68 from R0m1k3/antigravity
feat: add IPTV player screen supporting live, VOD, and series streams…
This commit is contained in:
commit
fb41656ecb
3 files changed
+201
-22
No files matched your search
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in new issue
Block a user