mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-24 12:15:27 -07:00
fix(subtitles): extract embedded subtitle tracks from network-mounted media
The network-mount skip made SMB/NFS libraries fall back to live mpv text for any release shipping subtitles only inside the container, losing karaoke reconstruction, sidebar cues, and mining. The starvation it guarded against had a different cause, and measured extraction runs at wire speed (~10s/GB on gigabit) once per episode. Skip extraction only for true remote URLs, which have no on-disk container to demux, and raise the extraction timeout to cover large Bluray remuxes read over the network.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
type: fixed
|
||||
area: subtitles
|
||||
|
||||
- Prevented embedded subtitle parsing from starving network playback: mounted SMB/NFS media now uses deduplicated mpv live text, while duplicate extraction requests for local media share one ffmpeg process.
|
||||
- Live subtitle text from per-glyph typeset karaoke (network-mounted media without parsed cues) no longer shows a wall of scattered letters in the overlays; the glyph wall and its typed-syllable fragments are suppressed while concurrent dialogue lines remain.
|
||||
- Embedded subtitle tracks on network-mounted (SMB/NFS) media are extracted and parsed again, restoring full karaoke reconstruction, sidebar cues, and mining for releases that ship subtitles only inside the container. Extraction reads the whole file once per episode (roughly 10 seconds per GB on gigabit), its timeout now accommodates large Bluray remuxes, and duplicate extraction requests share one ffmpeg process. Only true remote URLs keep the live-text-only path.
|
||||
- Live subtitle text from per-glyph typeset karaoke no longer shows a wall of scattered letters in the overlays while extraction is still running or when no parsed cues exist (remote URLs, unreadable sources); the glyph wall and its typed-syllable fragments are suppressed while concurrent dialogue lines remain.
|
||||
|
||||
@@ -98,13 +98,15 @@ coming and prefetching would otherwise idle for the rest of the cue.
|
||||
## Secondary Subtitle Flow
|
||||
|
||||
- `secondary-sub-text` remains the immediate fallback, so unreadable subtitle sources, remote URLs,
|
||||
and files on network mounts still appear without waiting for file resolution. Embedded-track
|
||||
extraction is skipped for those sources to avoid competing with playback for network bandwidth.
|
||||
and still-extracting embedded tracks appear without waiting for file resolution. Embedded-track
|
||||
extraction runs for local and network-mounted files alike (demuxing reads the whole container,
|
||||
about 10 seconds per GB on gigabit, under a generous timeout); only true remote URLs skip it,
|
||||
having no on-disk container to demux.
|
||||
- The live fallback also suppresses per-glyph typesetting walls: when many simultaneous
|
||||
one-glyph lines are present (generated karaoke lettering flattened into live text), those
|
||||
lines and their short syllable companions are dropped while concurrent dialogue lines stay.
|
||||
This covers network-mounted media, where embedded-track extraction is skipped and no parsed
|
||||
cues exist to substitute.
|
||||
This keeps the overlay clean while extraction is still in flight and for sources that never
|
||||
produce parsed cues.
|
||||
- Parsed secondary text and the live fallback remove exact repeated lines at any length. A
|
||||
flattened-line identity also removes long dialogue/sign repetitions that differ only in
|
||||
whitespace or terminal punctuation, while distinct simultaneous short lines remain separate.
|
||||
|
||||
@@ -2059,7 +2059,6 @@ const cachedInternalSubtitleTrackExtractor = createCachedInternalSubtitleTrackEx
|
||||
const detectRemoteMediaPath = createRemoteMediaPathDetector();
|
||||
const resolveActiveSubtitleSidebarSourceHandler = createResolveActiveSubtitleSidebarSourceHandler({
|
||||
getFfmpegPath: () => configService.getConfig().subsync.ffmpeg_path.trim() || 'ffmpeg',
|
||||
isRemoteMediaPath: detectRemoteMediaPath,
|
||||
extractInternalSubtitleTrack: (ffmpegPath, videoPath, track) =>
|
||||
cachedInternalSubtitleTrackExtractor.extract(ffmpegPath, videoPath, track),
|
||||
logDebug: (message) => logger.debug(message),
|
||||
|
||||
@@ -864,15 +864,16 @@ test('subtitle sidebar snapshot prefers cached YouTube parsed cues before active
|
||||
);
|
||||
});
|
||||
|
||||
test('main process guards internal subtitle extraction with the remote media detector', () => {
|
||||
test('main process extracts internal subtitle tracks without a network-mount guard', () => {
|
||||
const source = readMainSource();
|
||||
const resolverWiring = source.match(
|
||||
/const resolveActiveSubtitleSidebarSourceHandler = createResolveActiveSubtitleSidebarSourceHandler\(\{(?<body>[\s\S]*?)\n\}\);/,
|
||||
)?.groups?.body;
|
||||
|
||||
assert.ok(resolverWiring);
|
||||
assert.match(source, /const detectRemoteMediaPath = createRemoteMediaPathDetector\(\);/);
|
||||
assert.match(resolverWiring, /isRemoteMediaPath:\s*detectRemoteMediaPath/);
|
||||
// Network-mounted files are extracted like local ones; only remote URLs skip
|
||||
// extraction, handled inside the resolver itself.
|
||||
assert.doesNotMatch(resolverWiring, /isRemoteMediaPath/);
|
||||
assert.match(
|
||||
resolverWiring,
|
||||
/extractInternalSubtitleTrack:[\s\S]*cachedInternalSubtitleTrackExtractor\.extract/,
|
||||
|
||||
@@ -46,7 +46,10 @@ export type InternalSubtitleTrackExtractor = (
|
||||
track: MpvSubtitleTrackLike,
|
||||
) => Promise<ExtractedInternalSubtitleTrack | null>;
|
||||
|
||||
const DEFAULT_EXTRACTION_TIMEOUT_MS = 30_000;
|
||||
// Subtitle packets are interleaved through the container, so extraction reads the
|
||||
// entire file. Network mounts move ~100 MB/s on gigabit, so large Bluray remuxes
|
||||
// need well over 30 seconds.
|
||||
const DEFAULT_EXTRACTION_TIMEOUT_MS = 120_000;
|
||||
|
||||
export function parseTrackId(value: unknown): number | null {
|
||||
if (typeof value === 'number' && Number.isInteger(value) && value >= 0) {
|
||||
|
||||
@@ -157,14 +157,16 @@ test('subtitle prefetch runtime does not extract internal subtitle tracks from r
|
||||
assert.equal(extracted, false);
|
||||
});
|
||||
|
||||
test('subtitle prefetch runtime does not extract internal subtitle tracks from network mounts', async () => {
|
||||
test('subtitle prefetch runtime extracts internal subtitle tracks from network-mounted media', async () => {
|
||||
let extracted = false;
|
||||
const resolveSource = createResolveActiveSubtitleSidebarSourceHandler({
|
||||
getFfmpegPath: () => 'ffmpeg-custom',
|
||||
isRemoteMediaPath: async (videoPath) => videoPath.startsWith('/Volumes/jellyfin/'),
|
||||
extractInternalSubtitleTrack: async () => {
|
||||
extracted = true;
|
||||
return null;
|
||||
return {
|
||||
path: '/tmp/subminer-sidebar-123/track_7.ass',
|
||||
cleanup: async () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -181,8 +183,8 @@ test('subtitle prefetch runtime does not extract internal subtitle tracks from n
|
||||
videoPath: '/Volumes/jellyfin/movie.mkv',
|
||||
});
|
||||
|
||||
assert.equal(resolved, null);
|
||||
assert.equal(extracted, false);
|
||||
assert.equal(resolved?.path, '/tmp/subminer-sidebar-123/track_7.ass');
|
||||
assert.equal(extracted, true);
|
||||
});
|
||||
|
||||
test('subtitle prefetch refresh logs a warning when source resolution throws', async () => {
|
||||
|
||||
@@ -17,8 +17,6 @@ type ActiveSubtitleSidebarSource = {
|
||||
cleanup?: () => Promise<void>;
|
||||
};
|
||||
|
||||
type RemoteMediaPathDetector = (mediaPath: string) => boolean | Promise<boolean>;
|
||||
|
||||
function parseTrackId(value: unknown): number | null {
|
||||
if (typeof value === 'number' && Number.isInteger(value)) {
|
||||
return value;
|
||||
@@ -88,7 +86,6 @@ function getActiveSubtitleTrack(
|
||||
|
||||
export function createResolveActiveSubtitleSidebarSourceHandler(deps: {
|
||||
getFfmpegPath: () => string;
|
||||
isRemoteMediaPath?: RemoteMediaPathDetector;
|
||||
extractInternalSubtitleTrack: (
|
||||
ffmpegPath: string,
|
||||
videoPath: string,
|
||||
@@ -129,8 +126,10 @@ export function createResolveActiveSubtitleSidebarSourceHandler(deps: {
|
||||
return { path: externalFilename, sourceKey: externalFilename };
|
||||
}
|
||||
|
||||
const isRemoteMediaPath = deps.isRemoteMediaPath ?? isRemoteMediaUrl;
|
||||
if (await isRemoteMediaPath(input.videoPath)) {
|
||||
// Network-mounted files extract like local ones: demuxing reads the whole
|
||||
// container (~10s/GB on gigabit), which a LAN handles alongside playback.
|
||||
// Only true remote URLs have no on-disk container to demux.
|
||||
if (isRemoteMediaUrl(input.videoPath)) {
|
||||
deps.logDebug?.('[subtitle-prefetch] skipping internal subtitle extraction for remote media');
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user