@joewinke/jatui 0.34.2 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -0
- package/dist/components/ListenControl.svelte +1064 -0
- package/dist/components/ListenControl.svelte.d.ts +87 -0
- package/dist/components/table/DataTable.svelte.d.ts +2 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.js +6 -0
- package/dist/listen/keys.d.ts +36 -0
- package/dist/listen/keys.js +68 -0
- package/dist/listen/mediaSession.d.ts +90 -0
- package/dist/listen/mediaSession.js +108 -0
- package/dist/listen/prefs.d.ts +49 -0
- package/dist/listen/prefs.js +97 -0
- package/dist/listen/segments.d.ts +101 -0
- package/dist/listen/segments.js +224 -0
- package/dist/listen/types.d.ts +81 -0
- package/dist/listen/types.js +7 -0
- package/package.json +1 -1
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ListenControl sequencing — the ONE timeline across per-sentence audio
|
|
3
|
+
* files (jatui-eev2n).
|
|
4
|
+
*
|
|
5
|
+
* A "listen to this page" player is a playlist, not one media element: the
|
|
6
|
+
* host synthesizes each sentence/paragraph separately, so the player walks
|
|
7
|
+
* segments sequentially and reports ONE document-wide time. Every position —
|
|
8
|
+
* the elapsed clock, the progress fill, the chapter ticks, a ±15s skip that
|
|
9
|
+
* crosses a segment boundary, a seek from the slider — maps through
|
|
10
|
+
* `buildTimeline`'s offsets, the same one-axis discipline the VideoPlayer
|
|
11
|
+
* band uses (`video/band/axis.ts`).
|
|
12
|
+
*
|
|
13
|
+
* Pure and DOM-free: safe on garbage input, node-testable.
|
|
14
|
+
*/
|
|
15
|
+
import type { ListenChapter, ListenSegment, ListenSegmentPage, ListenSegmentSource } from './types';
|
|
16
|
+
/** Placeholder width of a segment whose duration is not known yet (pending synth). */
|
|
17
|
+
export declare const DEFAULT_ESTIMATE_SEC = 3;
|
|
18
|
+
/**
|
|
19
|
+
* A "previous sentence" press RESTARTS the current segment when it is already
|
|
20
|
+
* this deep in — past that it means "the one before". Podcast |</<< convention.
|
|
21
|
+
*/
|
|
22
|
+
export declare const SENTENCE_RESTART_SEC = 3;
|
|
23
|
+
/** The document-wide timeline: each segment's start, effective duration and total. */
|
|
24
|
+
export interface ListenTimeline {
|
|
25
|
+
/** Start time of each segment on the one timeline. */
|
|
26
|
+
offsets: number[];
|
|
27
|
+
/** Measured > host-supplied > estimated duration per segment. */
|
|
28
|
+
durations: number[];
|
|
29
|
+
/** `offsets[n] + durations[n]` — the end of the last segment. */
|
|
30
|
+
total: number;
|
|
31
|
+
/** True where the duration is the estimate, not a real one. */
|
|
32
|
+
estimated: boolean[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Is this segment playable right now? A segment exists before its audio does
|
|
36
|
+
* (`url: null` while synthesizing).
|
|
37
|
+
*/
|
|
38
|
+
export declare function isPlayable(segment: ListenSegment | null | undefined): boolean;
|
|
39
|
+
/** First index ≥ `from` with playable audio; -1 when none. */
|
|
40
|
+
export declare function firstPlayableFrom(segments: ListenSegment[], from?: number): number;
|
|
41
|
+
/**
|
|
42
|
+
* The one timeline. An unknown duration borrows `estimateSec` so later
|
|
43
|
+
* segments still get stable offsets (self-correcting as real durations land);
|
|
44
|
+
* `estimated[i]` says which, so the UI can hatch the not-yet-real tail.
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildTimeline(segments: ListenSegment[], estimateSec?: number): ListenTimeline;
|
|
47
|
+
/** Where does document time `t` live? Binary search; `t` past the end clamps to the last segment's end. */
|
|
48
|
+
export declare function locate(t: number, timeline: ListenTimeline): {
|
|
49
|
+
index: number;
|
|
50
|
+
into: number;
|
|
51
|
+
} | null;
|
|
52
|
+
/**
|
|
53
|
+
* Document-time seek: ±delta from `t`, clamped to [0, total]. Returns the
|
|
54
|
+
* segment to load and the offset into it — a skip that lands mid-segment
|
|
55
|
+
* resumes inside it; one that lands past an estimate still resolves (the
|
|
56
|
+
* offset self-corrects when the real duration arrives).
|
|
57
|
+
*/
|
|
58
|
+
export declare function seekTo(t: number, timeline: ListenTimeline): {
|
|
59
|
+
index: number;
|
|
60
|
+
into: number;
|
|
61
|
+
} | null;
|
|
62
|
+
/**
|
|
63
|
+
* Previous / next SENTENCE (the skip-sentence controls and ←/→ keys). Each
|
|
64
|
+
* segment is one sentence, so the move is index±1 — with one exception: deep
|
|
65
|
+
* inside the current segment (past `SENTENCE_RESTART_SEC`) "previous" means
|
|
66
|
+
* "restart this one". Returns the target index, or null at the edge.
|
|
67
|
+
*/
|
|
68
|
+
export declare function jumpSentence(index: number, into: number, count: number, direction: 1 | -1, restartAfterSec?: number): number | null;
|
|
69
|
+
/** A chapter tick for every chapter that owns at least one segment; `frac` is its 0–100 % position. */
|
|
70
|
+
export declare function chapterTicks(chapters: ListenChapter[], segments: ListenSegment[], timeline: ListenTimeline): {
|
|
71
|
+
id: string;
|
|
72
|
+
label: string;
|
|
73
|
+
frac: number;
|
|
74
|
+
}[];
|
|
75
|
+
/** The chapter active at document time `t` — for the bar's section caption. */
|
|
76
|
+
export declare function activeChapter(chapters: ListenChapter[], segments: ListenSegment[], timeline: ListenTimeline, t: number): {
|
|
77
|
+
id: string;
|
|
78
|
+
label: string;
|
|
79
|
+
} | null;
|
|
80
|
+
/**
|
|
81
|
+
* Merge one page into the running list. New ids append in page order; a
|
|
82
|
+
* REPEATED id is an update — the host streaming a later page that fills
|
|
83
|
+
* `url`/`durationSec` in for a segment it announced before. Only defined
|
|
84
|
+
* fields overwrite (a page that re-announces `{id}` alone changes nothing),
|
|
85
|
+
* so late-arriving audio can never blank a known field.
|
|
86
|
+
*/
|
|
87
|
+
export declare function mergePages(existing: ListenSegment[], page: ListenSegmentPage): ListenSegment[];
|
|
88
|
+
/**
|
|
89
|
+
* Normalize the four source shapes (async iterable, sync iterable, Promise of
|
|
90
|
+
* a page, bare page) into one async iteration, so the component consumes a
|
|
91
|
+
* single `for await`. A rejected/throwing source ends the iteration — the
|
|
92
|
+
* component keeps whatever pages landed and flags `done`.
|
|
93
|
+
*/
|
|
94
|
+
export declare function iterateSource(source: ListenSegmentSource, voiceId: string): AsyncGenerator<ListenSegmentPage, void, undefined>;
|
|
95
|
+
/**
|
|
96
|
+
* The contiguous run of NOT-yet-synthesized segments at the end of the list —
|
|
97
|
+
* the common TTS shape (synthesized in order). The progress bar hatches this
|
|
98
|
+
* region to say "more is coming". Returns the start index, or -1 when every
|
|
99
|
+
* trailing segment already has audio.
|
|
100
|
+
*/
|
|
101
|
+
export declare function pendingTailStart(segments: ListenSegment[]): number;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ListenControl sequencing — the ONE timeline across per-sentence audio
|
|
3
|
+
* files (jatui-eev2n).
|
|
4
|
+
*
|
|
5
|
+
* A "listen to this page" player is a playlist, not one media element: the
|
|
6
|
+
* host synthesizes each sentence/paragraph separately, so the player walks
|
|
7
|
+
* segments sequentially and reports ONE document-wide time. Every position —
|
|
8
|
+
* the elapsed clock, the progress fill, the chapter ticks, a ±15s skip that
|
|
9
|
+
* crosses a segment boundary, a seek from the slider — maps through
|
|
10
|
+
* `buildTimeline`'s offsets, the same one-axis discipline the VideoPlayer
|
|
11
|
+
* band uses (`video/band/axis.ts`).
|
|
12
|
+
*
|
|
13
|
+
* Pure and DOM-free: safe on garbage input, node-testable.
|
|
14
|
+
*/
|
|
15
|
+
/** Placeholder width of a segment whose duration is not known yet (pending synth). */
|
|
16
|
+
export const DEFAULT_ESTIMATE_SEC = 3;
|
|
17
|
+
/**
|
|
18
|
+
* A "previous sentence" press RESTARTS the current segment when it is already
|
|
19
|
+
* this deep in — past that it means "the one before". Podcast |</<< convention.
|
|
20
|
+
*/
|
|
21
|
+
export const SENTENCE_RESTART_SEC = 3;
|
|
22
|
+
/**
|
|
23
|
+
* Is this segment playable right now? A segment exists before its audio does
|
|
24
|
+
* (`url: null` while synthesizing).
|
|
25
|
+
*/
|
|
26
|
+
export function isPlayable(segment) {
|
|
27
|
+
return typeof segment?.url === 'string' && segment.url.length > 0;
|
|
28
|
+
}
|
|
29
|
+
/** First index ≥ `from` with playable audio; -1 when none. */
|
|
30
|
+
export function firstPlayableFrom(segments, from = 0) {
|
|
31
|
+
for (let i = Math.max(0, from); i < segments.length; i++)
|
|
32
|
+
if (isPlayable(segments[i]))
|
|
33
|
+
return i;
|
|
34
|
+
return -1;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The one timeline. An unknown duration borrows `estimateSec` so later
|
|
38
|
+
* segments still get stable offsets (self-correcting as real durations land);
|
|
39
|
+
* `estimated[i]` says which, so the UI can hatch the not-yet-real tail.
|
|
40
|
+
*/
|
|
41
|
+
export function buildTimeline(segments, estimateSec = DEFAULT_ESTIMATE_SEC) {
|
|
42
|
+
const est = Number.isFinite(estimateSec) && estimateSec > 0 ? estimateSec : DEFAULT_ESTIMATE_SEC;
|
|
43
|
+
const offsets = [];
|
|
44
|
+
const durations = [];
|
|
45
|
+
const estimated = [];
|
|
46
|
+
let at = 0;
|
|
47
|
+
for (const s of segments) {
|
|
48
|
+
offsets.push(at);
|
|
49
|
+
const d = s.durationSec;
|
|
50
|
+
const known = Number.isFinite(d) && d > 0 ? d : est;
|
|
51
|
+
estimated.push(!(Number.isFinite(d) && d > 0));
|
|
52
|
+
durations.push(known);
|
|
53
|
+
at += known;
|
|
54
|
+
}
|
|
55
|
+
return { offsets, durations, estimated, total: at };
|
|
56
|
+
}
|
|
57
|
+
/** Where does document time `t` live? Binary search; `t` past the end clamps to the last segment's end. */
|
|
58
|
+
export function locate(t, timeline) {
|
|
59
|
+
const n = timeline.offsets.length;
|
|
60
|
+
if (n === 0)
|
|
61
|
+
return null;
|
|
62
|
+
if (!Number.isFinite(t) || t <= 0)
|
|
63
|
+
return { index: 0, into: 0 };
|
|
64
|
+
// Binary search the highest segment whose offset <= t.
|
|
65
|
+
let lo = 0;
|
|
66
|
+
let hi = n - 1;
|
|
67
|
+
while (lo < hi) {
|
|
68
|
+
const mid = (lo + hi + 1) >> 1;
|
|
69
|
+
if (timeline.offsets[mid] <= t)
|
|
70
|
+
lo = mid;
|
|
71
|
+
else
|
|
72
|
+
hi = mid - 1;
|
|
73
|
+
}
|
|
74
|
+
return { index: lo, into: Math.min(t - timeline.offsets[lo], timeline.durations[lo]) };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Document-time seek: ±delta from `t`, clamped to [0, total]. Returns the
|
|
78
|
+
* segment to load and the offset into it — a skip that lands mid-segment
|
|
79
|
+
* resumes inside it; one that lands past an estimate still resolves (the
|
|
80
|
+
* offset self-corrects when the real duration arrives).
|
|
81
|
+
*/
|
|
82
|
+
export function seekTo(t, timeline) {
|
|
83
|
+
const n = timeline.offsets.length;
|
|
84
|
+
if (n === 0)
|
|
85
|
+
return null;
|
|
86
|
+
const clamped = Math.min(Math.max(0, Number.isFinite(t) ? t : 0), timeline.total);
|
|
87
|
+
const at = locate(clamped, timeline);
|
|
88
|
+
return at ? { index: at.index, into: Math.min(at.into, timeline.durations[at.index]) } : null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Previous / next SENTENCE (the skip-sentence controls and ←/→ keys). Each
|
|
92
|
+
* segment is one sentence, so the move is index±1 — with one exception: deep
|
|
93
|
+
* inside the current segment (past `SENTENCE_RESTART_SEC`) "previous" means
|
|
94
|
+
* "restart this one". Returns the target index, or null at the edge.
|
|
95
|
+
*/
|
|
96
|
+
export function jumpSentence(index, into, count, direction, restartAfterSec = SENTENCE_RESTART_SEC) {
|
|
97
|
+
if (count === 0 || !Number.isFinite(index))
|
|
98
|
+
return null;
|
|
99
|
+
const i = Math.min(Math.max(0, Math.floor(index)), count - 1);
|
|
100
|
+
if (direction === 1)
|
|
101
|
+
return i < count - 1 ? i + 1 : null;
|
|
102
|
+
// Backwards: restart the current sentence first when we are already deep in it.
|
|
103
|
+
if (Number.isFinite(into) && into > restartAfterSec)
|
|
104
|
+
return i;
|
|
105
|
+
return i > 0 ? i - 1 : null;
|
|
106
|
+
}
|
|
107
|
+
/** A chapter tick for every chapter that owns at least one segment; `frac` is its 0–100 % position. */
|
|
108
|
+
export function chapterTicks(chapters, segments, timeline) {
|
|
109
|
+
if (timeline.total <= 0 || chapters.length === 0)
|
|
110
|
+
return [];
|
|
111
|
+
const firstOf = new Map();
|
|
112
|
+
for (let i = 0; i < segments.length; i++) {
|
|
113
|
+
const cid = segments[i].chapterId;
|
|
114
|
+
if (cid && !firstOf.has(cid))
|
|
115
|
+
firstOf.set(cid, i);
|
|
116
|
+
}
|
|
117
|
+
const ticks = [];
|
|
118
|
+
for (const c of chapters) {
|
|
119
|
+
const i = firstOf.get(c.id);
|
|
120
|
+
if (i === undefined)
|
|
121
|
+
continue;
|
|
122
|
+
ticks.push({ id: c.id, label: c.label, frac: Math.min(100, (timeline.offsets[i] / timeline.total) * 100) });
|
|
123
|
+
}
|
|
124
|
+
return ticks;
|
|
125
|
+
}
|
|
126
|
+
/** The chapter active at document time `t` — for the bar's section caption. */
|
|
127
|
+
export function activeChapter(chapters, segments, timeline, t) {
|
|
128
|
+
if (chapters.length === 0 || segments.length === 0)
|
|
129
|
+
return null;
|
|
130
|
+
const at = locate(t, timeline);
|
|
131
|
+
if (!at)
|
|
132
|
+
return null;
|
|
133
|
+
// Walk back from the current segment to the nearest one that names a chapter.
|
|
134
|
+
for (let i = at.index; i >= 0; i--) {
|
|
135
|
+
const cid = segments[i].chapterId;
|
|
136
|
+
if (!cid)
|
|
137
|
+
continue;
|
|
138
|
+
const c = chapters.find((x) => x.id === cid);
|
|
139
|
+
if (c)
|
|
140
|
+
return { id: c.id, label: c.label };
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Merge one page into the running list. New ids append in page order; a
|
|
146
|
+
* REPEATED id is an update — the host streaming a later page that fills
|
|
147
|
+
* `url`/`durationSec` in for a segment it announced before. Only defined
|
|
148
|
+
* fields overwrite (a page that re-announces `{id}` alone changes nothing),
|
|
149
|
+
* so late-arriving audio can never blank a known field.
|
|
150
|
+
*/
|
|
151
|
+
export function mergePages(existing, page) {
|
|
152
|
+
const incoming = Array.isArray(page?.segments) ? page.segments : [];
|
|
153
|
+
if (incoming.length === 0)
|
|
154
|
+
return existing;
|
|
155
|
+
const out = existing.slice();
|
|
156
|
+
const byId = new Map();
|
|
157
|
+
out.forEach((s, i) => byId.set(s.id, i));
|
|
158
|
+
for (const raw of incoming) {
|
|
159
|
+
if (!raw || typeof raw.id !== 'string' || raw.id === '')
|
|
160
|
+
continue;
|
|
161
|
+
const i = byId.get(raw.id);
|
|
162
|
+
if (i === undefined) {
|
|
163
|
+
byId.set(raw.id, out.length);
|
|
164
|
+
out.push({ ...raw });
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
const prev = out[i];
|
|
168
|
+
out[i] = {
|
|
169
|
+
...prev,
|
|
170
|
+
...(raw.text !== undefined ? { text: raw.text } : {}),
|
|
171
|
+
...(raw.url !== undefined ? { url: raw.url } : {}),
|
|
172
|
+
...(raw.durationSec !== undefined ? { durationSec: raw.durationSec } : {}),
|
|
173
|
+
...(raw.chapterId !== undefined ? { chapterId: raw.chapterId } : {})
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Normalize the four source shapes (async iterable, sync iterable, Promise of
|
|
181
|
+
* a page, bare page) into one async iteration, so the component consumes a
|
|
182
|
+
* single `for await`. A rejected/throwing source ends the iteration — the
|
|
183
|
+
* component keeps whatever pages landed and flags `done`.
|
|
184
|
+
*/
|
|
185
|
+
export async function* iterateSource(source, voiceId) {
|
|
186
|
+
const result = source(voiceId);
|
|
187
|
+
if (result == null)
|
|
188
|
+
return;
|
|
189
|
+
const asPromise = result;
|
|
190
|
+
if (typeof asPromise.then === 'function') {
|
|
191
|
+
yield await asPromise;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const asAsync = result;
|
|
195
|
+
if (typeof asAsync[Symbol.asyncIterator] === 'function') {
|
|
196
|
+
for await (const page of asAsync) {
|
|
197
|
+
if (page != null)
|
|
198
|
+
yield page;
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const asSync = result;
|
|
203
|
+
if (typeof asSync[Symbol.iterator] === 'function') {
|
|
204
|
+
for (const page of asSync) {
|
|
205
|
+
if (page != null)
|
|
206
|
+
yield page;
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// Bare page.
|
|
211
|
+
yield result;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The contiguous run of NOT-yet-synthesized segments at the end of the list —
|
|
215
|
+
* the common TTS shape (synthesized in order). The progress bar hatches this
|
|
216
|
+
* region to say "more is coming". Returns the start index, or -1 when every
|
|
217
|
+
* trailing segment already has audio.
|
|
218
|
+
*/
|
|
219
|
+
export function pendingTailStart(segments) {
|
|
220
|
+
let i = segments.length;
|
|
221
|
+
while (i > 0 && !isPlayable(segments[i - 1]))
|
|
222
|
+
i--;
|
|
223
|
+
return i < segments.length ? i : -1;
|
|
224
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ListenControl data contracts (jatui-eev2n) — the "listen to this page"
|
|
3
|
+
* playback component. Pure types, DOM-free, node-testable like
|
|
4
|
+
* `video/types.ts`; the component re-exports them so
|
|
5
|
+
* `import type … from 'jatui'` keeps working.
|
|
6
|
+
*/
|
|
7
|
+
/** One listenable unit — a sentence or paragraph, whichever the host splits at. */
|
|
8
|
+
export interface ListenSegment {
|
|
9
|
+
/**
|
|
10
|
+
* Stable id. `onSegmentChange` fires with it as playback crosses segment
|
|
11
|
+
* boundaries; the host keys its highlight + scroll-follow off it.
|
|
12
|
+
*/
|
|
13
|
+
id: string;
|
|
14
|
+
/** The text being read — feeds the bar's caption and aria-live line. */
|
|
15
|
+
text?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Audio for this segment. `null`/`undefined` while the host is still
|
|
18
|
+
* synthesizing it — the player starts on the first chunk that HAS a url
|
|
19
|
+
* and lights the rest in as pages arrive.
|
|
20
|
+
*/
|
|
21
|
+
url?: string | null;
|
|
22
|
+
/**
|
|
23
|
+
* Duration in seconds when the host knows it (TTS usually reports it).
|
|
24
|
+
* Missing durations are measured when the audio loads and ESTIMATED
|
|
25
|
+
* (`estimateSec`) until then, so the progress bar can lay out before
|
|
26
|
+
* every chunk exists.
|
|
27
|
+
*/
|
|
28
|
+
durationSec?: number;
|
|
29
|
+
/** Section (`ListenChapter.id`) this segment belongs to — drives the progress ticks. */
|
|
30
|
+
chapterId?: string;
|
|
31
|
+
}
|
|
32
|
+
/** A chapter/section heading — renders as a tick on the progress bar. */
|
|
33
|
+
export interface ListenChapter {
|
|
34
|
+
id: string;
|
|
35
|
+
label: string;
|
|
36
|
+
}
|
|
37
|
+
/** A synthetic voice the host offers. The list is host-supplied; the player persists the pick. */
|
|
38
|
+
export interface ListenVoice {
|
|
39
|
+
id: string;
|
|
40
|
+
label: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* One page from the async segment source. Pages arrive as chunks finish
|
|
44
|
+
* synthesizing; a page may REPEAT an earlier id to fill its `url` in late
|
|
45
|
+
* (merge-by-id — `mergePages` in `listen/segments.ts`).
|
|
46
|
+
*/
|
|
47
|
+
export interface ListenSegmentPage {
|
|
48
|
+
segments: ListenSegment[];
|
|
49
|
+
/** True on the final page — no more are coming for this voice. */
|
|
50
|
+
done?: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Host-supplied async segment source: ids → audio. Called once per voice
|
|
54
|
+
* (again when the voice changes) and consumed page by page, so playback can
|
|
55
|
+
* start on the first chunk while the rest synthesizes. A plain page, a
|
|
56
|
+
* Promise of one, a sync or async iterable of them all work.
|
|
57
|
+
*/
|
|
58
|
+
export type ListenSegmentSource = (voiceId: string) => AsyncIterable<ListenSegmentPage> | Iterable<ListenSegmentPage> | Promise<ListenSegmentPage> | ListenSegmentPage;
|
|
59
|
+
/** Playback status. `buffering` = the user pressed play but the current segment has no audio yet. */
|
|
60
|
+
export type ListenStatus = 'idle' | 'playing' | 'paused' | 'buffering' | 'ended';
|
|
61
|
+
/**
|
|
62
|
+
* Instance API (bind:this on ListenControl) — the same actions the bar's
|
|
63
|
+
* buttons and keyboard map drive, for hosts wiring their own global keys or
|
|
64
|
+
* external controls.
|
|
65
|
+
*/
|
|
66
|
+
export interface ListenApi {
|
|
67
|
+
play(): void;
|
|
68
|
+
pause(): void;
|
|
69
|
+
toggle(): void;
|
|
70
|
+
next(): void;
|
|
71
|
+
prev(): void;
|
|
72
|
+
/** ±delta seconds on the document-wide timeline. */
|
|
73
|
+
skip(deltaSec: number): void;
|
|
74
|
+
/** Seek to a document-wide time (seconds). */
|
|
75
|
+
seekTo(sec: number): void;
|
|
76
|
+
open(): void;
|
|
77
|
+
close(): void;
|
|
78
|
+
readonly status: ListenStatus;
|
|
79
|
+
/** The segment under the playhead — null once the document finished. */
|
|
80
|
+
readonly currentId: string | null;
|
|
81
|
+
}
|