mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 12:55:16 -07:00
fix autoplay gate to hold pause until subtitle prime and tokenization re
- use pluginRuntimeConfig.autoStart (not effectivePluginRuntimeConfig) so pause-until-ready is preserved when attaching to a background app - await subtitle priming before signaling autoplay readiness - move sub-auto/sid defaults to start-file so they are not overwritten after track load
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
type: fixed
|
||||||
|
area: launcher
|
||||||
|
|
||||||
|
- Kept launcher-opened videos paused when attaching to an already-running background app until subtitle priming and tokenization readiness complete.
|
||||||
|
- Moved mpv plugin subtitle auto-selection to pre-load so launch-time subtitle choices are not reset after the video opens.
|
||||||
@@ -261,7 +261,7 @@ test('plugin auto-start playback attaches a warm background app through the laun
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(calls, ['startMpv', 'startOverlay:--show-visible-overlay --texthooker']);
|
assert.deepEqual(calls, ['startMpv', 'startOverlay:--show-visible-overlay --texthooker']);
|
||||||
assert.equal(receivedStartMpvOptions[0]?.startPaused, false);
|
assert.equal(receivedStartMpvOptions[0]?.startPaused, true);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
(receivedStartMpvOptions[0]?.runtimePluginConfig as { autoStart?: boolean } | undefined)
|
(receivedStartMpvOptions[0]?.runtimePluginConfig as { autoStart?: boolean } | undefined)
|
||||||
?.autoStart,
|
?.autoStart,
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ export async function runPlaybackCommandWithDeps(
|
|||||||
: pluginRuntimeConfig;
|
: pluginRuntimeConfig;
|
||||||
|
|
||||||
const shouldPauseUntilOverlayReady =
|
const shouldPauseUntilOverlayReady =
|
||||||
effectivePluginRuntimeConfig.autoStart &&
|
pluginRuntimeConfig.autoStart &&
|
||||||
pluginRuntimeConfig.autoStartVisibleOverlay &&
|
pluginRuntimeConfig.autoStartVisibleOverlay &&
|
||||||
pluginRuntimeConfig.autoStartPauseUntilReady;
|
pluginRuntimeConfig.autoStartPauseUntilReady;
|
||||||
|
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ async function waitForFile(filePath: string, timeoutMs = 1500): Promise<void> {
|
|||||||
if (fs.existsSync(filePath)) return;
|
if (fs.existsSync(filePath)) return;
|
||||||
await new Promise<void>((resolve) => setTimeout(resolve, 50));
|
await new Promise<void>((resolve) => setTimeout(resolve, 50));
|
||||||
}
|
}
|
||||||
|
throw new Error(`Timed out waiting for file ${filePath} after ${timeoutMs}ms`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startFakeControlServer(
|
async function startFakeControlServer(
|
||||||
@@ -270,10 +271,19 @@ const server = net.createServer((socket) => {
|
|||||||
let buffer = '';
|
let buffer = '';
|
||||||
socket.on('data', (chunk) => {
|
socket.on('data', (chunk) => {
|
||||||
buffer += chunk.toString('utf8');
|
buffer += chunk.toString('utf8');
|
||||||
const line = buffer.split(/\\r?\\n/, 1)[0];
|
let handledLine = false;
|
||||||
if (!line) return;
|
while (true) {
|
||||||
fs.appendFileSync(logPath, line + '\\n');
|
const newlineMatch = buffer.match(/\\r?\\n/);
|
||||||
socket.end(JSON.stringify({ ok: true }) + '\\n');
|
if (!newlineMatch || newlineMatch.index === undefined) break;
|
||||||
|
const line = buffer.slice(0, newlineMatch.index).trim();
|
||||||
|
buffer = buffer.slice(newlineMatch.index + newlineMatch[0].length);
|
||||||
|
if (!line) continue;
|
||||||
|
fs.appendFileSync(logPath, line + '\\n');
|
||||||
|
handledLine = true;
|
||||||
|
}
|
||||||
|
if (handledLine) {
|
||||||
|
socket.end(JSON.stringify({ ok: true }) + '\\n');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -60,17 +60,31 @@ function M.create(ctx)
|
|||||||
return state.auto_start_retry_generation
|
return state.auto_start_retry_generation
|
||||||
end
|
end
|
||||||
|
|
||||||
local function rearm_managed_subtitle_defaults()
|
local function has_matching_subminer_socket()
|
||||||
if not process.has_matching_mpv_ipc_socket(opts.socket_path) then
|
if not process.has_matching_mpv_ipc_socket(opts.socket_path) then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rearm_managed_subtitle_load_defaults()
|
||||||
|
if not has_matching_subminer_socket() then
|
||||||
|
return false
|
||||||
|
end
|
||||||
mp.set_property_native("sub-auto", "fuzzy")
|
mp.set_property_native("sub-auto", "fuzzy")
|
||||||
mp.set_property_native("sid", "auto")
|
mp.set_property_native("sid", "auto")
|
||||||
mp.set_property_native("secondary-sid", "auto")
|
mp.set_property_native("secondary-sid", "auto")
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function refresh_managed_subtitle_autoloading()
|
||||||
|
if not has_matching_subminer_socket() then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
mp.set_property_native("sub-auto", "fuzzy")
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
local function start_overlay_when_socket_ready(generation, media_identity, same_media_loaded, attempt)
|
local function start_overlay_when_socket_ready(generation, media_identity, same_media_loaded, attempt)
|
||||||
if generation ~= state.auto_start_retry_generation then
|
if generation ~= state.auto_start_retry_generation then
|
||||||
return
|
return
|
||||||
@@ -83,7 +97,7 @@ function M.create(ctx)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local has_matching_socket = rearm_managed_subtitle_defaults()
|
local has_matching_socket = refresh_managed_subtitle_autoloading()
|
||||||
if not has_matching_socket then
|
if not has_matching_socket then
|
||||||
if attempt < AUTO_START_SOCKET_RETRY_MAX_ATTEMPTS then
|
if attempt < AUTO_START_SOCKET_RETRY_MAX_ATTEMPTS then
|
||||||
mp.add_timeout(AUTO_START_SOCKET_RETRY_DELAY_SECONDS, function()
|
mp.add_timeout(AUTO_START_SOCKET_RETRY_DELAY_SECONDS, function()
|
||||||
@@ -109,6 +123,13 @@ function M.create(ctx)
|
|||||||
schedule_aniskip_fetch("overlay-start", 0.8)
|
schedule_aniskip_fetch("overlay-start", 0.8)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function on_start_file()
|
||||||
|
if state.pending_reload_media_identity ~= nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
rearm_managed_subtitle_load_defaults()
|
||||||
|
end
|
||||||
|
|
||||||
local function on_file_loaded()
|
local function on_file_loaded()
|
||||||
local media_identity = resolve_media_identity()
|
local media_identity = resolve_media_identity()
|
||||||
local retry_generation = next_auto_start_retry_generation()
|
local retry_generation = next_auto_start_retry_generation()
|
||||||
@@ -151,7 +172,7 @@ function M.create(ctx)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
rearm_managed_subtitle_defaults()
|
refresh_managed_subtitle_autoloading()
|
||||||
schedule_aniskip_fetch("file-loaded", 0)
|
schedule_aniskip_fetch("file-loaded", 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -165,6 +186,7 @@ function M.create(ctx)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function register_lifecycle_hooks()
|
local function register_lifecycle_hooks()
|
||||||
|
mp.register_event("start-file", on_start_file)
|
||||||
mp.register_event("file-loaded", on_file_loaded)
|
mp.register_event("file-loaded", on_file_loaded)
|
||||||
mp.register_event("shutdown", on_shutdown)
|
mp.register_event("shutdown", on_shutdown)
|
||||||
mp.register_event("file-loaded", function()
|
mp.register_event("file-loaded", function()
|
||||||
|
|||||||
@@ -1095,18 +1095,54 @@ do
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
assert_true(recorded ~= nil, "plugin failed to load for subtitle rearm scenario: " .. tostring(err))
|
assert_true(recorded ~= nil, "plugin failed to load for subtitle rearm scenario: " .. tostring(err))
|
||||||
fire_event(recorded, "file-loaded")
|
fire_event(recorded, "start-file")
|
||||||
assert_true(
|
assert_true(
|
||||||
has_property_set(recorded.property_sets, "sub-auto", "fuzzy"),
|
has_property_set(recorded.property_sets, "sub-auto", "fuzzy"),
|
||||||
"managed file-loaded should rearm sub-auto for idle mpv sessions"
|
"managed start-file should rearm sub-auto before mpv loads tracks"
|
||||||
)
|
)
|
||||||
assert_true(
|
assert_true(
|
||||||
has_property_set(recorded.property_sets, "sid", "auto"),
|
has_property_set(recorded.property_sets, "sid", "auto"),
|
||||||
"managed file-loaded should rearm primary subtitle selection for idle mpv sessions"
|
"managed start-file should rearm primary subtitle selection before mpv loads tracks"
|
||||||
)
|
)
|
||||||
assert_true(
|
assert_true(
|
||||||
has_property_set(recorded.property_sets, "secondary-sid", "auto"),
|
has_property_set(recorded.property_sets, "secondary-sid", "auto"),
|
||||||
"managed file-loaded should rearm secondary subtitle selection for idle mpv sessions"
|
"managed start-file should rearm secondary subtitle selection before mpv loads tracks"
|
||||||
|
)
|
||||||
|
fire_event(recorded, "file-loaded")
|
||||||
|
assert_true(
|
||||||
|
count_property_set(recorded.property_sets, "sid", "auto") == 1,
|
||||||
|
"managed file-loaded should not reset primary subtitle selection after mpv loads tracks"
|
||||||
|
)
|
||||||
|
assert_true(
|
||||||
|
count_property_set(recorded.property_sets, "secondary-sid", "auto") == 1,
|
||||||
|
"managed file-loaded should not reset secondary subtitle selection after mpv loads tracks"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local recorded, err = run_plugin_scenario({
|
||||||
|
process_list = "",
|
||||||
|
option_overrides = {
|
||||||
|
binary_path = binary_path,
|
||||||
|
auto_start = "no",
|
||||||
|
socket_path = "/tmp/subminer-socket",
|
||||||
|
},
|
||||||
|
input_ipc_server = "/tmp/subminer-socket",
|
||||||
|
media_title = "Random Movie",
|
||||||
|
files = {
|
||||||
|
[binary_path] = true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert_true(recorded ~= nil, "plugin failed to load for attached subtitle rearm scenario: " .. tostring(err))
|
||||||
|
fire_event(recorded, "start-file")
|
||||||
|
fire_event(recorded, "file-loaded")
|
||||||
|
assert_true(
|
||||||
|
count_property_set(recorded.property_sets, "sid", "auto") == 1,
|
||||||
|
"attached background app path should select primary subtitle before load only"
|
||||||
|
)
|
||||||
|
assert_true(
|
||||||
|
count_property_set(recorded.property_sets, "secondary-sid", "auto") == 1,
|
||||||
|
"attached background app path should select secondary subtitle before load only"
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ import { sendAppControlCommand } from '../../shared/app-control-client';
|
|||||||
import { startAppControlServer } from './app-control-server';
|
import { startAppControlServer } from './app-control-server';
|
||||||
|
|
||||||
async function waitForSocketPath(socketPath: string): Promise<void> {
|
async function waitForSocketPath(socketPath: string): Promise<void> {
|
||||||
const deadline = Date.now() + 1000;
|
const timeoutMs = 1000;
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
if (fs.existsSync(socketPath)) return;
|
if (fs.existsSync(socketPath)) return;
|
||||||
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
||||||
}
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Timed out waiting for control socket ${socketPath} after ${timeoutMs}ms`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
test('app control server dispatches argv requests and replies ok', async () => {
|
test('app control server dispatches argv requests and replies ok', async () => {
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import assert from 'node:assert/strict';
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { createAutoplayTokenizationWarmRelease } from './autoplay-tokenization-warm-release';
|
import { createAutoplayTokenizationWarmRelease } from './autoplay-tokenization-warm-release';
|
||||||
|
|
||||||
|
function flushMicrotasks(): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
|
||||||
test('autoplay tokenization warm release signals immediately when warmups are ready', () => {
|
test('autoplay tokenization warm release signals immediately when warmups are ready', () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const release = createAutoplayTokenizationWarmRelease({
|
const release = createAutoplayTokenizationWarmRelease({
|
||||||
@@ -45,14 +49,17 @@ test('autoplay tokenization warm release primes subtitles before waiting for war
|
|||||||
|
|
||||||
resolveWarmup();
|
resolveWarmup();
|
||||||
await warmup;
|
await warmup;
|
||||||
await Promise.resolve();
|
await flushMicrotasks();
|
||||||
|
|
||||||
assert.deepEqual(calls, ['prime', 'warmup', 'signal']);
|
assert.deepEqual(calls, ['prime', 'warmup', 'signal']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('autoplay tokenization warm release does not await subtitle priming before signaling ready media', async () => {
|
test('autoplay tokenization warm release waits for subtitle priming before signaling ready media', async () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const never = new Promise<void>(() => {});
|
let resolvePrime!: () => void;
|
||||||
|
const prime = new Promise<void>((resolve) => {
|
||||||
|
resolvePrime = resolve;
|
||||||
|
});
|
||||||
const release = createAutoplayTokenizationWarmRelease({
|
const release = createAutoplayTokenizationWarmRelease({
|
||||||
isTokenizationWarmupReady: () => true,
|
isTokenizationWarmupReady: () => true,
|
||||||
startTokenizationWarmups: async () => {
|
startTokenizationWarmups: async () => {
|
||||||
@@ -61,7 +68,7 @@ test('autoplay tokenization warm release does not await subtitle priming before
|
|||||||
getCurrentMediaPath: () => '/tmp/video.mkv',
|
getCurrentMediaPath: () => '/tmp/video.mkv',
|
||||||
primeCurrentSubtitle: () => {
|
primeCurrentSubtitle: () => {
|
||||||
calls.push('prime');
|
calls.push('prime');
|
||||||
return never;
|
return prime;
|
||||||
},
|
},
|
||||||
signalAutoplayReady: () => calls.push('signal'),
|
signalAutoplayReady: () => calls.push('signal'),
|
||||||
warn: () => {},
|
warn: () => {},
|
||||||
@@ -70,6 +77,12 @@ test('autoplay tokenization warm release does not await subtitle priming before
|
|||||||
release('/tmp/video.mkv');
|
release('/tmp/video.mkv');
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
|
|
||||||
|
assert.deepEqual(calls, ['prime']);
|
||||||
|
|
||||||
|
resolvePrime();
|
||||||
|
await prime;
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
assert.deepEqual(calls, ['prime', 'signal']);
|
assert.deepEqual(calls, ['prime', 'signal']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -22,24 +22,41 @@ export function createAutoplayTokenizationWarmRelease(deps: {
|
|||||||
deps.signalAutoplayReady();
|
deps.signalAutoplayReady();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const primeSubtitleForRelease = (mediaPath: string): Promise<void> | null => {
|
||||||
|
if (!deps.primeCurrentSubtitle) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Promise.resolve(deps.primeCurrentSubtitle(mediaPath)).catch((error) => {
|
||||||
|
deps.warn('Startup subtitle priming failed before autoplay readiness release:', error);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
deps.warn('Startup subtitle priming failed before autoplay readiness release:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (mediaPath) => {
|
return (mediaPath) => {
|
||||||
const normalizedPath = normalizeMediaPath(mediaPath);
|
const normalizedPath = normalizeMediaPath(mediaPath);
|
||||||
if (!normalizedPath) {
|
if (!normalizedPath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
const primePromise = primeSubtitleForRelease(normalizedPath);
|
||||||
void Promise.resolve(deps.primeCurrentSubtitle?.(normalizedPath)).catch((error) => {
|
|
||||||
deps.warn('Startup subtitle priming failed before autoplay readiness release:', error);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
deps.warn('Startup subtitle priming failed before autoplay readiness release:', error);
|
|
||||||
}
|
|
||||||
if (deps.isTokenizationWarmupReady()) {
|
if (deps.isTokenizationWarmupReady()) {
|
||||||
signalIfCurrent(normalizedPath);
|
if (!primePromise) {
|
||||||
|
signalIfCurrent(normalizedPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void primePromise.then(() => {
|
||||||
|
signalIfCurrent(normalizedPath);
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void deps
|
const warmupPromise = deps.startTokenizationWarmups();
|
||||||
.startTokenizationWarmups()
|
const readinessPromise = primePromise
|
||||||
|
? Promise.all([primePromise, warmupPromise]).then(() => {})
|
||||||
|
: warmupPromise;
|
||||||
|
void readinessPromise
|
||||||
.then(() => {
|
.then(() => {
|
||||||
signalIfCurrent(normalizedPath);
|
signalIfCurrent(normalizedPath);
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user