Files
sync_replay_project/apps/desktop/test/files_thumbnails_test.dart
T
foxyzandClaude Opus 5.5 51c1c5ab78
ci / core (push) Successful in 31s
ci / server (push) Successful in 1m3s
ci / updater-windows (push) Successful in 6m6s
Restructure step R: the app is Starsync (dev.foxyz.starsync)
- Linux app id dev.foxyz.starsync, binary starsync; Windows starsync.exe,
  company foxyz; server starsync-server; Dart package starsync; archives
  starsync-<ver>-*.
- 1.2.x compatibility: watch_together and watch-together-server
  launchers (Linux) / copies (Windows) in the archive; the updater
  accepts both names and relaunches the new binary; update.sh/.bat use
  whichever server exists.
- First start copies (never moves) the old support folder and this
  app's Hive boxes out of ~/Documents into <support>/hive; a marker
  makes it run once; AppDirs is a port; settings schema version with
  ordered migrations.
- Launcher entry dev.foxyz.starsync.desktop; the old one removed.
- Repository name and URLs unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:21:20 +03:00

97 lines
3.7 KiB
Dart

import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:image/image.dart' as img;
import 'package:media_kit/media_kit.dart';
import 'package:starsync/src/files/thumbnails.dart';
// Real libmpv: frames grabbed with vo=null + screenshot-to-file. The test
// media comes from ffmpeg (this machine only; the app never needs it).
late Directory _tmp;
/// 8 s, 640x360: red until 1.5 s, green until 4 s, then blue; a keyframe
/// every 0.5 s, so the 25% frame (2 s) is green.
Future<String> _media(String name) async {
final path = '${_tmp.path}/$name';
final r = await Process.run('ffmpeg', [
'-y', '-v', 'error',
'-f', 'lavfi', '-i', 'color=c=red:size=640x360:rate=24:duration=1.5',
'-f', 'lavfi', '-i', 'color=c=green:size=640x360:rate=24:duration=2.5',
'-f', 'lavfi', '-i', 'color=c=blue:size=640x360:rate=24:duration=4',
'-filter_complex', '[0][1][2]concat=n=3:v=1:a=0',
'-c:v', 'libx264', '-preset', 'ultrafast', '-g', '12', '-pix_fmt', 'yuv420p',
path,
]);
if (r.exitCode != 0) fail('ffmpeg failed: ${r.stderr}');
return path;
}
void main() {
setUpAll(() {
MediaKit.ensureInitialized();
_tmp = Directory.systemTemp.createTempSync('files_thumbs_');
thumbnailCacheRoot = () async => Directory('${_tmp.path}/cache');
});
tearDownAll(() => _tmp.deleteSync(recursive: true));
test('a JPEG of the 25% frame plus the duration, then from the cache',
timeout: const Timeout(Duration(minutes: 2)), () async {
final path = await _media('a.mkv');
final sw = Stopwatch()..start();
final t = await thumbnailFor(path);
final first = sw.elapsedMilliseconds;
expect(t, isNotNull);
expect(t!.duration.inMilliseconds, closeTo(8000, 100));
final bytes = File(t.imagePath).readAsBytesSync();
expect(bytes.sublist(0, 2), [0xFF, 0xD8]); // JPEG
final image = img.decodeJpg(bytes)!;
expect(image.width, thumbnailWidth);
expect(image.height, 270);
final px = image.getPixel(image.width ~/ 2, image.height ~/ 2);
expect(px.g, greaterThan(100), reason: 'green frame, got $px');
expect(px.r, lessThan(60));
expect(px.b, lessThan(60));
sw.reset();
final again = await thumbnailFor(path);
final cachedMs = sw.elapsedMilliseconds;
expect(again?.imagePath, t.imagePath);
expect(again?.duration, t.duration);
// ignore: avoid_print
print('thumbnail: first $first ms, cached $cachedMs ms');
expect(cachedMs, lessThan(first));
});
test('several at once (2 at a time), same file deduplicated',
timeout: const Timeout(Duration(minutes: 2)), () async {
final paths = [for (var i = 0; i < 4; i++) await _media('m$i.mp4')];
final sw = Stopwatch()..start();
final results = await Future.wait([
for (final p in paths) thumbnailFor(p),
thumbnailFor(paths.first),
]);
// ignore: avoid_print
print('4 thumbnails in ${sw.elapsedMilliseconds} ms');
expect(results.every((t) => t != null), isTrue);
expect(results.first!.imagePath, results.last!.imagePath);
});
test('not a video, missing, cancelled: null, never a throw',
timeout: const Timeout(Duration(minutes: 1)), () async {
final text = File('${_tmp.path}/notes.mkv')..writeAsStringSync('hello');
expect(await thumbnailFor(text.path), isNull);
// The failure is remembered: the second answer comes from the cache.
final sw = Stopwatch()..start();
expect(await thumbnailFor(text.path), isNull);
expect(sw.elapsedMilliseconds, lessThan(500));
expect(await thumbnailFor('${_tmp.path}/missing.mkv'), isNull);
final path = await _media('c.mkv');
expect(await thumbnailFor(path, cancelled: () => true), isNull);
// A cancellation is not a remembered failure.
expect(await thumbnailFor(path), isNotNull);
});
}