mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +02:00
Merge pull request #74 from R0m1k3/antigravity
feat: Add initial IPTV player with HLS/MPEG-TS support, query paramet…
This commit is contained in:
commit
eddd209a66
5 files changed
+138
-11
No files matched your search
@@ -165,7 +165,7 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
|||||||
children: [
|
children: [
|
||||||
_HeroButton(
|
_HeroButton(
|
||||||
icon: Icons.play_arrow,
|
icon: Icons.play_arrow,
|
||||||
label: 'Play',
|
label: 'Lecture',
|
||||||
onPressed: () => widget.onTap(item),
|
onPressed: () => widget.onTap(item),
|
||||||
isPrimary: true,
|
isPrimary: true,
|
||||||
),
|
),
|
||||||
@@ -173,7 +173,7 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
|||||||
if (item.onMoreInfo != null)
|
if (item.onMoreInfo != null)
|
||||||
_HeroButton(
|
_HeroButton(
|
||||||
icon: Icons.info_outline,
|
icon: Icons.info_outline,
|
||||||
label: 'More Info',
|
label: 'Plus d\'infos',
|
||||||
onPressed: item.onMoreInfo!,
|
onPressed: item.onMoreInfo!,
|
||||||
isPrimary: false,
|
isPrimary: false,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -56,8 +56,18 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
|||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
bool _showControls = false;
|
bool _showControls = false;
|
||||||
|
bool _isPlaying = true;
|
||||||
|
double _currentPosition = 0;
|
||||||
|
double _totalDuration = 1;
|
||||||
Timer? _controlsTimer;
|
Timer? _controlsTimer;
|
||||||
StreamSubscription? _messageSubscription;
|
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
|
// Premium Features State
|
||||||
List<Map<String, dynamic>> _audioTracks = [];
|
List<Map<String, dynamic>> _audioTracks = [];
|
||||||
@@ -75,9 +85,18 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
|||||||
if (type == 'playback_position' && _contentId.isNotEmpty) {
|
if (type == 'playback_position' && _contentId.isNotEmpty) {
|
||||||
final currentTime = (data['currentTime'] as num).toDouble();
|
final currentTime = (data['currentTime'] as num).toDouble();
|
||||||
final duration = (data['duration'] as num).toDouble();
|
final duration = (data['duration'] as num).toDouble();
|
||||||
|
setState(() {
|
||||||
|
_currentPosition = currentTime;
|
||||||
|
_totalDuration = duration > 0 ? duration : 1;
|
||||||
|
});
|
||||||
ref.read(playbackPositionsProvider.notifier).savePosition(
|
ref.read(playbackPositionsProvider.notifier).savePosition(
|
||||||
_contentId, currentTime, duration
|
_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 == 'playback_ended' && _contentId.isNotEmpty) {
|
||||||
} else if (type == 'user_activity') {
|
} else if (type == 'user_activity') {
|
||||||
_onHover();
|
_onHover();
|
||||||
@@ -138,7 +157,23 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
_controlsTimer?.cancel();
|
_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() {
|
void _playNext() {
|
||||||
@@ -583,11 +618,87 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
|||||||
|
|
||||||
// Hover Controls Layer
|
// Hover Controls Layer
|
||||||
if (_showControls) ...[
|
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)
|
// Previous Channel (Left Top Zone)
|
||||||
if (widget.channels != null && widget.channels!.isNotEmpty)
|
if (widget.channels != null && widget.channels!.isNotEmpty)
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 0,
|
left: 0,
|
||||||
top: 80, // Moved to top
|
top: 80,
|
||||||
bottom: null,
|
bottom: null,
|
||||||
height: 200,
|
height: 200,
|
||||||
width: 100,
|
width: 100,
|
||||||
|
|||||||
@@ -162,8 +162,8 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
|
|||||||
: _movies.where((m) => settings.matchesMoviesFilter(m.categoryName)).toList();
|
: _movies.where((m) => settings.matchesMoviesFilter(m.categoryName)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hero Items (Take 5 random or first 5)
|
// Hero Items (Take 5 random or first 5 from filtered list)
|
||||||
final heroItems = _movies.take(5).map((m) => HeroItem(
|
final heroItems = displayMovies.take(5).map((m) => HeroItem(
|
||||||
id: m.streamId,
|
id: m.streamId,
|
||||||
title: m.name,
|
title: m.name,
|
||||||
imageUrl: m.streamIcon ?? '',
|
imageUrl: m.streamIcon ?? '',
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Hero Items
|
// 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
|
id: s.seriesId.toString(), // ID logic might differ for Series
|
||||||
title: s.name,
|
title: s.name,
|
||||||
imageUrl: s.cover ?? '',
|
imageUrl: s.cover ?? '',
|
||||||
|
|||||||
+20
-4
@@ -182,17 +182,33 @@
|
|||||||
video.style.objectFit = 'contain';
|
video.style.objectFit = 'contain';
|
||||||
}
|
}
|
||||||
} else if (data.type === 'set_audio_track') {
|
} 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);
|
log('Switching audio track index: ' + data.index);
|
||||||
if (window.hlsInstance) {
|
if (window.hlsInstance) {
|
||||||
window.hlsInstance.audioTrack = data.index;
|
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
|
// Report available audio tracks
|
||||||
function reportAudioTracks() {
|
function reportAudioTracks() {
|
||||||
let tracks = [];
|
let tracks = [];
|
||||||
|
|||||||
Reference in new issue
Block a user