JJ_MKVMaker/integration_test/full_player_test.dart

186 lines
9.5 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:jj_mkvmaker/app/app_controller.dart';
import 'package:jj_mkvmaker/app/i18n_controller.dart';
import 'package:jj_mkvmaker/core/playlist.dart';
import 'package:jj_mkvmaker/l10n/tr.dart';
import 'package:jj_mkvmaker/platform/common/media_kit_full_player.dart';
import 'package:jj_mkvmaker/platform/common/media_kit_player.dart';
import 'package:jj_mkvmaker/platform/windows/desktop_shell.dart';
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
import 'package:jj_mkvmaker/services/platform_services.dart';
import 'package:jj_mkvmaker/services/media_player.dart';
import 'package:jj_mkvmaker/ui/player_page.dart';
import 'package:jj_mkvmaker/ui/theme.dart';
import 'package:path/path.dart' as p;
import 'package:window_manager/window_manager.dart';
import 'support/temp_dir.dart';
/// 실제 mpv 플레이어 + 창 모드
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('재생 목록 · 자막 트랙 · 음량 · 전체 화면 · 팝업 · 창 크기', (tester) async {
MediaKitPreviewPlayer.ensureInitialized();
final dir = testTempDir('jj_fp_');
final srt = p.join(dir.path, 's.srt');
File(srt).writeAsStringSync('1\n00:00:00,000 --> 00:00:05,000\n내장 자막\n');
final ext = p.join(dir.path, 'ep1_ko.srt');
File(ext).writeAsStringSync('1\n00:00:00,000 --> 00:00:05,000\n외부 자막\n');
final files = <String>[];
for (final n in ['ep1.mkv', 'ep2.mkv']) {
final f = p.join(dir.path, n);
final r = await Process.run('ffmpeg', [
'-y', '-f', 'lavfi', '-i', 'testsrc=size=640x360:rate=25', '-f', 'lavfi', '-i', 'sine',
'-i', srt, '-t', '6', '-map', '0', '-map', '1', '-map', '2',
'-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-c:s', 'srt',
'-metadata:s:s:0', 'language=kor', '-metadata:s:s:0', 'title=한국어', f,
]);
expect(r.exitCode, 0, reason: '${r.stderr}');
files.add(f);
}
final pl = MediaKitFullPlayer();
await tester.pumpWidget(MaterialApp(home: Scaffold(body: pl.buildView())));
Future<void> waitFor(bool Function(PlayerState) ok, String what) async {
for (var i = 0; i < 80 && !ok(pl.state.value); i++) {
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 100)));
await tester.pump();
}
expect(ok(pl.state.value), isTrue, reason: what);
}
await tester.runAsync(() => pl.open(files));
await waitFor((s) => s.playing && s.position > const Duration(milliseconds: 300), '재생 시작');
await waitFor((s) => s.width == 640 && s.subtitleTracks.isNotEmpty, '영상 크기 · 내장 자막 트랙');
expect(pl.state.value.subtitleTracks.first.label, contains('한국어'));
// 외부 자막으로 바꾸기 → 끄기
await tester.runAsync(() => pl.setSubtitleTrack(TrackInfo('file:$ext', 'ep1_ko.srt', file: ext)));
expect(pl.state.value.subtitleId, 'file:$ext');
await tester.runAsync(() => pl.setSubtitleTrack(null));
expect(pl.state.value.subtitleId, isNull);
// 음량
await tester.runAsync(() => pl.setVolume(30));
await waitFor((s) => (s.volume - 30).abs() < 1, '음량 30');
// 다음 파일
await tester.runAsync(() => pl.next());
await waitFor((s) => s.index == 1, '다음 파일');
await tester.runAsync(() => pl.add([files.first]));
expect(pl.playlist, hasLength(3));
// 창 모드
final shell = DesktopShell();
await tester.runAsync(() => windowManager.ensureInitialized());
Future<void> settle() => tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 600)));
await tester.runAsync(() => shell.setFullScreen(true));
await settle();
expect(await tester.runAsync(() => shell.isFullScreen()), isTrue);
await tester.runAsync(() => shell.setFullScreen(false));
await settle();
expect(await tester.runAsync(() => shell.isFullScreen()), isFalse);
final before = await tester.runAsync(() => windowManager.getBounds());
await tester.runAsync(() => shell.setPopup(true));
await settle();
final popup = await tester.runAsync(() => windowManager.getBounds());
expect(popup!.width, closeTo(480, 2));
expect(await tester.runAsync(() => windowManager.isAlwaysOnTop()), isTrue);
await tester.runAsync(() => shell.setPopup(false));
await settle();
final after = await tester.runAsync(() => windowManager.getBounds());
expect(after!.width, closeTo(before!.width, 2));
expect(await tester.runAsync(() => windowManager.isAlwaysOnTop()), isFalse);
await tester.runAsync(() => shell.fitVideo(640, 360, 1));
await settle();
final fit = await tester.runAsync(() => windowManager.getSize());
expect(fit!.width, closeTo(640 + 16, 2));
await tester.runAsync(() => pl.dispose());
dir.deleteSync(recursive: true);
}, timeout: const Timeout(Duration(minutes: 3)));
// P2 · 재생 목록 모듈 (관리자 지시 10/21, 28 찾음 - 82a87da 에서 옮김): 실제 mpv 로 재생 화면 - ① 창 폭 412 · 600 · 960 × ko · ja 에서 조절 줄이 넘치지 않고
// (좁으면 두 줄) 끝의 [전체 화면] 이 화면 안 ② mp3 를 고르면 같은 폴더의 다른 곡이 목록에 들고 동영상은 빠진다 ("폴더 전체")
testWidgets('재생 화면 조절 줄 · 음악 재생 목록 (실제 mpv)', (t) async {
MediaKitPreviewPlayer.ensureInitialized();
final out = Platform.environment['JJ_SHOT_DIR'] ?? '';
final dir = testTempDir('jj_fpctl_');
appTempInto(dir);
Future<void> make(String name, List<String> args) async {
final r = await t.runAsync(() => Process.run('ffmpeg', ['-y', '-v', 'error', ...args, p.join(dir.path, name)]));
expect(r!.exitCode, 0, reason: '${r.stderr}');
}
await make('ep1.mkv', ['-f', 'lavfi', '-i', 'testsrc=size=640x360:rate=25', '-f', 'lavfi', '-i', 'sine', '-t', '20', //
'-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-c:a', 'aac']);
for (final (n, hz) in [('Artist - Song A.mp3', 440), ('Other - Track 2.mp3', 550), ('Another title.mp3', 660)]) {
await make(n, ['-f', 'lavfi', '-i', 'sine=frequency=$hz:duration=20', '-c:a', 'libmp3lame']);
}
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool.locate(), storage: DesktopStorageService()));
c.settings.playback.playlistMode = PlaylistMode.folder; // 나눈 설정 (playback)
// ② 재생 목록 모듈: mp3 하나를 고르면 같은 폴더의 곡 셋 (동영상 ep1.mkv 는 빠짐)
final plan = await t.runAsync(() => c.preparePlayback([p.join(dir.path, 'Other - Track 2.mp3')], internal: true));
expect(plan!.$1.map(p.basename).toList(), ['Another title.mp3', 'Artist - Song A.mp3', 'Other - Track 2.mp3']);
expect(plan.$1[plan.$2], endsWith('Other - Track 2.mp3'));
final shotKey = GlobalKey();
for (final (files, start, tag) in [(plan.$1, plan.$2, 'audio'), ([p.join(dir.path, 'ep1.mkv')], 0, 'video')]) {
final pl = MediaKitFullPlayer();
await t.pumpWidget(RepaintBoundary(
key: shotKey,
child: MaterialApp(
theme: buildTheme(), debugShowCheckedModeBanner: false, home: PlayerPage(c: c, player: pl, files: files, start: start)),
));
for (var i = 0; i < 100 && !(pl.state.value.playing && pl.state.value.position > const Duration(milliseconds: 300)); i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 100)));
await t.pump();
}
expect(pl.state.value.playing, isTrue, reason: '$tag 재생 시작');
expect(pl.playlist.length, files.length, reason: '$tag 재생 목록');
for (final lang in ['ko', 'ja']) {
await t.runAsync(() => i18n.apply(lang, save: false));
for (final (w, h) in [(412, 900), (600, 800), (960, 600)]) {
t.view.physicalSize = Size(w.toDouble(), h.toDouble());
t.view.devicePixelRatio = 1;
for (var i = 0; i < 6; i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 80)));
await t.pump();
}
if (out.isNotEmpty) {
final r = shotKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
final img = await t.runAsync(() => r.toImage());
final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png));
Directory(out).createSync(recursive: true);
File(p.join(out, 'player_${tag}_${lang}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
}
expect(t.takeException(), isNull, reason: '$tag $lang $w 넘침');
final full = t.getRect(find.byTooltip(tr('전체 화면 (F)')));
expect(full.right <= w + 0.5, isTrue, reason: '$tag $lang $w: [전체 화면] 이 화면 안 ($full)');
for (final tip in [tr('재생 속도'), tr('자막 (S)'), tr('재생 목록 (L)')]) {
final r = t.getRect(find.byTooltip(tip));
expect(r.right <= w + 0.5, isTrue, reason: '$tag $lang $w: $tip 화면 안 $r');
}
}
}
// PlayerPage 가 닫힐 때 플레이어를 정리한다
await t.pumpWidget(const SizedBox());
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 500)));
}
t.view.reset();
await t.runAsync(() => i18n.apply('ko', save: false));
}, timeout: const Timeout(Duration(minutes: 3)));
}