mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-30 17:40:53 -07:00
fix(overlay): drive timing review playhead from mpv playback position
- Preview session observes time-pos, waits for loaded media before seeking, and acks each command - Forward positions to the modal over media-timing-review:preview-position - Cursor follows the real position, pauses while buffering, and stays at the clip end when done - Replace the fixed end-of-clip timer with a 15s stall timeout - Document preview cursor behavior and ownership in docs
This commit is contained in:
@@ -410,14 +410,17 @@ test('media timing review downloads one window of a remote stream for the wavefo
|
||||
endTime: 14.5,
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(previewStarts, [
|
||||
{
|
||||
mediaPath: '/tmp/window-7.5-14.5.mkv',
|
||||
executablePath: 'mpv',
|
||||
volume: 60,
|
||||
absoluteTimestamps: true,
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(
|
||||
previewStarts.map(({ onPlaybackPosition, ...options }) => options),
|
||||
[
|
||||
{
|
||||
mediaPath: '/tmp/window-7.5-14.5.mkv',
|
||||
executablePath: 'mpv',
|
||||
volume: 60,
|
||||
absoluteTimestamps: true,
|
||||
},
|
||||
],
|
||||
);
|
||||
assert.deepEqual(previewPlays, [['/tmp/window-7.5-14.5.mkv', 9.5, 12.5]]);
|
||||
assert.deepEqual(disposed, ['/tmp/window-7.5-14.5.mkv']);
|
||||
});
|
||||
@@ -512,9 +515,10 @@ test('media timing review falls back to the remote stream after one failed windo
|
||||
audioStreamIndex: 2,
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(previewStarts, [
|
||||
{ mediaPath: REMOTE_STREAM_URL, executablePath: 'mpv', volume: 60, audioTrackId: 3 },
|
||||
]);
|
||||
assert.deepEqual(
|
||||
previewStarts.map(({ onPlaybackPosition, ...options }) => options),
|
||||
[{ mediaPath: REMOTE_STREAM_URL, executablePath: 'mpv', volume: 60, audioTrackId: 3 }],
|
||||
);
|
||||
assert.deepEqual(previewPlays, [[REMOTE_STREAM_URL, 9.5, 12.5]]);
|
||||
});
|
||||
|
||||
@@ -981,7 +985,8 @@ test('disposing owns a preview session whose startup is still pending', async ()
|
||||
|
||||
test('media timing review forwards the hidden player finishing a preview to the modal', async () => {
|
||||
const endedReviewIds: string[] = [];
|
||||
const playback: { ended?: () => void } = {};
|
||||
const positions: Array<{ reviewId: string; time: number }> = [];
|
||||
const playback: { ended?: () => void; position?: (time: number) => void } = {};
|
||||
let publishPayload!: (payload: MediaTimingReviewOpenPayload) => void;
|
||||
const openedPayload = new Promise<MediaTimingReviewOpenPayload>((resolve) => {
|
||||
publishPayload = resolve;
|
||||
@@ -997,7 +1002,9 @@ test('media timing review forwards the hidden player finishing a preview to the
|
||||
getMpvExecutablePath: () => 'mpv',
|
||||
generateWaveform: async () => [],
|
||||
createPreviewSession: () => ({
|
||||
start: async () => undefined,
|
||||
start: async (options) => {
|
||||
playback.position = options.onPlaybackPosition;
|
||||
},
|
||||
play: async () => undefined,
|
||||
stop: async () => undefined,
|
||||
onPlaybackEnded: (listener) => {
|
||||
@@ -1012,6 +1019,7 @@ test('media timing review forwards the hidden player finishing a preview to the
|
||||
onPreviewEnded: (reviewId) => {
|
||||
endedReviewIds.push(reviewId);
|
||||
},
|
||||
onPreviewPosition: (position) => positions.push(position),
|
||||
showStatus: () => undefined,
|
||||
});
|
||||
const pendingDecision = runtime.requestReview({
|
||||
@@ -1031,6 +1039,9 @@ test('media timing review forwards the hidden player finishing a preview to the
|
||||
},
|
||||
);
|
||||
assert.ok(playback.ended);
|
||||
assert.ok(playback.position);
|
||||
playback.position(11);
|
||||
assert.deepEqual(positions, [{ reviewId: payload.reviewId, time: 11 }]);
|
||||
playback.ended();
|
||||
assert.deepEqual(endedReviewIds, [payload.reviewId]);
|
||||
|
||||
@@ -1038,6 +1049,8 @@ test('media timing review forwards the hidden player finishing a preview to the
|
||||
await pendingDecision;
|
||||
playback.ended();
|
||||
assert.deepEqual(endedReviewIds, [payload.reviewId]);
|
||||
playback.position(12);
|
||||
assert.deepEqual(positions, [{ reviewId: payload.reviewId, time: 11 }]);
|
||||
});
|
||||
|
||||
test('preview reports a stale review when the review ends during playback', async () => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
MediaTimingReviewDecision,
|
||||
MediaTimingReviewOpenPayload,
|
||||
MediaTimingReviewPreviewRequest,
|
||||
MediaTimingReviewPreviewPosition,
|
||||
MediaTimingReviewRequest,
|
||||
MediaTimingReviewResolveRequest,
|
||||
MediaTimingReviewFrameRequest,
|
||||
@@ -13,6 +14,7 @@ import type {
|
||||
MediaTimingReviewWaveformResult,
|
||||
} from '../../types/anki';
|
||||
import type { SpeechWaveformOptions } from '../../core/services/media-timing-waveform';
|
||||
import type { MediaTimingPreviewStartOptions } from '../../core/services/media-timing-preview';
|
||||
import type { MediaTimingFrameOptions } from '../../core/services/media-timing-frame';
|
||||
import {
|
||||
isRemoteMediaWindowSourcePath,
|
||||
@@ -36,13 +38,7 @@ interface ReviewMpvClient {
|
||||
}
|
||||
|
||||
interface PreviewSession {
|
||||
start(options: {
|
||||
mediaPath: string;
|
||||
executablePath?: string;
|
||||
audioTrackId?: number;
|
||||
volume?: number;
|
||||
absoluteTimestamps?: boolean;
|
||||
}): Promise<void>;
|
||||
start(options: MediaTimingPreviewStartOptions): Promise<void>;
|
||||
play(startTime: number, endTime: number): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
/** Fires when the player reaches the end of the clip started by play(). */
|
||||
@@ -113,6 +109,7 @@ export interface MediaTimingReviewRuntimeDeps {
|
||||
openModal: (payload: MediaTimingReviewOpenPayload, signal: AbortSignal) => Promise<boolean>;
|
||||
/** Tells the modal that the hidden player finished the previewed clip. */
|
||||
onPreviewEnded?: (reviewId: string) => void;
|
||||
onPreviewPosition?: (position: MediaTimingReviewPreviewPosition) => void;
|
||||
showStatus: (message: string) => void;
|
||||
}
|
||||
|
||||
@@ -354,6 +351,11 @@ export function createMediaTimingReviewRuntime(deps: MediaTimingReviewRuntimeDep
|
||||
}
|
||||
await session.start({
|
||||
mediaPath,
|
||||
onPlaybackPosition: (time) => {
|
||||
if (active === review && review.preview?.session === started) {
|
||||
deps.onPreviewPosition?.({ reviewId: review.payload.reviewId, time });
|
||||
}
|
||||
},
|
||||
...previewOptions,
|
||||
// A cached window keeps one audio stream, so mpv's track id from the source no longer applies.
|
||||
...(window
|
||||
|
||||
Reference in New Issue
Block a user