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

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.8",
3
+ "version": "2.4.0-beta.9",
4
4
  "type": "module",
5
5
  "description": "Next generation SDK for publications in Web Apps",
6
6
  "author": "readium",
@@ -1,4 +1,4 @@
1
- import { Link, Locator, LocatorLocations, Publication } from "@readium/shared";
1
+ import { Link, Locator, LocatorLocations, Publication, Timeline, TimelineItem } from "@readium/shared";
2
2
  import { MediaNavigator, IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../Navigator";
3
3
  import { Configurable } from "../preferences";
4
4
  import { WebAudioEngine, PlaybackState } from "./engine";
@@ -11,7 +11,6 @@ import {
11
11
  IAudioDefaults
12
12
  } from "./preferences";
13
13
  import { AudioPoolManager } from "./AudioPoolManager";
14
- import { AudioTimeline } from "./AudioTimeline";
15
14
  import { ContextMenuEvent, KeyboardEventData, SuspiciousActivityEvent } from "@readium/navigator-html-injectables";
16
15
  import { AudioNavigatorProtector } from "./protection/AudioNavigatorProtector";
17
16
  import { NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT } from "../protection/NavigatorProtector";
@@ -20,6 +19,7 @@ import { KeyboardPeripherals, NAVIGATOR_KEYBOARD_PERIPHERAL_EVENT } from "../per
20
19
  export interface AudioNavigatorListeners {
21
20
  trackLoaded: (media: HTMLMediaElement) => void;
22
21
  positionChanged: (locator: Locator) => void;
22
+ timelineItemChanged: (item: TimelineItem | undefined) => void;
23
23
  error: (error: any, locator: Locator) => void;
24
24
  trackEnded: (locator: Locator) => void;
25
25
  play: (locator: Locator) => void;
@@ -36,6 +36,7 @@ export interface AudioNavigatorListeners {
36
36
  const defaultListeners = (listeners: Partial<AudioNavigatorListeners>): AudioNavigatorListeners => ({
37
37
  trackLoaded: listeners.trackLoaded ?? (() => {}),
38
38
  positionChanged: listeners.positionChanged ?? (() => {}),
39
+ timelineItemChanged: listeners.timelineItemChanged ?? (() => {}),
39
40
  error: listeners.error ?? (() => {}),
40
41
  trackEnded: listeners.trackEnded ?? (() => {}),
41
42
  play: listeners.play ?? (() => {}),
@@ -71,7 +72,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
71
72
  private _mediaSessionEnabled: boolean = false;
72
73
  private pool: AudioPoolManager;
73
74
  private readonly _navigatorProtector: AudioNavigatorProtector | null = null;
74
- private readonly _timeline: AudioTimeline;
75
+ private _currentTimelineItem: TimelineItem | undefined;
75
76
  private readonly _keyboardPeripheralsManager: KeyboardPeripherals | null = null;
76
77
  private readonly _suspiciousActivityListener: ((event: Event) => void) | null = null;
77
78
  private readonly _keyboardPeripheralListener: ((event: Event) => void) | null = null;
@@ -82,7 +83,6 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
82
83
  }) {
83
84
  super();
84
85
  this.pub = publication;
85
- this._timeline = new AudioTimeline(publication);
86
86
  this.listeners = defaultListeners(listeners);
87
87
 
88
88
  this._preferences = new AudioPreferences(configuration.preferences);
@@ -167,7 +167,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
167
167
  this.waitForLoadedAndSeeked(initialTime)
168
168
  .then(() => {
169
169
  this.listeners.trackLoaded(this.pool.audioEngine.getMediaElement());
170
- this._timeline.update(this.currentLocator);
170
+ this._notifyTimelineChange(this.currentLocator);
171
171
  this.listeners.positionChanged(this.currentLocator);
172
172
  })
173
173
  .catch(() => {
@@ -214,8 +214,16 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
214
214
  return this.pub;
215
215
  }
216
216
 
217
- get timeline(): AudioTimeline {
218
- return this._timeline;
217
+ get timeline(): Timeline {
218
+ return this.pub.timeline;
219
+ }
220
+
221
+ private _notifyTimelineChange(locator: Locator): void {
222
+ const item = this.pub.timeline.locate(locator);
223
+ if (item !== this._currentTimelineItem) {
224
+ this._currentTimelineItem = item;
225
+ this.listeners.timelineItemChanged(item);
226
+ }
219
227
  }
220
228
 
221
229
  private ensureLocatorLocations(locator: Locator): Locator {
@@ -365,7 +373,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
365
373
  progression,
366
374
  fragments: [`t=${currentTime}`]
367
375
  }));
368
- this._timeline.update(this.currentLocation);
376
+ this._notifyTimelineChange(this.currentLocation);
369
377
  this.listeners.positionChanged(this.currentLocation);
370
378
  }
371
379
  });
@@ -373,6 +381,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
373
381
  this.pool.audioEngine.on("seeking", () => this.listeners.seeking(true));
374
382
  this.pool.audioEngine.on("waiting", () => this.listeners.seeking(true));
375
383
  this.pool.audioEngine.on("stalled", () => this.listeners.stalled(true));
384
+ this.pool.audioEngine.on("canplaythrough", () => this.listeners.stalled(false));
376
385
  this.pool.audioEngine.on("progress", (seekable: TimeRanges) => this.listeners.seekable(seekable));
377
386
 
378
387
  this.pool.audioEngine.on("loadedmetadata", () => {
@@ -417,7 +426,7 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
417
426
  progression,
418
427
  fragments: [`t=${currentTime}`]
419
428
  }));
