Swap shows only the new media, no guessed endings, quieter screen
- A swap stops the previous media at once; buffer counts only for the media the player holds (client) and the current mediaId (server), so the old video can no longer start the new one. - The automatic frozen-picture reload is gone (false alarms on still scenes); Reload video stays. - The "Next episode" pill shows only from a known ending. - A web link's thumbnail is its cover, under the video's own name. - While playing, the unread chip stays off the picture; chat pop-ups can be All / Only when paused / Off. - Server fetch failures say when the server has no cookies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ab133dd82f
commit
27b4ec2c96
25 files changed
+274
-485
No files matched your search
@@ -1,65 +0,0 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../playback/frame_watchdog.dart';
|
||||
|
||||
/// Fingerprints what the compositor really shows of the video. Place
|
||||
/// [anchor] under a `RepaintBoundary` around the video (the session player
|
||||
/// does); [sample] captures that boundary around the newest mounted anchor
|
||||
/// at ~200 px wide — the texture included — and hashes its centre. This is the only per-presented-frame signal: mpv's own
|
||||
/// counters keep ticking when the Flutter texture is stuck.
|
||||
class ShownFrameProbe {
|
||||
final _anchors = <_AnchorState>[];
|
||||
|
||||
Widget anchor() => _Anchor(probe: this);
|
||||
|
||||
Future<int?> sample() async {
|
||||
for (final a in _anchors.reversed) {
|
||||
if (!a.mounted) continue;
|
||||
final boundary = a.context.findAncestorRenderObjectOfType<RenderRepaintBoundary>();
|
||||
if (boundary == null || !boundary.attached || !boundary.hasSize) return null;
|
||||
final width = boundary.size.width;
|
||||
if (width <= 0) return null;
|
||||
ui.Image? image;
|
||||
try {
|
||||
image = await boundary.toImage(pixelRatio: (200 / width).clamp(0.02, 1.0));
|
||||
final data = await image.toByteData(format: ui.ImageByteFormat.rawRgba);
|
||||
if (data == null) return null;
|
||||
return hashCenterPixels(data, image.width, image.height);
|
||||
} catch (_) {
|
||||
// Route not painted (covered) or mid-teardown: no reading.
|
||||
return null;
|
||||
} finally {
|
||||
image?.dispose();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class _Anchor extends StatefulWidget {
|
||||
final ShownFrameProbe probe;
|
||||
const _Anchor({required this.probe});
|
||||
|
||||
@override
|
||||
State<_Anchor> createState() => _AnchorState();
|
||||
}
|
||||
|
||||
class _AnchorState extends State<_Anchor> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.probe._anchors.add(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.probe._anchors.remove(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => const SizedBox.shrink();
|
||||
}
|
||||
Reference in new issue
Block a user