125 lines
3.1 KiB
TypeScript
125 lines
3.1 KiB
TypeScript
import type { StoredBeat } from "../infrastructure/storage/lectureStore";
|
|
import { pathToFileUrl } from "../infrastructure/platform/paths";
|
|
|
|
export interface PlayLectureHandle {
|
|
play: () => Promise<void>;
|
|
pause: () => void;
|
|
next: () => Promise<void>;
|
|
prev: () => Promise<void>;
|
|
jumpToBeatId: (beatId: string) => Promise<void>;
|
|
getCurrentBeatId: () => string | undefined;
|
|
isPlaying: () => boolean;
|
|
destroy: () => void;
|
|
setBeats: (beats: StoredBeat[]) => void;
|
|
}
|
|
|
|
export function createPlayLecture(options: {
|
|
audio: HTMLAudioElement;
|
|
beats: StoredBeat[];
|
|
onBeatChange?: (beatId: string | undefined) => void;
|
|
onPlayingChange?: (playing: boolean) => void;
|
|
onComplete?: () => void;
|
|
}): PlayLectureHandle {
|
|
let beats = options.beats.filter((b) => b.ttsStatus === "ready" && b.audioPath);
|
|
let index = 0;
|
|
let playing = false;
|
|
|
|
const notifyBeat = () => {
|
|
options.onBeatChange?.(beats[index]?.id);
|
|
};
|
|
|
|
const loadIndex = (i: number): boolean => {
|
|
if (i < 0 || i >= beats.length) return false;
|
|
index = i;
|
|
const beat = beats[i];
|
|
if (!beat.audioPath) return false;
|
|
options.audio.src = pathToFileUrl(beat.audioPath);
|
|
options.audio.load();
|
|
notifyBeat();
|
|
return true;
|
|
};
|
|
|
|
options.audio.onended = () => {
|
|
void (async () => {
|
|
if (index + 1 < beats.length) {
|
|
index += 1;
|
|
if (loadIndex(index)) {
|
|
try {
|
|
await options.audio.play();
|
|
} catch {
|
|
playing = false;
|
|
options.onPlayingChange?.(false);
|
|
}
|
|
}
|
|
} else {
|
|
playing = false;
|
|
options.onPlayingChange?.(false);
|
|
options.onComplete?.();
|
|
}
|
|
})();
|
|
};
|
|
|
|
return {
|
|
setBeats(next) {
|
|
beats = next.filter((b) => b.ttsStatus === "ready" && b.audioPath);
|
|
if (index >= beats.length) index = Math.max(0, beats.length - 1);
|
|
notifyBeat();
|
|
},
|
|
|
|
async play() {
|
|
if (!beats.length) return;
|
|
if (!options.audio.src || options.audio.paused) {
|
|
loadIndex(index);
|
|
}
|
|
try {
|
|
await options.audio.play();
|
|
playing = true;
|
|
options.onPlayingChange?.(true);
|
|
} catch (e) {
|
|
ztoolkit.log("[ChatPapers:Lecture] audio play failed", e);
|
|
playing = false;
|
|
options.onPlayingChange?.(false);
|
|
}
|
|
},
|
|
|
|
pause() {
|
|
options.audio.pause();
|
|
playing = false;
|
|
options.onPlayingChange?.(false);
|
|
},
|
|
|
|
async next() {
|
|
if (index + 1 >= beats.length) return;
|
|
index += 1;
|
|
if (loadIndex(index)) await this.play();
|
|
},
|
|
|
|
async prev() {
|
|
if (index <= 0) return;
|
|
index -= 1;
|
|
if (loadIndex(index)) await this.play();
|
|
},
|
|
|
|
async jumpToBeatId(beatId: string) {
|
|
const i = beats.findIndex((b) => b.id === beatId);
|
|
if (i < 0) return;
|
|
index = i;
|
|
if (loadIndex(index)) await this.play();
|
|
},
|
|
|
|
getCurrentBeatId() {
|
|
return beats[index]?.id;
|
|
},
|
|
|
|
isPlaying() {
|
|
return playing && !options.audio.paused;
|
|
},
|
|
|
|
destroy() {
|
|
options.audio.onended = null;
|
|
options.audio.pause();
|
|
playing = false;
|
|
},
|
|
};
|
|
}
|