186 lines
9.5 KiB
Dart
186 lines
9.5 KiB
Dart
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)));
|
||
}
|