@readium/navigator 2.4.0-alpha.8 → 2.4.0-beta.1
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/dist/index.js +1809 -1055
- package/dist/index.umd.cjs +170 -23
- package/package.json +10 -10
- package/src/Navigator.ts +55 -1
- package/src/audio/AudioNavigator.ts +497 -0
- package/src/audio/AudioPoolManager.ts +120 -0
- package/src/audio/engine/AudioEngine.ts +26 -10
- package/src/audio/engine/PreservePitchProcessor.js +149 -0
- package/src/audio/engine/PreservePitchWorklet.ts +79 -0
- package/src/audio/engine/WebAudioEngine.ts +558 -259
- package/src/audio/index.ts +3 -1
- package/src/audio/preferences/AudioDefaults.ts +43 -0
- package/src/audio/preferences/AudioPreferences.ts +54 -0
- package/src/audio/preferences/AudioPreferencesEditor.ts +123 -0
- package/src/audio/preferences/AudioSettings.ts +36 -0
- package/src/audio/preferences/index.ts +4 -0
- package/src/epub/EpubNavigator.ts +2 -2
- package/src/epub/frame/FrameBlobBuilder.ts +33 -84
- package/src/epub/frame/FramePoolManager.ts +23 -18
- package/src/epub/fxl/FXLFrameManager.ts +4 -11
- package/src/epub/fxl/FXLFramePoolManager.ts +22 -26
- package/src/epub/preferences/EpubPreferences.ts +4 -4
- package/src/injection/Injector.ts +5 -5
- package/src/preferences/Configurable.ts +2 -3
- package/src/preferences/PreferencesEditor.ts +1 -1
- package/src/preferences/Types.ts +19 -0
- package/src/webpub/WebPubNavigator.ts +1 -2
- package/src/webpub/preferences/WebPubPreferences.ts +3 -3
- package/types/src/Navigator.d.ts +46 -0
- package/types/src/audio/AudioNavigator.d.ts +79 -0
- package/types/src/audio/AudioPoolManager.d.ts +52 -0
- package/types/src/audio/engine/AudioEngine.d.ts +21 -7
- package/types/src/audio/engine/PreservePitchWorklet.d.ts +18 -0
- package/types/src/audio/engine/WebAudioEngine.d.ts +52 -7
- package/types/src/audio/index.d.ts +2 -0
- package/types/src/audio/preferences/AudioDefaults.d.ts +21 -0
- package/types/src/audio/preferences/AudioPreferences.d.ts +23 -0
- package/types/src/audio/preferences/AudioPreferencesEditor.d.ts +19 -0
- package/types/src/audio/preferences/AudioSettings.d.ts +24 -0
- package/types/src/audio/preferences/index.d.ts +4 -0
- package/types/src/epub/EpubNavigator.d.ts +2 -2
- package/types/src/epub/frame/FrameBlobBuilder.d.ts +3 -6
- package/types/src/epub/fxl/FXLFrameManager.d.ts +0 -2
- package/types/src/epub/preferences/EpubPreferences.d.ts +2 -2
- package/types/src/preferences/Configurable.d.ts +2 -3
- package/types/src/preferences/PreferencesEditor.d.ts +1 -1
- package/types/src/preferences/Types.d.ts +3 -0
- package/types/src/webpub/WebPubNavigator.d.ts +2 -2
- package/types/src/webpub/preferences/WebPubPreferences.d.ts +2 -2
- package/LICENSE +0 -28
- package/src/divina/DivinaNavigator.ts +0 -0
- package/src/divina/index.ts +0 -0
- package/types/src/divina/DivinaNavigator.d.ts +0 -0
- package/types/src/divina/index.d.ts +0 -0
package/src/audio/index.ts
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ensureBoolean,
|
|
3
|
+
ensureValueInRange,
|
|
4
|
+
ensureNonNegative
|
|
5
|
+
} from "../../preferences/guards";
|
|
6
|
+
import {
|
|
7
|
+
volumeRangeConfig,
|
|
8
|
+
playbackRateRangeConfig,
|
|
9
|
+
skipIntervalRangeConfig
|
|
10
|
+
} from "../../preferences/Types";
|
|
11
|
+
|
|
12
|
+
export interface IAudioDefaults {
|
|
13
|
+
volume?: number | null;
|
|
14
|
+
playbackRate?: number | null;
|
|
15
|
+
preservePitch?: boolean | null;
|
|
16
|
+
skipBackwardInterval?: number | null;
|
|
17
|
+
skipForwardInterval?: number | null;
|
|
18
|
+
pollInterval?: number | null;
|
|
19
|
+
autoPlay?: boolean | null;
|
|
20
|
+
enableMediaSession?: boolean | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class AudioDefaults {
|
|
24
|
+
public readonly volume: number;
|
|
25
|
+
public readonly playbackRate: number;
|
|
26
|
+
public readonly preservePitch: boolean;
|
|
27
|
+
public readonly skipBackwardInterval: number;
|
|
28
|
+
public readonly skipForwardInterval: number;
|
|
29
|
+
public readonly pollInterval: number;
|
|
30
|
+
public readonly autoPlay: boolean;
|
|
31
|
+
public readonly enableMediaSession: boolean;
|
|
32
|
+
|
|
33
|
+
constructor(defaults: IAudioDefaults = {}) {
|
|
34
|
+
this.volume = ensureValueInRange(defaults.volume, volumeRangeConfig.range) ?? 1;
|
|
35
|
+
this.playbackRate = ensureValueInRange(defaults.playbackRate, playbackRateRangeConfig.range) ?? 1;
|
|
36
|
+
this.preservePitch = ensureBoolean(defaults.preservePitch) ?? true;
|
|
37
|
+
this.skipBackwardInterval = ensureValueInRange(defaults.skipBackwardInterval, skipIntervalRangeConfig.range) ?? 10;
|
|
38
|
+
this.skipForwardInterval = ensureValueInRange(defaults.skipForwardInterval, skipIntervalRangeConfig.range) ?? 10;
|
|
39
|
+
this.pollInterval = ensureNonNegative(defaults.pollInterval) ?? 1000;
|
|
40
|
+
this.autoPlay = ensureBoolean(defaults.autoPlay) ?? true;
|
|
41
|
+
this.enableMediaSession = ensureBoolean(defaults.enableMediaSession) ?? true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ConfigurablePreferences } from "../../preferences/Configurable";
|
|
2
|
+
import {
|
|
3
|
+
ensureBoolean,
|
|
4
|
+
ensureValueInRange,
|
|
5
|
+
ensureNonNegative
|
|
6
|
+
} from "../../preferences/guards";
|
|
7
|
+
import {
|
|
8
|
+
volumeRangeConfig,
|
|
9
|
+
playbackRateRangeConfig,
|
|
10
|
+
skipIntervalRangeConfig
|
|
11
|
+
} from "../../preferences/Types";
|
|
12
|
+
|
|
13
|
+
export interface IAudioPreferences {
|
|
14
|
+
volume?: number | null;
|
|
15
|
+
playbackRate?: number | null;
|
|
16
|
+
preservePitch?: boolean | null;
|
|
17
|
+
skipBackwardInterval?: number | null;
|
|
18
|
+
skipForwardInterval?: number | null;
|
|
19
|
+
pollInterval?: number | null;
|
|
20
|
+
autoPlay?: boolean | null;
|
|
21
|
+
enableMediaSession?: boolean | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class AudioPreferences implements ConfigurablePreferences<AudioPreferences> {
|
|
25
|
+
public readonly volume: number | null | undefined;
|
|
26
|
+
public readonly playbackRate: number | null | undefined;
|
|
27
|
+
public readonly preservePitch: boolean | null | undefined;
|
|
28
|
+
public readonly skipBackwardInterval: number | null | undefined;
|
|
29
|
+
public readonly skipForwardInterval: number | null | undefined;
|
|
30
|
+
public readonly pollInterval: number | null | undefined;
|
|
31
|
+
public readonly autoPlay: boolean | null | undefined;
|
|
32
|
+
public readonly enableMediaSession: boolean | null | undefined;
|
|
33
|
+
|
|
34
|
+
constructor(preferences: IAudioPreferences = {}) {
|
|
35
|
+
this.volume = ensureValueInRange(preferences.volume, volumeRangeConfig.range);
|
|
36
|
+
this.playbackRate = ensureValueInRange(preferences.playbackRate, playbackRateRangeConfig.range);
|
|
37
|
+
this.preservePitch = ensureBoolean(preferences.preservePitch);
|
|
38
|
+
this.skipBackwardInterval = ensureValueInRange(preferences.skipBackwardInterval, skipIntervalRangeConfig.range);
|
|
39
|
+
this.skipForwardInterval = ensureValueInRange(preferences.skipForwardInterval, skipIntervalRangeConfig.range);
|
|
40
|
+
this.pollInterval = ensureNonNegative(preferences.pollInterval);
|
|
41
|
+
this.autoPlay = ensureBoolean(preferences.autoPlay);
|
|
42
|
+
this.enableMediaSession = ensureBoolean(preferences.enableMediaSession);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
merging(other: AudioPreferences): AudioPreferences {
|
|
46
|
+
const merged: IAudioPreferences = { ...this };
|
|
47
|
+
for (const key of Object.keys(other) as (keyof IAudioPreferences)[]) {
|
|
48
|
+
if (other[key] !== undefined) {
|
|
49
|
+
(merged as Record<string, unknown>)[key] = other[key];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return new AudioPreferences(merged);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { IPreferencesEditor } from "../../preferences/PreferencesEditor";
|
|
2
|
+
import { AudioPreferences } from "./AudioPreferences";
|
|
3
|
+
import { AudioSettings } from "./AudioSettings";
|
|
4
|
+
import { Preference, BooleanPreference, RangePreference } from "../../preferences/Preference";
|
|
5
|
+
import {
|
|
6
|
+
volumeRangeConfig,
|
|
7
|
+
playbackRateRangeConfig,
|
|
8
|
+
skipIntervalRangeConfig
|
|
9
|
+
} from "../../preferences/Types";
|
|
10
|
+
|
|
11
|
+
export class AudioPreferencesEditor implements IPreferencesEditor {
|
|
12
|
+
preferences: AudioPreferences;
|
|
13
|
+
private settings: AudioSettings;
|
|
14
|
+
|
|
15
|
+
constructor(initialPreferences: AudioPreferences, settings: AudioSettings) {
|
|
16
|
+
this.preferences = initialPreferences;
|
|
17
|
+
this.settings = settings;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
clear(): void {
|
|
21
|
+
this.preferences = new AudioPreferences();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private updatePreference<K extends keyof AudioPreferences>(key: K, value: AudioPreferences[K]) {
|
|
25
|
+
this.preferences[key] = value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get volume(): RangePreference<number> {
|
|
29
|
+
return new RangePreference<number>({
|
|
30
|
+
initialValue: this.preferences.volume,
|
|
31
|
+
effectiveValue: this.settings.volume,
|
|
32
|
+
isEffective: this.preferences.volume !== null,
|
|
33
|
+
onChange: (newValue: number | null | undefined) => {
|
|
34
|
+
this.updatePreference("volume", newValue ?? 1);
|
|
35
|
+
},
|
|
36
|
+
supportedRange: volumeRangeConfig.range,
|
|
37
|
+
step: volumeRangeConfig.step
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get playbackRate(): RangePreference<number> {
|
|
42
|
+
return new RangePreference<number>({
|
|
43
|
+
initialValue: this.preferences.playbackRate,
|
|
44
|
+
effectiveValue: this.settings.playbackRate,
|
|
45
|
+
isEffective: this.preferences.playbackRate !== null,
|
|
46
|
+
onChange: (newValue: number | null | undefined) => {
|
|
47
|
+
this.updatePreference("playbackRate", newValue ?? 1);
|
|
48
|
+
},
|
|
49
|
+
supportedRange: playbackRateRangeConfig.range,
|
|
50
|
+
step: playbackRateRangeConfig.step
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get preservePitch(): BooleanPreference {
|
|
55
|
+
return new BooleanPreference({
|
|
56
|
+
initialValue: this.preferences.preservePitch,
|
|
57
|
+
effectiveValue: this.settings.preservePitch,
|
|
58
|
+
isEffective: this.preferences.preservePitch !== null,
|
|
59
|
+
onChange: (newValue: boolean | null | undefined) => {
|
|
60
|
+
this.updatePreference("preservePitch", newValue ?? true);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get skipBackwardInterval(): RangePreference<number> {
|
|
66
|
+
return new RangePreference<number>({
|
|
67
|
+
initialValue: this.preferences.skipBackwardInterval,
|
|
68
|
+
effectiveValue: this.settings.skipBackwardInterval,
|
|
69
|
+
isEffective: this.preferences.skipBackwardInterval !== null,
|
|
70
|
+
onChange: (newValue: number | null | undefined) => {
|
|
71
|
+
this.updatePreference("skipBackwardInterval", newValue ?? 10);
|
|
72
|
+
},
|
|
73
|
+
supportedRange: skipIntervalRangeConfig.range,
|
|
74
|
+
step: skipIntervalRangeConfig.step
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get skipForwardInterval(): RangePreference<number> {
|
|
79
|
+
return new RangePreference<number>({
|
|
80
|
+
initialValue: this.preferences.skipForwardInterval,
|
|
81
|
+
effectiveValue: this.settings.skipForwardInterval,
|
|
82
|
+
isEffective: this.preferences.skipForwardInterval !== null,
|
|
83
|
+
onChange: (newValue: number | null | undefined) => {
|
|
84
|
+
this.updatePreference("skipForwardInterval", newValue ?? 10);
|
|
85
|
+
},
|
|
86
|
+
supportedRange: skipIntervalRangeConfig.range,
|
|
87
|
+
step: skipIntervalRangeConfig.step
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get pollInterval(): Preference<number> {
|
|
92
|
+
return new Preference<number>({
|
|
93
|
+
initialValue: this.preferences.pollInterval,
|
|
94
|
+
effectiveValue: this.settings.pollInterval,
|
|
95
|
+
isEffective: this.preferences.pollInterval !== null,
|
|
96
|
+
onChange: (newValue: number | null | undefined) => {
|
|
97
|
+
this.updatePreference("pollInterval", newValue ?? 1000);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get autoPlay(): BooleanPreference {
|
|
103
|
+
return new BooleanPreference({
|
|
104
|
+
initialValue: this.preferences.autoPlay,
|
|
105
|
+
effectiveValue: this.settings.autoPlay,
|
|
106
|
+
isEffective: this.preferences.autoPlay !== null,
|
|
107
|
+
onChange: (newValue: boolean | null | undefined) => {
|
|
108
|
+
this.updatePreference("autoPlay", newValue ?? true);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get enableMediaSession(): BooleanPreference {
|
|
114
|
+
return new BooleanPreference({
|
|
115
|
+
initialValue: this.preferences.enableMediaSession,
|
|
116
|
+
effectiveValue: this.settings.enableMediaSession,
|
|
117
|
+
isEffective: this.preferences.enableMediaSession !== null,
|
|
118
|
+
onChange: (newValue: boolean | null | undefined) => {
|
|
119
|
+
this.updatePreference("enableMediaSession", newValue ?? true);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AudioPreferences } from "./AudioPreferences";
|
|
2
|
+
import { AudioDefaults } from "./AudioDefaults";
|
|
3
|
+
import { ConfigurableSettings } from "../../preferences/Configurable";
|
|
4
|
+
|
|
5
|
+
export interface IAudioSettings extends ConfigurableSettings {
|
|
6
|
+
volume: number;
|
|
7
|
+
playbackRate: number;
|
|
8
|
+
preservePitch: boolean;
|
|
9
|
+
skipBackwardInterval: number;
|
|
10
|
+
skipForwardInterval: number;
|
|
11
|
+
pollInterval: number;
|
|
12
|
+
autoPlay: boolean;
|
|
13
|
+
enableMediaSession: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class AudioSettings implements IAudioSettings, ConfigurableSettings {
|
|
17
|
+
public readonly volume: number;
|
|
18
|
+
public readonly playbackRate: number;
|
|
19
|
+
public readonly preservePitch: boolean;
|
|
20
|
+
public readonly skipBackwardInterval: number;
|
|
21
|
+
public readonly skipForwardInterval: number;
|
|
22
|
+
public readonly pollInterval: number;
|
|
23
|
+
public readonly autoPlay: boolean;
|
|
24
|
+
public readonly enableMediaSession: boolean;
|
|
25
|
+
|
|
26
|
+
constructor(preferences: AudioPreferences, defaults: AudioDefaults) {
|
|
27
|
+
this.volume = preferences.volume ?? defaults.volume;
|
|
28
|
+
this.playbackRate = preferences.playbackRate ?? defaults.playbackRate;
|
|
29
|
+
this.preservePitch = preferences.preservePitch ?? defaults.preservePitch;
|
|
30
|
+
this.skipBackwardInterval = preferences.skipBackwardInterval ?? defaults.skipBackwardInterval;
|
|
31
|
+
this.skipForwardInterval = preferences.skipForwardInterval ?? defaults.skipForwardInterval;
|
|
32
|
+
this.pollInterval = preferences.pollInterval ?? defaults.pollInterval;
|
|
33
|
+
this.autoPlay = preferences.autoPlay ?? defaults.autoPlay;
|
|
34
|
+
this.enableMediaSession = preferences.enableMediaSession ?? defaults.enableMediaSession;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Layout, Link, Locator, Profile, Publication, ReadingProgression } from "@readium/shared";
|
|
2
|
-
import { Configurable,
|
|
2
|
+
import { Configurable, ConfigurableSettings, LineLengths, ProgressionRange, VisualNavigator, VisualNavigatorViewport } from "../";
|
|
3
3
|
import { FramePoolManager } from "./frame/FramePoolManager";
|
|
4
4
|
import { FXLFramePoolManager } from "./fxl/FXLFramePoolManager";
|
|
5
5
|
import { CommsEventKey, ContextMenuEvent, FXLModules, KeyboardEventData, ModuleLibrary, ModuleName, ReflowableModules } from "@readium/navigator-html-injectables";
|
|
@@ -64,7 +64,7 @@ const defaultListeners = (listeners: EpubNavigatorListeners): EpubNavigatorListe
|
|
|
64
64
|
peripheral: listeners.peripheral || (() => {}),
|
|
65
65
|
})
|
|
66
66
|
|
|
67
|
-
export class EpubNavigator extends VisualNavigator implements Configurable<ConfigurableSettings,
|
|
67
|
+
export class EpubNavigator extends VisualNavigator implements Configurable<ConfigurableSettings, EpubPreferences> {
|
|
68
68
|
private readonly pub: Publication;
|
|
69
69
|
private readonly container: HTMLElement;
|
|
70
70
|
private readonly listeners: EpubNavigatorListeners;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MediaType
|
|
1
|
+
import { MediaType } from "@readium/shared";
|
|
2
2
|
import { Link, Publication } from "@readium/shared";
|
|
3
3
|
import { Injector } from "../../injection/Injector";
|
|
4
4
|
|
|
@@ -20,120 +20,73 @@ const csp = (domains: string[]) => {
|
|
|
20
20
|
].join("; ");
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
export default class
|
|
23
|
+
export default class FrameBlobBuider {
|
|
24
|
+
private readonly item: Link;
|
|
25
|
+
private readonly burl: string;
|
|
26
|
+
private readonly pub: Publication;
|
|
24
27
|
private readonly cssProperties?: { [key: string]: string };
|
|
25
28
|
private readonly injector: Injector | null = null;
|
|
26
29
|
|
|
27
|
-
private currentUrl?: string;
|
|
28
|
-
private currentResource?: Resource;
|
|
29
|
-
|
|
30
30
|
constructor(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
pub: Publication,
|
|
32
|
+
baseURL: string,
|
|
33
|
+
item: Link,
|
|
34
34
|
options: {
|
|
35
35
|
cssProperties?: { [key: string]: string };
|
|
36
36
|
injector?: Injector | null;
|
|
37
37
|
}
|
|
38
38
|
) {
|
|
39
|
+
this.pub = pub;
|
|
39
40
|
this.item = item;
|
|
41
|
+
this.burl = item.toURL(baseURL) || "";
|
|
40
42
|
this.cssProperties = options.cssProperties;
|
|
41
43
|
this.injector = options.injector ?? null;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
public reset() {
|
|
45
|
-
this.currentUrl && URL.revokeObjectURL(this.currentUrl);
|
|
46
|
-
this.currentUrl = undefined;
|
|
47
|
-
this.currentResource?.close();
|
|
48
|
-
this.currentResource = undefined;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
46
|
public async build(fxl = false): Promise<string> {
|
|
52
|
-
if(this.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const link = await this.currentResource.link();
|
|
56
|
-
if(!this.currentResource) {
|
|
57
|
-
// Reset has occured in the meantime
|
|
58
|
-
return "about:blank";
|
|
59
|
-
}
|
|
60
|
-
if(!link.mediaType.isHTML) {
|
|
61
|
-
if(link.mediaType.isBitmap || link.mediaType.equals(MediaType.SVG)) {
|
|
62
|
-
const blobUrl = await this.buildImageFrame();
|
|
63
|
-
this.currentUrl = blobUrl;
|
|
64
|
-
return blobUrl;
|
|
47
|
+
if(!this.item.mediaType.isHTML) {
|
|
48
|
+
if(this.item.mediaType.isBitmap || this.item.mediaType.equals(MediaType.SVG)) {
|
|
49
|
+
return this.buildImageFrame();
|
|
65
50
|
} else
|
|
66
|
-
throw Error("Unsupported frame mediatype " +
|
|
51
|
+
throw Error("Unsupported frame mediatype " + this.item.mediaType.string);
|
|
67
52
|
} else {
|
|
68
|
-
|
|
69
|
-
this.currentUrl = blobUrl;
|
|
70
|
-
return blobUrl;
|
|
53
|
+
return await this.buildHtmlFrame(fxl);
|
|
71
54
|
}
|
|
72
55
|
}
|
|
73
56
|
|
|
74
57
|
private async buildHtmlFrame(fxl = false): Promise<string> {
|
|
75
|
-
if(!this.currentResource) throw new Error("No resource loaded");
|
|
76
|
-
|
|
77
58
|
// Load the HTML resource
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
59
|
+
const txt = await this.pub.get(this.item).readAsString();
|
|
60
|
+
if(!txt) throw new Error(`Failed reading item ${this.item.href}`);
|
|
61
|
+
|
|
62
|
+
const doc = new DOMParser().parseFromString(
|
|
63
|
+
txt,
|
|
64
|
+
this.item.mediaType.string as DOMParserSupportedType
|
|
65
|
+
);
|
|
66
|
+
|
|
81
67
|
const perror = doc.querySelector("parsererror");
|
|
82
68
|
if (perror) {
|
|
83
69
|
const details = perror.querySelector("div");
|
|
84
|
-
throw new Error(`Failed parsing item ${
|
|
70
|
+
throw new Error(`Failed parsing item ${this.item.href}: ${details?.textContent || perror.textContent}`);
|
|
85
71
|
}
|
|
86
72
|
|
|
87
73
|
// Apply resource injections if injection service is provided
|
|
88
74
|
if (this.injector) {
|
|
89
|
-
await this.injector.injectForDocument(doc,
|
|
75
|
+
await this.injector.injectForDocument(doc, this.item);
|
|
90
76
|
}
|
|
91
77
|
|
|
92
|
-
return this.finalizeDOM(doc, this.pub.baseURL,
|
|
78
|
+
return this.finalizeDOM(doc, this.pub.baseURL, this.burl, this.item.mediaType, fxl, this.cssProperties);
|
|
93
79
|
}
|
|
94
80
|
|
|
95
|
-
private
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
const burl = link.toURL(this.baseURL) || ""
|
|
99
|
-
|
|
100
|
-
// Rudimentary image display in an HTML doc
|
|
101
|
-
const doc = document.implementation.createHTMLDocument(link.title || link.href);
|
|
102
|
-
|
|
103
|
-
// Add viewport if available
|
|
104
|
-
if((link?.height || 0) > 0 && (link?.width || 0) > 0) {
|
|
105
|
-
const viewportMeta = doc.createElement("meta");
|
|
106
|
-
viewportMeta.name = "viewport";
|
|
107
|
-
viewportMeta.content = `width=${link.width}, height=${link.height}`;
|
|
108
|
-
viewportMeta.dataset.readium = "true";
|
|
109
|
-
doc.head.appendChild(viewportMeta);
|
|
110
|
-
}
|
|
111
|
-
|
|
81
|
+
private buildImageFrame(): string {
|
|
82
|
+
// Rudimentary image display
|
|
83
|
+
const doc = document.implementation.createHTMLDocument(this.item.title || this.item.href);
|
|
112
84
|
const simg = document.createElement("img");
|
|
113
|
-
simg.src = burl || "";
|
|
114
|
-
simg.alt =
|
|
85
|
+
simg.src = this.burl || "";
|
|
86
|
+
simg.alt = this.item.title || "";
|
|
115
87
|
simg.decoding = "async";
|
|
116
88
|
doc.body.appendChild(simg);
|
|
117
|
-
|
|
118
|
-
// Apply resource injections if injection service is provided
|
|
119
|
-
if (this.injector) {
|
|
120
|
-
await this.injector.injectForDocument(doc, new Link({
|
|
121
|
-
// Temporary solution to address injector only expecting (X)HTML
|
|
122
|
-
// documents for injection, which we are technically providing
|
|
123
|
-
href: "readium-image-frame.xhtml",
|
|
124
|
-
type: MediaType.XHTML.string
|
|
125
|
-
}));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Add image style
|
|
129
|
-
const sstyle = doc.createElement("style");
|
|
130
|
-
sstyle.dataset.readium = "true";
|
|
131
|
-
sstyle.textContent = `
|
|
132
|
-
html, body { width: 100%; height: 100%; margin: 0; padding: 0; font-size: 0; }
|
|
133
|
-
img { margin: 0; padding: 0; border: 0; }`;
|
|
134
|
-
doc.head.appendChild(sstyle);
|
|
135
|
-
|
|
136
|
-
return this.finalizeDOM(doc, this.pub.baseURL, burl, link.mediaType, true);
|
|
89
|
+
return this.finalizeDOM(doc, this.pub.baseURL, this.burl, this.item.mediaType, true);
|
|
137
90
|
}
|
|
138
91
|
|
|
139
92
|
private setProperties(cssProperties: { [key: string]: string }, doc: Document) {
|
|
@@ -148,10 +101,7 @@ export default class FrameBlobBuilder {
|
|
|
148
101
|
|
|
149
102
|
// Get allowed domains from injector if it exists
|
|
150
103
|
const allowedDomains = this.injector?.getAllowedDomains?.() || [];
|
|
151
|
-
|
|
152
|
-
// Remove query from root if present, as CSP doesn't allow them
|
|
153
|
-
root = root?.split("?")[0];
|
|
154
|
-
|
|
104
|
+
|
|
155
105
|
// Always include the root domain if provided
|
|
156
106
|
const domains = [...new Set([
|
|
157
107
|
...(root ? [root] : []),
|
|
@@ -174,7 +124,6 @@ export default class FrameBlobBuilder {
|
|
|
174
124
|
// loaded in parallel, greatly increasing overall speed.
|
|
175
125
|
doc.body.querySelectorAll("img").forEach((img) => {
|
|
176
126
|
img.setAttribute("fetchpriority", "high");
|
|
177
|
-
img.setAttribute("referrerpolicy", "origin");
|
|
178
127
|
});
|
|
179
128
|
|
|
180
129
|
// We need to ensure that lang is set on the root element
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ModuleName } from "@readium/navigator-html-injectables";
|
|
2
2
|
import { Locator, Publication } from "@readium/shared";
|
|
3
|
-
import
|
|
3
|
+
import FrameBlobBuider from "./FrameBlobBuilder";
|
|
4
4
|
import { FrameManager } from "./FrameManager";
|
|
5
5
|
import { Injector } from "../../injection/Injector";
|
|
6
6
|
import { IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../../Navigator";
|
|
@@ -14,7 +14,7 @@ export class FramePoolManager {
|
|
|
14
14
|
private _currentFrame: FrameManager | undefined;
|
|
15
15
|
private currentCssProperties: { [key: string]: string } | undefined;
|
|
16
16
|
private readonly pool: Map<string, FrameManager> = new Map();
|
|
17
|
-
private readonly blobs: Map<string,
|
|
17
|
+
private readonly blobs: Map<string, string> = new Map();
|
|
18
18
|
private readonly inprogress: Map<string, Promise<void>> = new Map();
|
|
19
19
|
private pendingUpdates: Map<string, { inPool: boolean }> = new Map();
|
|
20
20
|
private currentBaseURL: string | undefined;
|
|
@@ -62,8 +62,10 @@ export class FramePoolManager {
|
|
|
62
62
|
this.pool.clear();
|
|
63
63
|
|
|
64
64
|
// Revoke all blobs
|
|
65
|
-
this.blobs.forEach(v =>
|
|
66
|
-
|
|
65
|
+
this.blobs.forEach(v => {
|
|
66
|
+
this.injector?.releaseBlobUrl?.(v);
|
|
67
|
+
URL.revokeObjectURL(v);
|
|
68
|
+
});
|
|
67
69
|
|
|
68
70
|
// Clean up injector if it exists
|
|
69
71
|
this.injector?.dispose();
|
|
@@ -104,18 +106,15 @@ export class FramePoolManager {
|
|
|
104
106
|
this.pool.delete(href);
|
|
105
107
|
if(this.pendingUpdates.has(href))
|
|
106
108
|
this.pendingUpdates.set(href, { inPool: false });
|
|
107
|
-
// Note that we don't reset the blob here, unlike in the FXL pool.
|
|
108
|
-
// This is because FXL tends to have a ton more blobs. Maybe we'll adjust
|
|
109
|
-
// this at a later point with a much larger boundary for resets to deal
|
|
110
|
-
// with extremely long/large reflowable publications.
|
|
111
|
-
// Reflowable publication resources also tend to be much larger documents,
|
|
112
|
-
// so they're more expensive to preprocess with the FrameBlobBuilder.
|
|
113
109
|
});
|
|
114
110
|
|
|
115
111
|
// Check if base URL of publication has changed
|
|
116
112
|
if(this.currentBaseURL !== undefined && pub.baseURL !== this.currentBaseURL) {
|
|
117
113
|
// Revoke all blobs
|
|
118
|
-
this.blobs.forEach(v =>
|
|
114
|
+
this.blobs.forEach(v => {
|
|
115
|
+
this.injector?.releaseBlobUrl?.(v);
|
|
116
|
+
URL.revokeObjectURL(v);
|
|
117
|
+
});
|
|
119
118
|
this.blobs.clear();
|
|
120
119
|
}
|
|
121
120
|
this.currentBaseURL = pub.baseURL;
|
|
@@ -128,14 +127,18 @@ export class FramePoolManager {
|
|
|
128
127
|
// when navigating backwards, where paginated will go the
|
|
129
128
|
// start of the resource instead of the end due to the
|
|
130
129
|
// corrupted width ColumnSnapper (injectables) gets on init
|
|
131
|
-
this.blobs.forEach(v =>
|
|
130
|
+
this.blobs.forEach(v => {
|
|
131
|
+
this.injector?.releaseBlobUrl?.(v);
|
|
132
|
+
URL.revokeObjectURL(v);
|
|
133
|
+
});
|
|
132
134
|
this.blobs.clear();
|
|
133
135
|
this.pendingUpdates.clear();
|
|
134
136
|
}
|
|
135
137
|
if(this.pendingUpdates.has(href) && this.pendingUpdates.get(href)?.inPool === false) {
|
|
136
|
-
const
|
|
137
|
-
if(
|
|
138
|
-
|
|
138
|
+
const url = this.blobs.get(href);
|
|
139
|
+
if(url) {
|
|
140
|
+
this.injector?.releaseBlobUrl?.(url);
|
|
141
|
+
URL.revokeObjectURL(url);
|
|
139
142
|
this.blobs.delete(href);
|
|
140
143
|
this.pendingUpdates.delete(href);
|
|
141
144
|
}
|
|
@@ -154,7 +157,7 @@ export class FramePoolManager {
|
|
|
154
157
|
const itm = pub.readingOrder.findWithHref(href);
|
|
155
158
|
if(!itm) return; // TODO throw?
|
|
156
159
|
if(!this.blobs.has(href)) {
|
|
157
|
-
|
|
160
|
+
const blobBuilder = new FrameBlobBuider(
|
|
158
161
|
pub,
|
|
159
162
|
this.currentBaseURL || "",
|
|
160
163
|
itm,
|
|
@@ -162,11 +165,13 @@ export class FramePoolManager {
|
|
|
162
165
|
cssProperties: this.currentCssProperties,
|
|
163
166
|
injector: this.injector
|
|
164
167
|
}
|
|
165
|
-
)
|
|
168
|
+
);
|
|
169
|
+
const blobURL = await blobBuilder.build();
|
|
170
|
+
this.blobs.set(href, blobURL);
|
|
166
171
|
}
|
|
167
172
|
|
|
168
173
|
// Create <iframe>
|
|
169
|
-
const fm = new FrameManager(
|
|
174
|
+
const fm = new FrameManager(this.blobs.get(href)!, this.contentProtectionConfig, this.keyboardPeripheralsConfig);
|
|
170
175
|
if(href !== newHref) await fm.hide(); // Avoid unecessary hide
|
|
171
176
|
this.container.appendChild(fm.iframe);
|
|
172
177
|
await fm.load(modules);
|
|
@@ -7,7 +7,6 @@ import { IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../../Navi
|
|
|
7
7
|
|
|
8
8
|
export class FXLFrameManager {
|
|
9
9
|
private frame: HTMLIFrameElement;
|
|
10
|
-
private frameIsAppended = false;
|
|
11
10
|
private loader: Loader | undefined;
|
|
12
11
|
public source: string;
|
|
13
12
|
private comms: FrameComms | undefined;
|
|
@@ -21,7 +20,6 @@ export class FXLFrameManager {
|
|
|
21
20
|
public debugHref: string;
|
|
22
21
|
private loadPromise: Promise<Window> | undefined;
|
|
23
22
|
private showPromise: Promise<void> | undefined;
|
|
24
|
-
private viewportSize: { width: number, height: number } | undefined = undefined;
|
|
25
23
|
|
|
26
24
|
constructor(
|
|
27
25
|
peripherals: FXLPeripherals,
|
|
@@ -56,13 +54,11 @@ export class FXLFrameManager {
|
|
|
56
54
|
this.wrapper = document.createElement("div");
|
|
57
55
|
this.wrapper.style.position = "relative";
|
|
58
56
|
this.wrapper.style.float = this.wrapper.style.cssFloat = direction === ReadingProgression.rtl ? "right" : "left";
|
|
57
|
+
|
|
58
|
+
this.wrapper.appendChild(this.frame);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
async load(modules: ModuleName[], source: string): Promise<Window> {
|
|
62
|
-
if(!this.frameIsAppended) {
|
|
63
|
-
this.wrapper.appendChild(this.frame);
|
|
64
|
-
this.frameIsAppended = true;
|
|
65
|
-
}
|
|
66
62
|
if(this.source === source && this.loadPromise/* && this.loaded*/) {
|
|
67
63
|
if([...this.currModules].sort().join("|") === [...modules].sort().join("|")) {
|
|
68
64
|
return this.loadPromise;
|
|
@@ -108,7 +104,6 @@ export class FXLFrameManager {
|
|
|
108
104
|
|
|
109
105
|
// Parses the page size from the viewport meta tag of the loaded resource.
|
|
110
106
|
loadPageSize(): { width: number, height: number } {
|
|
111
|
-
if(this.viewportSize) return this.viewportSize;
|
|
112
107
|
const wnd = this.frame.contentWindow!;
|
|
113
108
|
|
|
114
109
|
// Try to get the page size from the viewport meta tag
|
|
@@ -125,10 +120,8 @@ export class FXLFrameManager {
|
|
|
125
120
|
else if(match[1] === "height")
|
|
126
121
|
height = Number.parseFloat(match[2]);
|
|
127
122
|
}
|
|
128
|
-
if(width > 0 && height > 0)
|
|
129
|
-
|
|
130
|
-
return this.viewportSize;
|
|
131
|
-
}
|
|
123
|
+
if(width > 0 && height > 0)
|
|
124
|
+
return { width, height };
|
|
132
125
|
}
|
|
133
126
|
|
|
134
127
|
// Otherwise get it from the size of the loaded content
|