fix(player): drop chapter-derived intro markers longer than three minutes
A movie whose first chapter is titled "Opening Credits" or "Introduction" got a Skip Intro button that skipped the whole chapter — several minutes of picture. Detected intros are short; only Plezy's own chapter-title fallback produced these. Cap chapter-derived intros at three minutes. Server-supplied markers and credits chapters are unaffected. close #2235
This commit is contained in:
@@ -376,11 +376,22 @@ class PlaybackExtras {
|
||||
return RegExp(source, caseSensitive: false);
|
||||
}
|
||||
|
||||
/// Longest chapter that may become a chapter-derived intro marker.
|
||||
///
|
||||
/// Detected intros (Plex, Intro Skipper) fall well inside two minutes; a
|
||||
/// movie's first chapter titled "Opening Credits" or "Introduction" runs
|
||||
/// five to ten minutes of actual picture, and skipping it skips the film
|
||||
/// (#2235). Applies only to markers minted here from chapter titles, never
|
||||
/// to server-supplied markers, and never to credits, which are legitimately
|
||||
/// long on movies.
|
||||
static const maxChapterIntroDuration = Duration(minutes: 3);
|
||||
|
||||
/// Returns [PlaybackExtras] using real markers when available, filling any
|
||||
/// missing marker types from chapter titles matching intro/credits patterns.
|
||||
/// [forceChapterFallback] prefers chapter-derived markers for any type they
|
||||
/// provide. When real markers exist, reclassifies markers with unknown types
|
||||
/// against the patterns so non-standard type strings get recognized.
|
||||
/// Chapter-derived intros longer than [maxChapterIntroDuration] are dropped.
|
||||
factory PlaybackExtras.withChapterFallback({
|
||||
required List<MediaChapter> chapters,
|
||||
required List<MediaMarker> markers,
|
||||
@@ -411,6 +422,7 @@ class PlaybackExtras {
|
||||
|
||||
final end = ch.endTimeOffset ?? (i + 1 < chapters.length ? chapters[i + 1].startTimeOffset : null);
|
||||
if (end == null) continue;
|
||||
if (type == 'intro' && end - start > maxChapterIntroDuration.inMilliseconds) continue;
|
||||
|
||||
synthetic.add(MediaMarker(id: ch.id, type: type, startTimeOffset: start, endTimeOffset: end));
|
||||
}
|
||||
|
||||
@@ -169,4 +169,85 @@ void main() {
|
||||
expect(extras.markers.single.startTimeOffset, 0);
|
||||
});
|
||||
});
|
||||
|
||||
group('PlaybackExtras.withChapterFallback intro duration cap', () {
|
||||
const cap = PlaybackExtras.maxChapterIntroDuration;
|
||||
const movieEnd = 7200000;
|
||||
|
||||
test('a movie-length "Opening Credits" chapter does not become an intro', () {
|
||||
final extras = PlaybackExtras.withChapterFallback(
|
||||
chapters: [
|
||||
MediaChapter(id: 1, startTimeOffset: 0, endTimeOffset: 480000, title: 'Opening Credits'),
|
||||
MediaChapter(id: 2, startTimeOffset: 480000, endTimeOffset: movieEnd, title: 'Chapter 2'),
|
||||
],
|
||||
markers: [],
|
||||
);
|
||||
|
||||
expect(extras.markers, isEmpty);
|
||||
});
|
||||
|
||||
test('a chapter exactly at the cap still qualifies; one millisecond over does not', () {
|
||||
final atCap = PlaybackExtras.withChapterFallback(
|
||||
chapters: [
|
||||
MediaChapter(id: 1, startTimeOffset: 1000, endTimeOffset: 1000 + cap.inMilliseconds, title: 'Intro'),
|
||||
],
|
||||
markers: [],
|
||||
);
|
||||
final overCap = PlaybackExtras.withChapterFallback(
|
||||
chapters: [
|
||||
MediaChapter(id: 1, startTimeOffset: 1000, endTimeOffset: 1001 + cap.inMilliseconds, title: 'Intro'),
|
||||
],
|
||||
markers: [],
|
||||
);
|
||||
|
||||
expect(atCap.markers.map((m) => m.type), ['intro']);
|
||||
expect(overCap.markers, isEmpty);
|
||||
});
|
||||
|
||||
test('the cap uses the next chapter start when the chapter has no end', () {
|
||||
final extras = PlaybackExtras.withChapterFallback(
|
||||
chapters: [
|
||||
MediaChapter(id: 1, startTimeOffset: 0, title: 'Opening'),
|
||||
MediaChapter(id: 2, startTimeOffset: 600000, title: 'Chapter 2'),
|
||||
],
|
||||
markers: [],
|
||||
);
|
||||
|
||||
expect(extras.markers, isEmpty);
|
||||
});
|
||||
|
||||
test('long credits chapters are still derived', () {
|
||||
final extras = PlaybackExtras.withChapterFallback(
|
||||
chapters: [
|
||||
MediaChapter(id: 1, startTimeOffset: 0, endTimeOffset: 6600000, title: 'Chapter 1'),
|
||||
MediaChapter(id: 2, startTimeOffset: 6600000, endTimeOffset: movieEnd, title: 'End Credits'),
|
||||
],
|
||||
markers: [],
|
||||
);
|
||||
|
||||
expect(extras.markers.map((m) => m.type), ['credits']);
|
||||
expect(extras.markers.single.endTimeOffset, movieEnd);
|
||||
});
|
||||
|
||||
test('server-supplied intro markers are never capped', () {
|
||||
final extras = PlaybackExtras.withChapterFallback(
|
||||
chapters: [],
|
||||
markers: [MediaMarker(id: 1, type: 'intro', startTimeOffset: 0, endTimeOffset: 600000)],
|
||||
);
|
||||
|
||||
expect(extras.markers.map((m) => m.type), ['intro']);
|
||||
expect(extras.markers.single.endTimeOffset, 600000);
|
||||
});
|
||||
|
||||
test('a dropped chapter intro leaves native markers intact under forced fallback', () {
|
||||
final extras = PlaybackExtras.withChapterFallback(
|
||||
chapters: [MediaChapter(id: 1, startTimeOffset: 0, endTimeOffset: 480000, title: 'Opening Credits')],
|
||||
markers: [MediaMarker(id: 9, type: 'credits', startTimeOffset: 6600000, endTimeOffset: movieEnd)],
|
||||
forceChapterFallback: true,
|
||||
);
|
||||
|
||||
expect(extras.markers.map((m) => m.type), ['credits']);
|
||||
expect(extras.markers.single.id, 9);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user