feat(iptv): introduce web-based video player screen for live, VOD, and series content

This commit is contained in:
Michael committed 2025-12-08 20:10:40 +01:00
1 parent ec9ea0f66b
commit d48d25f65e
6 files changed
+51 -14

No files matched your search

+4 -3
View File
@@ -88,7 +88,8 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
final type = data['type'];
if (type == 'playback_position' && _contentId.isNotEmpty) {
final currentTime = (data['currentTime'] as num).toDouble();
final duration = (data['duration'] as num).toDouble();
final rawDuration = (data['duration'] as num).toDouble();
final duration = rawDuration.isFinite ? rawDuration : 0.0;
setState(() {
_currentPosition = currentTime;
_totalDuration = duration > 0 ? duration : 1;
@@ -936,11 +937,11 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_formatDuration(Duration(seconds: _currentPosition.toInt())),
_formatDuration(Duration(seconds: _currentPosition.isFinite ? _currentPosition.toInt() : 0)),
style: const TextStyle(color: Colors.white, fontSize: 12),
),
Text(
_formatDuration(Duration(seconds: _totalDuration.toInt())),
_formatDuration(Duration(seconds: _totalDuration.isFinite ? _totalDuration.toInt() : 0)),
style: const TextStyle(color: Colors.white, fontSize: 12),
),
],
+11 -2
View File
@@ -140,6 +140,15 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
return rating;
}
/// Proxy HTTP images through backend to avoid CORS/mixed-content issues
String? _getProxiedImageUrl(String? originalUrl) {
if (originalUrl == null || originalUrl.isEmpty) return null;
if (originalUrl.startsWith('http://')) {
return '/api/xtream/$originalUrl';
}
return originalUrl;
}
void _playMovie(Movie movie) {
ref.read(watchHistoryProvider.notifier).markMovieWatched(movie.streamId);
@@ -175,7 +184,7 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
final heroItems = displayMovies.take(5).map((m) => HeroItem(
id: m.streamId,
title: m.name,
imageUrl: m.streamIcon ?? '',
imageUrl: _getProxiedImageUrl(m.streamIcon),
subtitle: m.rating != null ? '${_formatRating(m.rating)} ★' : null,
onMoreInfo: () {
_playMovie(m);
@@ -291,7 +300,7 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
return MediaCard(
title: movie.name,
imageUrl: movie.streamIcon,
imageUrl: _getProxiedImageUrl(movie.streamIcon),
subtitle: movie.rating != null ? '${_formatRating(movie.rating)} ★' : null,
rating: _formatRating(movie.rating),
+11 -2
View File
@@ -140,6 +140,15 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
return rating;
}
/// Proxy HTTP images through backend to avoid CORS/mixed-content issues
String? _getProxiedImageUrl(String? originalUrl) {
if (originalUrl == null || originalUrl.isEmpty) return null;
if (originalUrl.startsWith('http://')) {
return '/api/xtream/$originalUrl';
}
return originalUrl;
}
void _openSeries(Series series) {
Navigator.push(
context,
@@ -173,7 +182,7 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
final heroItems = displaySeries.take(5).map((s) => HeroItem(
id: s.seriesId.toString(), // ID logic might differ for Series
title: s.name,
imageUrl: s.cover ?? '',
imageUrl: _getProxiedImageUrl(s.cover),
subtitle: s.rating != null ? '${_formatRating(s.rating)} ★' : null,
onMoreInfo: () => _openSeries(s),
)).toList();
@@ -281,7 +290,7 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
final serie = displaySeries[index];
return MediaCard(
title: serie.name,
imageUrl: serie.cover,
imageUrl: _getProxiedImageUrl(serie.cover),
subtitle: serie.rating != null ? '${_formatRating(serie.rating)} ★' : null,
rating: _formatRating(serie.rating),
@@ -125,6 +125,15 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
return rating;
}
/// Proxy HTTP images through backend to avoid CORS/mixed-content issues
String? _getProxiedImageUrl(String? originalUrl) {
if (originalUrl == null || originalUrl.isEmpty) return null;
if (originalUrl.startsWith('http://')) {
return '/api/xtream/$originalUrl';
}
return originalUrl;
}
void _playMovie(Movie movie) {
ref.read(watchHistoryProvider.notifier).markMovieWatched(movie.streamId);
@@ -160,7 +169,7 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
final heroItems = _movies.take(3).map((m) => HeroItem(
id: m.streamId,
title: m.name,
imageUrl: m.streamIcon ?? '',
imageUrl: _getProxiedImageUrl(m.streamIcon),
subtitle: m.rating != null ? '${_formatRating(m.rating)} ★' : null,
onMoreInfo: () => _playMovie(m),
)).toList();
@@ -251,7 +260,7 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
return MediaCard(
title: movie.name,
imageUrl: movie.streamIcon,
imageUrl: _getProxiedImageUrl(movie.streamIcon),
subtitle: movie.rating != null ? '${_formatRating(movie.rating)} ★' : null,
rating: _formatRating(movie.rating),
@@ -124,6 +124,15 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> {
return rating;
}
/// Proxy HTTP images through backend to avoid CORS/mixed-content issues
String? _getProxiedImageUrl(String? originalUrl) {
if (originalUrl == null || originalUrl.isEmpty) return null;
if (originalUrl.startsWith('http://')) {
return '/api/xtream/$originalUrl';
}
return originalUrl;
}
void _openSeries(Series series) {
Navigator.push(
context,
@@ -153,7 +162,7 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> {
final heroItems = _series.take(3).map((s) => HeroItem(
id: s.seriesId.toString(),
title: s.name,
imageUrl: s.cover ?? '',
imageUrl: _getProxiedImageUrl(s.cover),
subtitle: s.rating != null ? '${_formatRating(s.rating)} ★' : null,
onMoreInfo: () => _openSeries(s),
)).toList();
@@ -251,7 +260,7 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> {
return MediaCard(
title: series.name,
imageUrl: series.cover,
imageUrl: _getProxiedImageUrl(series.cover),
subtitle: series.rating != null ? '${_formatRating(series.rating)} ★' : null,
rating: _formatRating(series.rating),
+3 -3
View File
@@ -144,7 +144,7 @@
window.parent.postMessage({
type: 'playback_position',
currentTime: video.currentTime,
duration: video.duration || 0
duration: isFinite(video.duration) ? video.duration : 0
}, '*');
}
}
@@ -158,7 +158,7 @@
window.parent.postMessage({
type: 'playback_position',
currentTime: video.currentTime,
duration: video.duration || 0
duration: isFinite(video.duration) ? video.duration : 0
}, '*');
});
@@ -166,7 +166,7 @@
video.addEventListener('ended', () => {
window.parent.postMessage({
type: 'playback_ended',
duration: video.duration || 0
duration: isFinite(video.duration) ? video.duration : 0
}, '*');
});