fix(player): tick the frame-clock keep-alive only on Linux
The 1x1 keep-alive repaint loop was extended to Windows in a87aa296 to
paper over the legacy compositing path's resize desync (#227); the DComp
rework replaced that presentation path entirely. On the DComp engine the
100ms repaints become DirectComposition commits during playback, and
once fullscreen focus engages VRR (FreeSync/G-Sync) every commit forces
a scanout off the video's cadence - the micro-stutter of #1707.
The widget now owns the platform decision behind a test seam, and a new
quiescence test pins the hidden-chrome player UI to zero scheduled
frames so no future ticker can silently reintroduce the defect.
This commit is contained in:
@@ -1021,10 +1021,11 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
onHover: (_) => _showControlsFromPointerActivity(),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Keep-alive: 1px widget that continuously repaints to prevent
|
||||
// Flutter animations from freezing when the frame clock goes idle
|
||||
if (Platform.isLinux || Platform.isWindows)
|
||||
const Positioned(top: 0, left: 0, child: LinuxKeepAlive()),
|
||||
// Keep-alive for Linux's idle GTK frame clock; inert on every
|
||||
// other platform (the widget owns the platform decision).
|
||||
// Windows must NOT tick here: forced repaints during playback
|
||||
// perturb VRR scanout in fullscreen (#1707).
|
||||
const Positioned(top: 0, left: 0, child: LinuxKeepAlive()),
|
||||
// Also handles long-press for 2x speed.
|
||||
Positioned.fill(
|
||||
child: Semantics(
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
import 'dart:async' show Timer;
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A 1x1 pixel widget that continuously repaints to keep Flutter's frame clock active on Linux.
|
||||
/// This prevents animations from freezing when GTK's frame clock goes idle.
|
||||
/// A 1x1 pixel widget that continuously repaints to keep Flutter's frame clock
|
||||
/// active on Linux, where GTK's frame clock goes idle and freezes animations.
|
||||
///
|
||||
/// Linux-only by design. Mounting this on other platforms is harmful: on
|
||||
/// Windows the 10Hz repaints become DirectComposition commits during playback,
|
||||
/// and once fullscreen focus engages VRR (FreeSync/G-Sync) each commit forces a
|
||||
/// scanout off the video's cadence — the micro-stutter of issue #1707. Every
|
||||
/// non-Linux platform must instead uphold the invariant that the player UI
|
||||
/// schedules no frames while its chrome is hidden.
|
||||
class LinuxKeepAlive extends StatefulWidget {
|
||||
const LinuxKeepAlive({super.key});
|
||||
|
||||
/// Forces the platform decision so tests can exercise both the ticking and
|
||||
/// the inert path regardless of host OS.
|
||||
@visibleForTesting
|
||||
static bool? debugIsLinuxOverride;
|
||||
|
||||
/// Whether this widget repaints on the current platform. Exposed so the
|
||||
/// quiescence test can pin the policy to exactly [Platform.isLinux].
|
||||
@visibleForTesting
|
||||
static bool get ticksOnThisPlatform => debugIsLinuxOverride ?? Platform.isLinux;
|
||||
|
||||
@override
|
||||
State<LinuxKeepAlive> createState() => _LinuxKeepAliveState();
|
||||
}
|
||||
@@ -18,6 +36,7 @@ class _LinuxKeepAliveState extends State<LinuxKeepAlive> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (!LinuxKeepAlive.ticksOnThisPlatform) return;
|
||||
// Repaint every 100ms to keep Flutter's frame scheduler active.
|
||||
_timer = Timer.periodic(const Duration(milliseconds: 100), (_) {
|
||||
if (mounted) {
|
||||
@@ -36,6 +55,7 @@ class _LinuxKeepAliveState extends State<LinuxKeepAlive> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_timer == null) return const SizedBox.shrink();
|
||||
return SizedBox(width: 1, height: 1, child: ColoredBox(color: Color.fromRGBO(0, 0, 0, _tick % 2 == 0 ? 0.1 : 0.2)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user