feat(player): bouton plein écran et reconnexion du proxy live

Plein écran — le bouton n'existait que dans les contrôles VOD, sous une icône
aspect_ratio peu parlante, et les contrôles Live TV n'en avaient aucun. Ajout
du bouton côté Live TV et passage aux icônes fullscreen / fullscreen_exit.
L'état suit désormais l'évènement fullscreenchange du document : sortir par la
touche Échap laissait l'icône désynchronisée.

Proxy live — la route /api/live/<id>.ts relayait la source sans aucune reprise.
Les panneaux Xtream ferment régulièrement la connexion en cours de route, et le
MediaSource du navigateur recevait alors un sourceEnded : la lecture s'arrêtait
net au bout de quelques dizaines de secondes. Le proxy rouvre maintenant la
source tant que le client écoute, avec quatre tentatives et une seconde
d'attente entre chaque, le temps que le panneau libère le slot de connexion.
Le compteur repart de zéro dès qu'une connexion a tenu plus d'une minute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5 committed 2026-08-11 10:31:13 +02:00
1 parent 8b82b101aa
commit f490e96448
2 files changed
+159 -38

No files matched your search

+50 -10
View File
@@ -61,6 +61,12 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
bool _isMuted = false;
bool _ignoreStatusUpdates = false;
/// Reflète l'état plein écran du document. Le navigateur peut en sortir
/// sans passer par notre bouton (touche Échap), d'où l'écoute de
/// `fullscreenchange` plutôt qu'un simple booléen basculé au clic.
bool _isFullscreen = false;
StreamSubscription<html.Event>? _fullscreenSubscription;
/// Server-side transcoding quality. Live TV defaults to `source`
/// (direct MPEG-TS proxy, zero transcoding); VOD defaults to `high`.
late StreamQuality _quality = widget.streamType == StreamType.live
@@ -85,15 +91,32 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
_onHover(); // Start the auto-hide timer
_initializePlayer(startTimeOverride: widget.startTime);
_setupMessageListener();
_fullscreenSubscription =
html.document.onFullscreenChange.listen((_) => _syncFullscreenState());
}
@override
void dispose() {
_messageSubscription?.cancel();
_fullscreenSubscription?.cancel();
_controlsTimer?.cancel();
super.dispose();
}
bool get _documentIsFullscreen {
final doc = html.document;
return doc.fullscreenElement != null ||
(doc as dynamic).webkitFullscreenElement != null ||
(doc as dynamic).mozFullScreenElement != null;
}
void _syncFullscreenState() {
final value = _documentIsFullscreen;
if (mounted && value != _isFullscreen) {
setState(() => _isFullscreen = value);
}
}
Future<void> _initializePlayer({
double? startTimeOverride,
bool isChannelSwitch = false,
@@ -320,15 +343,10 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
void _toggleFullscreen() {
try {
final doc = html.document;
final isFullscreen = doc.fullscreenElement != null ||
(doc as dynamic).webkitFullscreenElement != null ||
(doc as dynamic).mozFullScreenElement != null;
if (isFullscreen) {
doc.exitFullscreen();
if (_documentIsFullscreen) {
html.document.exitFullscreen();
} else {
final element = doc.documentElement;
final element = html.document.documentElement;
if (element != null) {
element.requestFullscreen();
}
@@ -337,6 +355,12 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
print('[PlayerScreen] Fullscreen error: $e');
_sendMessage({'type': 'request_fullscreen'});
}
// `fullscreenchange` arrive de façon asynchrone : relire l'état juste
// après l'appel évite que l'icône reste figée si l'évènement est perdu.
Future.delayed(
const Duration(milliseconds: 150),
_syncFullscreenState,
);
}
void _toggleMute() {
@@ -860,10 +884,14 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
),
const SizedBox(width: 16),
_buildGlassIconButton(
icon: Icons.aspect_ratio_rounded,
icon: _isFullscreen
? Icons.fullscreen_exit_rounded
: Icons.fullscreen_rounded,
onTap: _toggleFullscreen,
transparent: true,
tooltip: 'Plein écran',
tooltip: _isFullscreen
? 'Quitter le plein écran'
: 'Plein écran',
),
],
),
@@ -972,6 +1000,18 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
onSelected: _changeQuality,
size: buttonSize,
),
SizedBox(width: spacing),
// Fullscreen
_buildSimpleIconButton(
icon: _isFullscreen
? Icons.fullscreen_exit_rounded
: Icons.fullscreen_rounded,
onTap: _toggleFullscreen,
size: buttonSize,
tooltip: _isFullscreen ? 'Quitter le plein écran' : 'Plein écran',
),
];
}