Fix overlapping audio when explaining multi-line PDF selections.

Use exclusive playback sessions so popup and pane do not play simultaneously, and stop standalone audio when the lecture pane is closed.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yhy
2026-08-29 23:23:16 +08:00
co-authored by Cursor
parent 913e3675ae
commit 0fcc662464
2 changed files with 36 additions and 8 deletions
@@ -136,6 +136,14 @@ export async function playSelectionExplainStandalone(
await playSelectionExplainExclusive(result); await playSelectionExplainExclusive(result);
} }
export function pauseStandalonePlayback(): void {
standaloneAudio?.pause();
if (standaloneAudio) {
standaloneAudio.onended = null;
standaloneAudio.removeAttribute("src");
}
}
export function stopSelectionExplainStandalone(): void { export function stopSelectionExplainStandalone(): void {
stopAllLecturePlayback(); stopAllLecturePlayback();
} }
+28 -8
View File
@@ -50,9 +50,9 @@ import {
import { import {
beginSelectionExplainSession, beginSelectionExplainSession,
isSelectionExplainSessionActive, isSelectionExplainSessionActive,
playSelectionExplainAudio,
playSelectionExplainExclusive, playSelectionExplainExclusive,
registerLecturePlaybackHandle, registerLecturePlaybackHandle,
pauseStandalonePlayback,
stopAllLecturePlayback, stopAllLecturePlayback,
} from "../application/playSelectionExplain"; } from "../application/playSelectionExplain";
import { getString } from "../../../utils/locale"; import { getString } from "../../../utils/locale";
@@ -264,10 +264,23 @@ export class LecturePaneView {
this.updateActionButton(); this.updateActionButton();
this.bindEvents(); this.bindEvents();
this.unregisterPlayback = registerLecturePlaybackHandle({
pause: () => {
this.paragraphPlayer?.pause();
this.player?.pause();
this.audioEl?.pause();
},
});
await this.refreshFromStore(); await this.refreshFromStore();
} }
destroy(): void { destroy(): void {
this.unregisterPlayback?.();
this.unregisterPlayback = undefined;
this.paragraphPlayer?.pause();
this.player?.pause();
this.audioEl?.pause();
pauseStandalonePlayback();
for (const off of this.unsubscribers) off(); for (const off of this.unsubscribers) off();
this.unsubscribers = []; this.unsubscribers = [];
this.player?.destroy(); this.player?.destroy();
@@ -313,7 +326,7 @@ export class LecturePaneView {
}), }),
lectureEvents.on("lecture:selection_explain", (d) => { lectureEvents.on("lecture:selection_explain", (d) => {
if (String(getRegularItem(this.item).id) !== d.itemId) return; if (String(getRegularItem(this.item).id) !== d.itemId) return;
void this.applySelectionExplainResult(d.result); void this.applySelectionExplainResult(d.result, { play: false });
}), }),
); );
} }
@@ -828,6 +841,8 @@ export class LecturePaneView {
this.explainingSelection = true; this.explainingSelection = true;
this.explainSelectionBtn.disabled = true; this.explainSelectionBtn.disabled = true;
this.explainSessionId = beginSelectionExplainSession();
const sessionId = this.explainSessionId;
this.setStatus("summarizing", getString("lecture-selection-explaining")); this.setStatus("summarizing", getString("lecture-selection-explaining"));
try { try {
@@ -836,7 +851,9 @@ export class LecturePaneView {
selectedText, selectedText,
onProgress: (message) => this.setStatus("summarizing", message), onProgress: (message) => this.setStatus("summarizing", message),
}); });
await this.applySelectionExplainResult(result); if (!isSelectionExplainSessionActive(sessionId)) return;
await this.applySelectionExplainResult(result, { play: true });
if (!isSelectionExplainSessionActive(sessionId)) return;
this.setStatus("ready", getString("lecture-selection-done")); this.setStatus("ready", getString("lecture-selection-done"));
} catch (e) { } catch (e) {
const msg = e instanceof Error ? e.message : String(e); const msg = e instanceof Error ? e.message : String(e);
@@ -849,12 +866,17 @@ export class LecturePaneView {
private async applySelectionExplainResult( private async applySelectionExplainResult(
result: SelectionExplainResult, result: SelectionExplainResult,
options?: { play?: boolean },
): Promise<void> { ): Promise<void> {
this.lastSelectionResult = result; this.lastSelectionResult = result;
this.renderSelectionExplain(result); this.renderSelectionExplain(result);
await this.syncPdfHighlight(result.original, result.page); await this.syncPdfHighlight(result.original, result.page);
if (options?.play === false) return;
stopAllLecturePlayback();
if (result.fromPrepared && result.matched) { if (result.fromPrepared && result.matched) {
if (!this.paragraphPlayer) { if (!this.paragraphPlayer) {
await this.refreshPlayer(); await this.refreshPlayer();
@@ -881,12 +903,10 @@ export class LecturePaneView {
} }
if (!this.audioEl) return; if (!this.audioEl) return;
this.paragraphPlayer?.pause();
this.player?.pause();
this.playBtn.hidden = true; this.playBtn.hidden = true;
this.pauseBtn.hidden = false; this.pauseBtn.hidden = false;
try { try {
await playSelectionExplainAudio(this.audioEl, result); await playSelectionExplainExclusive(result, this.audioEl);
} catch (e) { } catch (e) {
ztoolkit.log("[ChatPapers:Lecture] selection audio failed", e); ztoolkit.log("[ChatPapers:Lecture] selection audio failed", e);
} finally { } finally {
@@ -938,8 +958,8 @@ export class LecturePaneView {
this.doc.createTextNode(getString("lecture-selection-replay")), this.doc.createTextNode(getString("lecture-selection-replay")),
); );
replayBtn.addEventListener("click", () => { replayBtn.addEventListener("click", () => {
if (!this.audioEl || !this.lastSelectionResult) return; if (!this.lastSelectionResult) return;
void playSelectionExplainAudio(this.audioEl, this.lastSelectionResult); void playSelectionExplainExclusive(this.lastSelectionResult, this.audioEl);
}); });
this.selectionExplainEl.append(replayBtn); this.selectionExplainEl.append(replayBtn);
} }