@readium/navigator 2.4.0-beta.6 → 2.4.0-beta.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@readium/navigator",
3
- "version": "2.4.0-beta.6",
3
+ "version": "2.4.0-beta.8",
4
4
  "type": "module",
5
5
  "description": "Next generation SDK for publications in Web Apps",
6
6
  "author": "readium",
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Represents a single entry in the publication's timeline.
3
+ * The timeline contextualizes the reading order, table of contents,
4
+ * positions list, and guided navigation document (GND) so that
5
+ * consuming apps can, for instance, display position numbers
6
+ * for TOC items, select the current TOC entry when
7
+ * it has a fragment, or display previous/next as chapter titles while
8
+ * pointing to the actual resources.
9
+ *
10
+ * Properties vary by format — not all will be populated for every format.
11
+ */
12
+ export interface TimelineItem {
13
+ /** Title of this timeline entry (e.g. chapter or section name). */
14
+ title: string;
15
+ /** References as hrefs with optional fragments, e.g. ["chapter1.html"], ["track1.mp3#t=60"], ["#page=6"]. */
16
+ references: string[];
17
+ /** Roles of this entry, e.g. ["chapter"], ["section"], ["part"]. */
18
+ role?: string[];
19
+ /** Position number in the reading order context. */
20
+ position?: number;
21
+ /** Scroll progression within the resource (0 to 1), for entries that start mid-way in a resource. */
22
+ scroll?: number;
23
+ /** Nested timeline entries. */
24
+ children?: TimelineItem[];
25
+ }
26
+
27
+ export type TimelineChangeCallback = (current: TimelineItem | undefined, previous: TimelineItem | undefined, next: TimelineItem | undefined) => void;
28
+
29
+ /**
30
+ * A timeline built from a publication's structure.
31
+ * Format-specific subclasses are responsible for building the items
32
+ * from the publication's reading order, table of contents, positions list and GND.
33
+ */
34
+ export abstract class Timeline {
35
+ abstract readonly items: TimelineItem[];
36
+
37
+ /** The current timeline item matching the navigator's position. */
38
+ abstract get current(): TimelineItem | undefined;
39
+
40
+ /** The previous entry relative to current, based on resource boundaries in the TOC tree. */
41
+ abstract get previous(): TimelineItem | undefined;
42
+
43
+ /** The next entry relative to current, based on resource boundaries in the TOC tree. */
44
+ abstract get next(): TimelineItem | undefined;
45
+
46
+ /** Returns the previous and next entries relative to current. */
47
+ get adjacent(): { previous: TimelineItem | undefined; next: TimelineItem | undefined } {
48
+ return { previous: this.previous, next: this.next };
49
+ }
50
+
51
+ protected changeCallback: TimelineChangeCallback | null = null;
52
+
53
+ /** Register a callback invoked when the current timeline item changes. */
54
+ onChange(callback: TimelineChangeCallback): void {
55
+ this.changeCallback = callback;
56
+ }
57
+
58
+ }
@@ -11,6 +11,7 @@ import {
11
11
  IAudioDefaults
12
12
  } from "./preferences";
13
13
  import { AudioPoolManager } from "./AudioPoolManager";
14
+ import { AudioTimeline } from "./AudioTimeline";
14
15
  import { ContextMenuEvent, KeyboardEventData, SuspiciousActivityEvent } from "@readium/navigator-html-injectables";
15
16
  import { AudioNavigatorProtector } from "./protection/AudioNavigatorProtector";
16
17
  import { NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT } from "../protection/NavigatorProtector";
@@ -59,6 +60,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
59
60
  private readonly pub: Publication;
60
61
  private positionPollInterval: ReturnType<typeof setInterval> | null = null;
61
62
  private navigationId: number = 0;
63
+ private _playIntent: boolean = false;
62
64
  private listeners: AudioNavigatorListeners;
63
65
  private currentLocation!: Locator;
64
66
 
@@ -69,6 +71,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
69
71
  private _mediaSessionEnabled: boolean = false;
70
72
  private pool: AudioPoolManager;
71
73
  private readonly _navigatorProtector: AudioNavigatorProtector | null = null;
