fix(player): smooth timed lyrics highlighting

This commit is contained in:
zarzet
2026-08-23 15:06:46 +07:00
parent c6437d8428
commit 73051a5feb
4 changed files with 692 additions and 36 deletions
+21 -3
View File
@@ -24,11 +24,14 @@ void main() {
}
final arguments = (call.arguments as Map).cast<String, dynamic>();
final path = arguments['file_path']?.toString() ?? '';
final lyrics = path.endsWith('/timed.flac')
? '''<tt xmlns="http://www.w3.org/ns/ttml"><body><div><p begin="00:00.000" end="00:02.000"><span begin="00:00.000">Short</span></p></div></body></tt>'''
: path.endsWith('/second.flac')
? '[00:01.00]Second lyric'
: '[00:01.00]First lyric';
return jsonEncode({
'title': path.endsWith('/second.flac') ? 'Second' : 'First',
'lyrics': path.endsWith('/second.flac')
? '[00:01.00]Second lyric'
: '[00:01.00]First lyric',
'lyrics': lyrics,
});
});
});
@@ -89,6 +92,21 @@ void main() {
},
);
testWidgets('short timed lyric remains horizontally centered', (
tester,
) async {
await pumpNowPlaying(tester);
mediaItems.add(item('timed'));
await tester.pumpAndSettle();
await tester.drag(find.byType(PageView), const Offset(-700, 0));
await tester.pumpAndSettle();
final lyric = find.bySemanticsLabel('Short');
expect(lyric, findsOneWidget);
expect(tester.getCenter(lyric).dx, closeTo(540, 1));
});
testWidgets('Now Playing menu exposes Go to Album when album is known', (
tester,
) async {
+109
View File
@@ -27,4 +27,113 @@ void main() {
24,
);
});
test('advances at the exact next line boundary', () {
const starts = [
Duration(seconds: 1),
Duration(seconds: 3),
Duration(seconds: 3),
Duration(seconds: 6),
];
expect(
syncedLyricsDueLineIndex(
lineStarts: starts,
currentIndex: 0,
position: const Duration(milliseconds: 2999),
),
0,
);
expect(
syncedLyricsDueLineIndex(
lineStarts: starts,
currentIndex: 0,
position: const Duration(seconds: 3),
),
2,
);
});
group('smooth timed lyric highlight', () {
test('interpolates position only while playback is advancing', () {
expect(
interpolatedSyncedLyricsPosition(
anchorPosition: const Duration(seconds: 10),
elapsedSinceAnchor: const Duration(milliseconds: 250),
isPlaying: true,
),
const Duration(milliseconds: 10250),
);
expect(
interpolatedSyncedLyricsPosition(
anchorPosition: const Duration(seconds: 10),
elapsedSinceAnchor: const Duration(milliseconds: 250),
isPlaying: false,
),
const Duration(seconds: 10),
);
});
test('blends small clock corrections and applies seeks immediately', () {
expect(
reconcileSyncedLyricsPosition(
predictedPosition: const Duration(milliseconds: 1000),
reportedPosition: const Duration(milliseconds: 1100),
),
const Duration(milliseconds: 1025),
);
expect(
reconcileSyncedLyricsPosition(
predictedPosition: const Duration(seconds: 1),
reportedPosition: const Duration(seconds: 5),
),
const Duration(seconds: 5),
);
});
test('calculates continuous progress inside a timed segment', () {
const start = Duration(seconds: 2);
const end = Duration(seconds: 3);
expect(
syncedLyricSegmentProgress(
position: const Duration(milliseconds: 1500),
start: start,
end: end,
),
0,
);
expect(
syncedLyricSegmentProgress(
position: const Duration(milliseconds: 2250),
start: start,
end: end,
),
0.25,
);
expect(
syncedLyricSegmentProgress(
position: const Duration(milliseconds: 3500),
start: start,
end: end,
),
1,
);
});
test('moves the reveal boundary from left to right', () {
expect(
syncedLyricsLeftToRightBoundary(left: 10, right: 110, progress: 0),
10,
);
expect(
syncedLyricsLeftToRightBoundary(left: 10, right: 110, progress: 0.5),
60,
);
expect(
syncedLyricsLeftToRightBoundary(left: 10, right: 110, progress: 1),
110,
);
});
});
}