420
- this._timeline.update(this.currentLocation);
429
+ this._notifyTimelineChange(this.currentLocation);
421
430
  this.listeners.positionChanged(this.currentLocation);
422
431
  }, this._settings.pollInterval);
423
432
  }
@@ -455,10 +464,13 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
455
464
 
456
465
  await this.waitForLoadedAndSeeked(time, id);
457
466
 
458
- if (id !== this.navigationId) return;
467
+ if (id !== this.navigationId) {
468
+ cb(false);
469
+ return;
470
+ }
459
471
 
460
472
  this.listeners.trackLoaded(this.pool.audioEngine.getMediaElement());
461
- this._timeline.update(this.currentLocator);
473
+ this._notifyTimelineChange(this.currentLocator);
462
474
  this.listeners.positionChanged(this.currentLocator);
463
475
 
464
476
  if (this._settings.enableMediaSession) {
@@ -466,12 +478,13 @@ export class AudioNavigator extends MediaNavigator implements Configurable<Audio
466
478
  }
467
479
 
468
480
  if (wasPlaying) this.play();
469
- this._playIntent = false;
470
481
 
471
482
  cb(true);
472
483
  } catch (error) {
473
484
  console.error("Failed to go to locator:", error);
474
485
  cb(false);
486
+ } finally {
487
+ this._playIntent = false;
475
488
  }
476
489
  }
477
490
 
@@ -1,4 +1,3 @@
1
1
  export * from './engine';
2
2
  export * from './preferences';
3
- export * from './AudioNavigator';
4
- export * from './AudioTimeline';
3
+ export * from './AudioNavigator';
package/src/index.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  export * from './Navigator';
2
- export * from './Timeline';
3
2
  export * from './webpub';
4
3
  export * from './epub';
5
4
  export * from './audio';
@@ -1,12 +1,12 @@
1
- import { Link, Locator, Publication } from "@readium/shared";
1
+ import { Link, Locator, Publication, Timeline, TimelineItem } 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";
6
5
  import { ContextMenuEvent, KeyboardEventData, SuspiciousActivityEvent } from "@readium/navigator-html-injectables";
7
6
  export interface AudioNavigatorListeners {
8
7
  trackLoaded: (media: HTMLMediaElement) => void;
9
8
  positionChanged: (locator: Locator) => void;
9
+ timelineItemChanged: (item: TimelineItem | undefined) => void;
10
10
  error: (error: any, locator: Locator) => void;
11
11
  trackEnded: (locator: Locator) => void;
12
12
  play: (locator: Locator) => void;
@@ -39,7 +39,7 @@ export declare class AudioNavigator extends MediaNavigator implements Configurab
39
39
  private _mediaSessionEnabled;
40
40
  private pool;
41
41
  private readonly _navigatorProtector;
42
- private readonly _timeline;
42
+ private _currentTimelineItem;
43
43
  private readonly _keyboardPeripheralsManager;
44
44
  private readonly _suspiciousActivityListener;
45
45
  private readonly _keyboardPeripheralListener;
@@ -49,7 +49,8 @@ export declare class AudioNavigator extends MediaNavigator implements Configurab
49
49
  submitPreferences(preferences: AudioPreferences): Promise<void>;
50
50
  private applyPreferences;
51
51
  get publication(): Publication;
52
- get timeline(): AudioTimeline;
52
+ get timeline(): Timeline;
53
+ private _notifyTimelineChange;
53
54
  private ensureLocatorLocations;
54
55
  /** Resolves a bare href (no fragment) to its index in the reading order. Returns -1 if not found. */
55
56
  private hrefToTrackIndex;
@@ -1,4 +1,3 @@
1
1
  export * from './engine';
2
2
  export * from './preferences';
3
3
  export * from './AudioNavigator';
4
- export * from './AudioTimeline';
@@ -1,5 +1,4 @@
1
1
  export * from './Navigator';
2
- export * from './Timeline';
3
2
  export * from './webpub';
4
3
  export * from './epub';
5
4
  export * from './audio';
package/src/Timeline.ts DELETED
@@ -1,58 +0,0 @@
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
- }
@@ -1,156 +0,0 @@
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
- }
@@ -1,48 +0,0 @@
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
- }
@@ -1,34 +0,0 @@
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
- }