74
+ private readonly _timeline: AudioTimeline;
72
75
  private readonly _keyboardPeripheralsManager: KeyboardPeripherals | null = null;
73
76
  private readonly _suspiciousActivityListener: ((event: Event) => void) | null = null;
74
77
  private readonly _keyboardPeripheralListener: ((event: Event) => void) | null = null;
@@ -79,6 +82,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
79
82
  }) {
80
83
  super();
81
84
  this.pub = publication;
85
+ this._timeline = new AudioTimeline(publication);
82
86
  this.listeners = defaultListeners(listeners);
83
87
 
84
88
  this._preferences = new AudioPreferences(configuration.preferences);
@@ -163,6 +167,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
163
167
  this.waitForLoadedAndSeeked(initialTime)
164
168
  .then(() => {
165
169
  this.listeners.trackLoaded(this.pool.audioEngine.getMediaElement());
170
+ this._timeline.update(this.currentLocator);
166
171
  this.listeners.positionChanged(this.currentLocator);
167
172
  })
168
173
  .catch(() => {
@@ -209,6 +214,10 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
209
214
  return this.pub;
210
215
  }
211
216
 
217
+ get timeline(): AudioTimeline {
218
+ return this._timeline;
219
+ }
220
+
212
221
  private ensureLocatorLocations(locator: Locator): Locator {
213
222
  return new Locator({
214
223
  ...locator,
@@ -356,6 +365,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
356
365
  progression,
357
366
  fragments: [`t=${currentTime}`]
358
367
  }));
368
+ this._timeline.update(this.currentLocation);
359
369
  this.listeners.positionChanged(this.currentLocation);
360
370
  }
361
371
  });
@@ -407,6 +417,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
407
417
  progression,
408
418
  fragments: [`t=${currentTime}`]
409
419
  }));
420
+ this._timeline.update(this.currentLocation);
410
421
  this.listeners.positionChanged(this.currentLocation);
411
422
  }, this._settings.pollInterval);
412
423
  }
@@ -432,7 +443,11 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
432
443
 
433
444
  const id = ++this.navigationId;
434
445
  const direction: "forward" | "backward" = trackIndex >= this.currentTrackIndex() ? "forward" : "backward";
435
- const wasPlaying = this.isPlaying;
446
+ // Use _playIntent rather than isPlaying — setMediaElement resets the
447
+ // engine's playing flag, so a rapid second go() would see false and
448
+ // never resume playback.
449
+ const wasPlaying = this.isPlaying || this._playIntent;
450
+ this._playIntent = wasPlaying;
436
451
 
437
452
  this.stopPositionPolling();
438
453
  this.pool.setCurrentAudio(trackIndex, direction);
@@ -443,6 +458,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
443
458
  if (id !== this.navigationId) return;
444
459
 
445
460
  this.listeners.trackLoaded(this.pool.audioEngine.getMediaElement());
461
+ this._timeline.update(this.currentLocator);
446
462
  this.listeners.positionChanged(this.currentLocator);
447
463
 
448
464
  if (this._settings.enableMediaSession) {
@@ -450,6 +466,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
450
466
  }
451
467
 
452
468
  if (wasPlaying) this.play();
469
+ this._playIntent = false;
453
470
 
454
471
  cb(true);
