No buffering rings at the end of short videos; links never guess an AniList show
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9a4b04672b
commit
24fc137e4f
6 files changed
+33
-5
No files matched your search
@@ -51,7 +51,8 @@ Future<ArtBytes> jellyfinArt(JellyfinClient client, String itemId,
|
|||||||
Future<ArtBytes?> aniListArt(AniListClient aniList, String name,
|
Future<ArtBytes?> aniListArt(AniListClient aniList, String name,
|
||||||
{Future<Uint8List?> Function(Uri url, Map<String, String> headers, int maxBytes) fetch = fetchImage}) async {
|
{Future<Uint8List?> Function(Uri url, Map<String, String> headers, int maxBytes) fetch = fetchImage}) async {
|
||||||
final series = parseReleaseName(name).series.trim();
|
final series = parseReleaseName(name).series.trim();
|
||||||
if (series.isEmpty) return null;
|
// "1", "2026-10-09": nothing a show could be called.
|
||||||
|
if (!RegExp(r'\p{L}', unicode: true).hasMatch(series)) return null;
|
||||||
final match = await aniList.searchMedia(series);
|
final match = await aniList.searchMedia(series);
|
||||||
if (match == null) return null;
|
if (match == null) return null;
|
||||||
Future<Uint8List?> get(String? url, int maxBytes) async {
|
Future<Uint8List?> get(String? url, int maxBytes) async {
|
||||||
|
|||||||
@@ -1059,6 +1059,9 @@ class SessionCoordinator {
|
|||||||
final ArtBytes? bytes;
|
final ArtBytes? bytes;
|
||||||
if (ref != null && client != null) {
|
if (ref != null && client != null) {
|
||||||
bytes = await jellyfinArt(client, ref.itemId);
|
bytes = await jellyfinArt(client, ref.itemId);
|
||||||
|
} else if (ref == null && (media.url ?? '').isNotEmpty) {
|
||||||
|
// A web link: its name is a URL tail ("1"), not a show; never guess.
|
||||||
|
bytes = null;
|
||||||
} else {
|
} else {
|
||||||
final name = ref == null ? media.name : jellyfinTitle(ref.title, ref.subtitle).$1;
|
final name = ref == null ? media.name : jellyfinTitle(ref.title, ref.subtitle).$1;
|
||||||
bytes = await aniListArt(_aniList ??= AniListClient(), name);
|
bytes = await aniListArt(_aniList ??= AniListClient(), name);
|
||||||
|
|||||||
@@ -208,6 +208,8 @@ void main() {
|
|||||||
expect(art.banner, jpeg);
|
expect(art.banner, jpeg);
|
||||||
expect(asked, unorderedEquals(['https://img/xl.jpg', 'https://img/banner.jpg', 'https://img/l.jpg']));
|
expect(asked, unorderedEquals(['https://img/xl.jpg', 'https://img/banner.jpg', 'https://img/l.jpg']));
|
||||||
expect(await aniListArt(c, 'Nothing Like It.mkv', fetch: (_, _, _) async => jpeg), isNull);
|
expect(await aniListArt(c, 'Nothing Like It.mkv', fetch: (_, _, _) async => jpeg), isNull);
|
||||||
|
// A link tail like ".../video/1" names no show: no AniList search at all.
|
||||||
|
expect(await aniListArt(c, '1', fetch: (_, _, _) async => jpeg), isNull);
|
||||||
c.close();
|
c.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -662,8 +662,8 @@ always scrolls the page). Space never presses a focused button
|
|||||||
the controls: `playerStatusFor` (pure) picks the most pressing of
|
the controls: `playerStatusFor` (pure) picks the most pressing of
|
||||||
removed, reconnecting (Cancel), a warn/error health message (Retry),
|
removed, reconnecting (Cancel), a warn/error health message (Retry),
|
||||||
connecting (only after 2 s), finished, someone loading (below
|
connecting (only after 2 s), finished, someone loading (below
|
||||||
`StateBroadcast.bufferNeed`, the gate's own need: less near the end,
|
`StateBroadcast.bufferNeed`, the gate's own need at the current
|
||||||
none at finished; their avatar with the progress round it), you not
|
position: less near the end, none at finished; their avatar with the progress round it), you not
|
||||||
ready (a click marks you ready) and who everyone waits for (avatars,
|
ready (a click marks you ready) and who everyone waits for (avatars,
|
||||||
"2/3 ready"); nothing otherwise, info never. 34 px avatars, a main line
|
"2/3 ready"); nothing otherwise, info never. 34 px avatars, a main line
|
||||||
and a muted detail line. Frosted glass, colour only for problems; it fades through
|
and a muted detail line. Frosted glass, colour only for problems; it fades through
|
||||||
|
|||||||
@@ -559,12 +559,17 @@ class StateBroadcast {
|
|||||||
/// Buffer an in-set member needs before playback can go on, as the
|
/// Buffer an in-set member needs before playback can go on, as the
|
||||||
/// server's gate counts it: [RelayConstants.minBufferSecs], less near the
|
/// server's gate counts it: [RelayConstants.minBufferSecs], less near the
|
||||||
/// end (mpv cannot buffer past it), none once finished.
|
/// end (mpv cannot buffer past it), none once finished.
|
||||||
double get bufferNeed {
|
double get bufferNeed => bufferNeedAt(DateTime.now().toUtc());
|
||||||
|
|
||||||
|
/// [bufferNeed] at [now]: while playing the position has moved on since
|
||||||
|
/// [serverTime] (the local clock is close enough for this).
|
||||||
|
double bufferNeedAt(DateTime now) {
|
||||||
if (transport == 'finished') return 0;
|
if (transport == 'finished') return 0;
|
||||||
final d = (media?['duration'] as num?)?.toDouble() ?? 0;
|
final d = (media?['duration'] as num?)?.toDouble() ?? 0;
|
||||||
const full = RelayConstants.minBufferSecs;
|
const full = RelayConstants.minBufferSecs;
|
||||||
if (d <= 0) return full;
|
if (d <= 0) return full;
|
||||||
return (d - position - RelayConstants.endSlackSecs).clamp(0.0, full).toDouble();
|
final moved = transport == 'playing' ? now.difference(serverTime).inMilliseconds / 1000 : 0;
|
||||||
|
return (d - (position + moved) - RelayConstants.endSlackSecs).clamp(0.0, full).toDouble();
|
||||||
}
|
}
|
||||||
|
|
||||||
final int eventId;
|
final int eventId;
|
||||||
|
|||||||
@@ -193,4 +193,21 @@ void main() {
|
|||||||
expect((r.duration, r.mediaId), (0.0, 0));
|
expect((r.duration, r.mediaId), (0.0, 0));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('bufferNeed counts the time played since the last event', () {
|
||||||
|
final t0 = DateTime.utc(2026, 1, 1);
|
||||||
|
final s = StateBroadcast(
|
||||||
|
eventId: 1,
|
||||||
|
transport: 'playing',
|
||||||
|
position: 0,
|
||||||
|
serverTime: t0,
|
||||||
|
mediaId: 1,
|
||||||
|
media: const {'name': 'clip', 'duration': 6.0},
|
||||||
|
members: const [],
|
||||||
|
);
|
||||||
|
expect(s.bufferNeedAt(t0), 4.0);
|
||||||
|
// 3 s in: 3 s left, minus the 2 s end slack.
|
||||||
|
expect(s.bufferNeedAt(t0.add(const Duration(seconds: 3))), closeTo(1.0, 1e-9));
|
||||||
|
expect(s.bufferNeedAt(t0.add(const Duration(seconds: 5))), 0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user