455
472
  } catch (error) {
@@ -59,6 +59,11 @@ export class AudioPoolManager {
59
59
  if (!element) {
60
60
  element = document.createElement("audio");
61
61
  element.preload = "auto";
62
+ // When Web Audio is active CORS already succeeded, so preload
63
+ // with crossOrigin to avoid a destructive reload at swap time.
64
+ if (this._audioEngine.isWebAudioActive) {
65
+ element.crossOrigin = "anonymous";
66
+ }
62
67
  element.src = href;
63
68
  element.load();
64
69
  this.pool.set(href, element);
@@ -105,16 +110,7 @@ export class AudioPoolManager {
105
110
  const href = this.pickPlayableHref(this._publication.readingOrder.items[currentIndex]);
106
111
  const element = this.ensure(href);
107
112
 
108
- // Hand the element to the engine. When Web Audio is active, the pooled
109
- // element doesn't have crossOrigin set (it would break non-CORS servers
110
- // during preload), so we swap in the fresh element and let loadAudio
111
- // handle CORS setup + fallback on the engine's own mediaElement.
112
- if (this.audioEngine.isWebAudioActive) {
113
- this.audioEngine.setMediaElement(element);
114
- this.audioEngine.loadAudio(href);
115
- } else {
116
- this.audioEngine.setMediaElement(element);
117
- }
113
+ this.audioEngine.setMediaElement(element);
118
114
 
119
115
  // Remove from pool so the engine fully owns it and we don't dispose it
120
116
  this.pool.delete(href);
@@ -0,0 +1,156 @@
1
+ import { Link, Locator, Publication } from "@readium/shared";
2
+ import { Timeline, TimelineItem } from "../Timeline";
3
+
4
+ /**
5
+ * Audio-specific timeline built from the publication's table of contents,
6
+ * falling back to the reading order when no TOC is available.
7
+ */
8
+ export class AudioTimeline extends Timeline {
9
+ readonly items: TimelineItem[];
10
+
11
+ private _current: TimelineItem | undefined;
12
+ private _previous: TimelineItem | undefined;
13
+ private _next: TimelineItem | undefined;
14
+ private readonly flat: TimelineItem[];
15
+
16
+ constructor(publication: Publication) {
17
+ super();
18
+ const toc = publication.toc;
19
+ this.items = toc && toc.items.length > 0
20
+ ? AudioTimeline.itemsFromToc(toc.items)
21
+ : AudioTimeline.itemsFromReadingOrder(publication.readingOrder.items);
22
+ this.flat = this.flatten(this.items);
23
+ }
24
+
25
+ get current(): TimelineItem | undefined {
26
+ return this._current;
27
+ }
28
+
29
+ get previous(): TimelineItem | undefined {
30
+ return this._previous;
31
+ }
32
+
33
+ get next(): TimelineItem | undefined {
34
+ return this._next;
35
+ }
36
+
37
+ update(locator: Locator): void {
38
+ const href = locator.href.split("#")[0];
39
+ const time = locator.locations?.time() ?? 0;
40
+
41
+ const matched = this.findCurrent(this.items, href, time);
42
+
43
+ // Only fire callback when timeline values actually change
44
+ if (matched !== this._current) {
45
+ this._current = matched;
46
+ this._previous = matched ? this.findPrevious(matched) : undefined;
47
+ this._next = matched ? this.findNext(matched) : undefined;
48
+ this.changeCallback?.(this._current, this._previous, this._next);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Finds the deepest timeline item whose reference matches the given
54
+ * href and whose time offset is at or before the current time.
55
+ */
56
+ private findCurrent(_items: TimelineItem[], href: string, time: number): TimelineItem | undefined {
57
+ // Search for time-based match first
58
+ let match: TimelineItem | undefined;
59
+
60
+ for (const item of this.flat) {
61
+ if (this.itemMatchesPosition(item, href, time)) {
62
+ match = item;
63
+ }
64
+ }
65
+
66
+ // If no time-based match, fallback to base href match
67
+ if (!match) {
68
+ for (const item of this.flat) {
69
+ if (this.bareHref(item) === href) {
70
+ match = item;
71
+ break;
72
+ }
73
+ }
74
+ }
75
+
76
+ return match;
77
+ }
78
+
79
+ /** First preceding entry in the flat list */
80
+ private findPrevious(target: TimelineItem): TimelineItem | undefined {
81
+ const index = this.flat.indexOf(target);
82
+ if (index > 0) {
83
+ return this.flat[index - 1];
84
+ }
85
+ return undefined;
86
+ }
87
+
88
+ /** First following entry in the flat list */
89
+ private findNext(target: TimelineItem): TimelineItem | undefined {
90
+ const index = this.flat.indexOf(target);
91
+ if (index < this.flat.length - 1) {
92
+ return this.flat[index + 1];
93
+ }
94
+ return undefined;
95
+ }
96
+
97
+ private bareHref(item: TimelineItem): string {
98
+ const ref = item.references[0];
99
+ if (!ref) return "";
100
+ return ref.split("#")[0];
101
+ }
102
+
103
+ private flatten(items: TimelineItem[]): TimelineItem[] {
104
+ const result: TimelineItem[] = [];
105
+ for (const item of items) {
106
+ result.push(item);
107
+ if (item.children) {
108
+ result.push(...this.flatten(item.children));
109
+ }
110
+ }
111
+ return result;
112
+ }
113
+
114
+ private itemMatchesPosition(item: TimelineItem, href: string, time: number): boolean {
115
+ for (const ref of item.references) {
116
+ const [refHref, refFragment] = ref.split("#");
117
+ const refBare = refHref || href; // empty refHref means same resource (e.g. "#t=60")
118
+ if (refBare !== href) continue;
119
+ const refTime = this.parseTimeFragment(refFragment);
120
+ if (time >= refTime) return true;
121
+ }
122
+ return false;
123
+ }
124
+
125
+ private parseTimeFragment(fragment: string | undefined): number {
126
+ if (!fragment) return 0;
127
+ const match = fragment.match(/(?:^|&)t=(\d+(?:\.\d+)?)/);
128
+ return match ? parseFloat(match[1]) : 0;
129
+ }
130
+
131
+ private static itemsFromToc(links: Link[]): TimelineItem[] {
132
+ return links
133
+ .map(link => AudioTimeline.linkToItem(link))
134
+ .filter(Boolean) as TimelineItem[];
135
+ }
136
+
137
+ private static itemsFromReadingOrder(links: Link[]): TimelineItem[] {
138
+ return links
139
+ .map(link => AudioTimeline.linkToItem(link))
140
+ .filter(Boolean) as TimelineItem[];
141
+ }
142
+
143
+ private static linkToItem(link: Link): TimelineItem | undefined {
144
+ if (!link.title) return undefined;
145
+
146
+ const children = link.children?.items
147
+ .map(child => AudioTimeline.linkToItem(child))
148
+ .filter(Boolean) as TimelineItem[] | undefined;
149
+
150
+ return {
151
+ title: link.title,
152
+ references: [link.href],
153
+ children: children && children.length > 0 ? children : undefined,
154
+ };
155
+ }
156
+ }
@@ -40,12 +40,6 @@ export interface AudioEngine {
40
40
  */
41
41
  playback: Playback;
42
42
 
43
- /**
44
- * Loads the audio resource at the given URL.
45
- * @param url The URL of the audio resource.
46
- */
47
- loadAudio(url: string): void;
48
-
49
43
  /**
50
44
  * Adds an event listener to the audio engine.
51
45
  * @param event The event name to listen.
@@ -95,52 +95,6 @@ export class WebAudioEngine implements AudioEngine {
95
95
  );
96
96
  }
97
97
 
98
- /**
99
- * Load the audio resource at the given URL.
100
- * @param url The URL of the audio resource.
101
- * */
102
- public loadAudio(url: string): void {
103
- // Abort any in-progress load before starting a new one.
104
- this.mediaElement.pause();
105
- this.mediaElement.removeAttribute("src");
106
- this.mediaElement.load();
107
-
108
- this.isLoadingValue = true;
109
- this.isLoadedValue = false;
110
- this.isPlayingValue = false;
111
- this.isPausedValue = false;
112
-
113
- if (this.webAudioActive) {
114
- this.mediaElement.crossOrigin = "anonymous";
115
- this.mediaElement.src = url;
116
- this.mediaElement.load();
117
- this.mediaElement.playbackRate = this.currentPlaybackRate;
118
-
119
- // If the server doesn't honour the CORS preflight, fall back to a
120
- // non-CORS load and tear down the Web Audio graph so the element
121
- // is never passed to MediaElementAudioSourceNode in a tainted state.
122
- const cleanup = () => {
123
- this.mediaElement.removeEventListener("error", onCORSError);
124
- this.mediaElement.removeEventListener("canplaythrough", onCORSSuccess);
125
- };
126
- const onCORSError = () => {
127
- cleanup();
128
- this.deactivateWebAudio();
129
- this.mediaElement.removeAttribute("crossOrigin");
130
- this.mediaElement.src = url;
131
- this.mediaElement.load();
132
- this.mediaElement.playbackRate = this.currentPlaybackRate;
133
- };
134
- const onCORSSuccess = () => cleanup();
135
- this.mediaElement.addEventListener("error", onCORSError);
136
- this.mediaElement.addEventListener("canplaythrough", onCORSSuccess);
137
- } else {
138
- this.mediaElement.src = url;
139
- this.mediaElement.load();
140
- this.mediaElement.playbackRate = this.currentPlaybackRate;
141
- }
142
- }
143
-
144
98
  private deactivateWebAudio(): void {
145
99
  if (this.worklet) {
146
100
  this.worklet.destroy();
@@ -162,18 +116,7 @@ export class WebAudioEngine implements AudioEngine {
162
116
  * @param element The HTML audio element to use.
163
117
  */
164
118
  public setMediaElement(element: HTMLAudioElement): void {
165
- // Pause the outgoing element before replacing it
166
- this.mediaElement.pause();
167
- this.isPlayingValue = false;
168
- this.isPausedValue = false;
169
-
170
- // Disconnect old source node if it exists
171
- if (this.sourceNode) {
172
- this.sourceNode.disconnect();
173
- this.sourceNode = null;
174
- }
175
-
176
- // Remove old event listeners from current mediaElement
119
+ // Remove listeners BEFORE pausing so the pause doesn't leak through
177
120
  this.mediaElement.removeEventListener("canplaythrough", this.boundOnCanPlayThrough);
178
121
  this.mediaElement.removeEventListener("timeupdate", this.boundOnTimeUpdate);
179
122
  this.mediaElement.removeEventListener("error", this.boundOnError);
@@ -190,6 +133,17 @@ export class WebAudioEngine implements AudioEngine {
190
133
  this.mediaElement.removeEventListener("pause", this.boundOnPause);
191
134
  this.mediaElement.removeEventListener("progress", this.boundOnProgress);
192
135
 
136
+ // Now safe to pause the outgoing element
137
+ this.mediaElement.pause();
138
+ this.isPlayingValue = false;
139
+ this.isPausedValue = false;
140
+
141
+ // Disconnect old source node if it exists
142
+ if (this.sourceNode) {
143
+ this.sourceNode.disconnect();
144
+ this.sourceNode = null;
145
+ }
146
+
193
147
  // Set new media element
194
148
  this.mediaElement = element;
195
149
 
@@ -214,6 +168,26 @@ export class WebAudioEngine implements AudioEngine {
214
168
  this.mediaElement.volume = this.isMutedValue ? 0 : this.currentVolume;
215
169
  this.mediaElement.playbackRate = this.currentPlaybackRate;
216
170
 
171
+ // Reconnect the Web Audio graph to the new element
172
+ if (this.webAudioActive) {
173
+ try {
174
+ const ctx = this.getOrCreateAudioContext();
175
+ this.sourceNode = new MediaElementAudioSourceNode(ctx, { mediaElement: this.mediaElement });
176
+ if (!this.gainNode) {
177
+ this.gainNode = ctx.createGain();
178
+ this.gainNode.connect(ctx.destination);
179
+ }
180
+ if (this.worklet?.workletNode) {
181
+ this.sourceNode.connect(this.worklet.workletNode);
182
+ } else {
183
+ this.sourceNode.connect(this.gainNode);
184
+ }
185
+ } catch {
186
+ // CORS failed on this element — deactivate Web Audio gracefully
187
+ this.deactivateWebAudio();
188
+ }
189
+ }
190
+
217
191
  // Check if metadata is already loaded (common with preloaded elements)
218
192
  if (this.mediaElement.readyState >= 1) {
219
193
  this.onLoadedMetadata(new Event('loadedmetadata'));
@@ -1,3 +1,4 @@
1
1
  export * from './engine';
2
2
  export * from './preferences';
3
- export * from './AudioNavigator';
3
+ export * from './AudioNavigator';
4
+ export * from './AudioTimeline';
@@ -85,7 +85,7 @@ export class FrameManager {
85
85
  }
86
86
 
87
87
  // Apply print protection if configured
88
- if (this.contentProtectionConfig.protectPrinting) {
88
+ if (this.contentProtectionConfig.protectPrinting?.disable) {
89
89
  this.comms!.send("print_protection", this.contentProtectionConfig.protectPrinting);
90
90
  }
91
91
  }
@@ -219,7 +219,7 @@ export class FXLFrameManager {
219
219
  }
220
220
 
221
221
  // Apply print protection if configured
222
- if (this.contentProtectionConfig.protectPrinting) {
222
+ if (this.contentProtectionConfig.protectPrinting?.disable) {
223
223
  this.comms!.send("print_protection", this.contentProtectionConfig.protectPrinting);
224
224
  }
225
225
  }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './Navigator';
2
+ export * from './Timeline';
2
3
  export * from './webpub';
3
4
  export * from './epub';
4
5
  export * from './audio';
@@ -81,7 +81,7 @@ export const volumeRangeConfig: RangeConfig = {
81
81
  }
82
82
 
83
83
  export const playbackRateRangeConfig: RangeConfig = {
84
- range: [0.5, 2],
84
+ range: [0.5, 4],
85
85
  step: 0.1
86
86
  }
87
87
 
@@ -282,6 +282,7 @@ export class DevToolsDetector {
282
282
  // Cleanup Web Worker
283
283
  if (this.workerConsole) {
284
284
  this.workerConsole.destroy();
285
+ this.workerConsole = undefined;
285
286
  }
286
287
 
287
288
  this.isOpen = false;
@@ -83,7 +83,7 @@ export class WebPubFrameManager {
83
83
  }
84
84
 
85
85
  // Apply print protection if configured
86
- if (this.contentProtectionConfig.protectPrinting) {
86
+ if (this.contentProtectionConfig.protectPrinting?.disable) {
87
87
  this.comms!.send("print_protection", this.contentProtectionConfig.protectPrinting);
88
88
  }
89
89
  }
@@ -128,8 +128,12 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
128
128
 
129
129
  // Listen for custom events from NavigatorProtector
130
130
  this._suspiciousActivityListener = (event: Event) => {
131
- const customEvent = event as CustomEvent;
132
- this.listeners.contentProtection(customEvent.detail.type, customEvent.detail);
131
+ const { type, ...activity } = (event as CustomEvent).detail;
132
+ if (type === "context_menu") {
133
+ this.listeners.contextMenu(activity as ContextMenuEvent);
134
+ } else {
135
+ this.listeners.contentProtection(type, activity);
136
+ }
133
137
  };
134
138
  window.addEventListener(NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT, this._suspiciousActivityListener);
135
139
  }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Represents a single entry in the publication's timeline.
3
+ * The timeline contextualizes the reading order, table of contents,
4
+ * positions list, and guided navigation document (GND) so that
5
+ * consuming apps can, for instance, display position numbers
6
+ * for TOC items, select the current TOC entry when
7
+ * it has a fragment, or display previous/next as chapter titles while
8
+ * pointing to the actual resources.
9
+ *
10
+ * Properties vary by format — not all will be populated for every format.
11
+ */
12
+ export interface TimelineItem {
13
+ /** Title of this timeline entry (e.g. chapter or section name). */
14
+ title: string;
15
+ /** References as hrefs with optional fragments, e.g. ["chapter1.html"], ["track1.mp3#t=60"], ["#page=6"]. */
16
+ references: string[];
17
+ /** Roles of this entry, e.g. ["chapter"], ["section"], ["part"]. */
18
+ role?: string[];
19
+ /** Position number in the reading order context. */
20
+ position?: number;
21
+ /** Scroll progression within the resource (0 to 1), for entries that start mid-way in a resource. */
22
+ scroll?: number;
23
+ /** Nested timeline entries. */
24
+ children?: TimelineItem[];
25
+ }
26
+ export type TimelineChangeCallback = (current: TimelineItem | undefined, previous: TimelineItem | undefined, next: TimelineItem | undefined) => void;
27
+ /**
28
+ * A timeline built from a publication's structure.
29
+ * Format-specific subclasses are responsible for building the items
30
+ * from the publication's reading order, table of contents, positions list and GND.
31
+ */
32
+ export declare abstract class Timeline {
33
+ abstract readonly items: TimelineItem[];
34
+ /** The current timeline item matching the navigator's position. */
35
+ abstract get current(): TimelineItem | undefined;
36
+ /** The previous entry relative to current, based on resource boundaries in the TOC tree. */
37
+ abstract get previous(): TimelineItem | undefined;
38
+ /** The next entry relative to current, based on resource boundaries in the TOC tree. */
39
+ abstract get next(): TimelineItem | undefined;
40
+ /** Returns the previous and next entries relative to current. */
41
+ get adjacent(): {
42
+ previous: TimelineItem | undefined;
43
+ next: TimelineItem | undefined;
44
+ };
45
+ protected changeCallback: TimelineChangeCallback | null;
46
+ /** Register a callback invoked when the current timeline item changes. */
47
+ onChange(callback: TimelineChangeCallback): void;
48
+ }
@@ -2,6 +2,7 @@ import { Link, Locator, Publication } from "@readium/shared";
2
2
  import { MediaNavigator, IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../Navigator";
3
3
  import { Configurable } from "../preferences";
4
4
  import { AudioPreferences, AudioSettings, AudioPreferencesEditor, IAudioPreferences, IAudioDefaults } from "./preferences";
5
+ import { AudioTimeline } from "./AudioTimeline";
5
6
  import { ContextMenuEvent, KeyboardEventData, SuspiciousActivityEvent } from "@readium/navigator-html-injectables";
6
7
  export interface AudioNavigatorListeners {
7
8
  trackLoaded: (media: HTMLMediaElement) => void;
@@ -28,6 +29,7 @@ export declare class AudioNavigator extends MediaNavigator implements Configurab
28
29
  private readonly pub;
29
30
  private positionPollInterval;
30
31
  private navigationId;
32
+ private _playIntent;
31
33
  private listeners;
32
34
  private currentLocation;
33
35
  private _preferences;
@@ -37,6 +39,7 @@ export declare class AudioNavigator extends MediaNavigator implements Configurab
37
39
  private _mediaSessionEnabled;
38
40
  private pool;
39
41
  private readonly _navigatorProtector;
42
+ private readonly _timeline;
40
43
  private readonly _keyboardPeripheralsManager;
41
44
  private readonly _suspiciousActivityListener;
42
45
  private readonly _keyboardPeripheralListener;
@@ -46,6 +49,7 @@ export declare class AudioNavigator extends MediaNavigator implements Configurab
46
49
  submitPreferences(preferences: AudioPreferences): Promise<void>;
47
50
  private applyPreferences;
48
51
  get publication(): Publication;
52
+ get timeline(): AudioTimeline;
49
53
  private ensureLocatorLocations;
50
54
  /** Resolves a bare href (no fragment) to its index in the reading order. Returns -1 if not found. */
51
55
  private hrefToTrackIndex;
@@ -0,0 +1,34 @@
1
+ import { Locator, Publication } from "@readium/shared";
2
+ import { Timeline, TimelineItem } from "../Timeline";
3
+ /**
4
+ * Audio-specific timeline built from the publication's table of contents,
5
+ * falling back to the reading order when no TOC is available.
6
+ */
7
+ export declare class AudioTimeline extends Timeline {
8
+ readonly items: TimelineItem[];
9
+ private _current;
10
+ private _previous;
11
+ private _next;
12
+ private readonly flat;
13
+ constructor(publication: Publication);
14
+ get current(): TimelineItem | undefined;
15
+ get previous(): TimelineItem | undefined;
16
+ get next(): TimelineItem | undefined;
17
+ update(locator: Locator): void;
18
+ /**
19
+ * Finds the deepest timeline item whose reference matches the given
20
+ * href and whose time offset is at or before the current time.
21
+ */
22
+ private findCurrent;
23
+ /** First preceding entry in the flat list */
24
+ private findPrevious;
25
+ /** First following entry in the flat list */
26
+ private findNext;
27
+ private bareHref;
28
+ private flatten;
29
+ private itemMatchesPosition;
30
+ private parseTimeFragment;
31
+ private static itemsFromToc;
32
+ private static itemsFromReadingOrder;
33
+ private static linkToItem;
34
+ }