@missing-elements/h5p-offline-player 0.1.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/LICENSE +21 -0
- package/README.md +273 -0
- package/dist/VERSION +1 -0
- package/dist/frame-assets/MANIFEST.json +4 -0
- package/dist/frame-assets/fonts/h5p-core-30.woff2 +0 -0
- package/dist/frame-assets/fonts/h5p-hub-publish.woff +0 -0
- package/dist/frame-assets/fonts/h5p-hub-publish.woff2 +0 -0
- package/dist/frame-assets/fonts/h5p-theme.woff2 +0 -0
- package/dist/frame-assets/fonts/inter/Inter-ExtraBold.woff2 +0 -0
- package/dist/frame-assets/fonts/inter/Inter-ExtraBoldItalic.woff2 +0 -0
- package/dist/frame-assets/fonts/inter/LICENSE.txt +92 -0
- package/dist/frame-assets/fonts/inter/inter-v18-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-600.woff2 +0 -0
- package/dist/frame-assets/fonts/inter/inter-v18-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-600italic.woff2 +0 -0
- package/dist/frame-assets/fonts/inter/inter-v18-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-italic.woff2 +0 -0
- package/dist/frame-assets/fonts/inter/inter-v18-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-regular.woff2 +0 -0
- package/dist/frame-assets/fonts/open-sans/OFL.txt +88 -0
- package/dist/frame-assets/fonts/open-sans/open-sans-v40-cyrillic_cyrillic-ext_greek_greek-ext_hebrew_latin_latin-ext_math_symbols_vietnamese-600.woff2 +0 -0
- package/dist/frame-assets/fonts/open-sans/open-sans-v40-cyrillic_cyrillic-ext_greek_greek-ext_hebrew_latin_latin-ext_math_symbols_vietnamese-600italic.woff2 +0 -0
- package/dist/frame-assets/fonts/open-sans/open-sans-v40-cyrillic_cyrillic-ext_greek_greek-ext_hebrew_latin_latin-ext_math_symbols_vietnamese-700.woff2 +0 -0
- package/dist/frame-assets/fonts/open-sans/open-sans-v40-cyrillic_cyrillic-ext_greek_greek-ext_hebrew_latin_latin-ext_math_symbols_vietnamese-700italic.woff2 +0 -0
- package/dist/frame-assets/fonts/open-sans/open-sans-v40-cyrillic_cyrillic-ext_greek_greek-ext_hebrew_latin_latin-ext_math_symbols_vietnamese-italic.woff2 +0 -0
- package/dist/frame-assets/fonts/open-sans/open-sans-v40-cyrillic_cyrillic-ext_greek_greek-ext_hebrew_latin_latin-ext_math_symbols_vietnamese-regular.woff2 +0 -0
- package/dist/frame-assets/frame.bundle.js +1 -0
- package/dist/frame-assets/images/h5p.svg +16 -0
- package/dist/frame-assets/images/throbber.gif +0 -0
- package/dist/frame-assets/main.bundle.js +2 -0
- package/dist/frame-assets/styles/h5p-fonts.css +103 -0
- package/dist/frame-assets/styles/h5p.css +1643 -0
- package/dist/h5p-player.js +1780 -0
- package/dist/h5p-sw-mount.js +88 -0
- package/dist/h5p-sw.js +88 -0
- package/package.json +81 -0
- package/types/h5p-offline-player.d.ts +154 -0
- package/types/jobs/chunk-writer.d.ts +16 -0
- package/types/jobs/jobs-worker.d.ts +1 -0
- package/types/node/tsconfig.node.tsbuildinfo +1 -0
- package/types/node/vite.config.d.ts +6 -0
- package/types/node/vite.demo.config.d.ts +2 -0
- package/types/node/vite.plugins.d.ts +19 -0
- package/types/shared/chunk-store.d.ts +104 -0
- package/types/shared/constants.d.ts +67 -0
- package/types/shared/entry-names.d.ts +29 -0
- package/types/shared/eviction.d.ts +16 -0
- package/types/shared/forward-index.d.ts +85 -0
- package/types/shared/idb.d.ts +9 -0
- package/types/shared/locks.d.ts +14 -0
- package/types/shared/mime.d.ts +7 -0
- package/types/shared/mp4.d.ts +41 -0
- package/types/shared/pkg-id.d.ts +21 -0
- package/types/shared/protocol.d.ts +232 -0
- package/types/shared/range.d.ts +16 -0
- package/types/shared/source.d.ts +57 -0
- package/types/shared/stream-utils.d.ts +7 -0
- package/types/sw/frame-document.d.ts +19 -0
- package/types/sw/mount.d.ts +17 -0
- package/types/sw/package-reader.d.ts +242 -0
- package/types/sw/routes.d.ts +26 -0
- package/types/sw/stream-utils.d.ts +9 -0
- package/types/sw/sw-entry.d.ts +1 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which package a quota squeeze may drop: the coldest by `lastPlayed` that is neither the one
|
|
3
|
+
* being written nor under a lock anywhere on the origin.
|
|
4
|
+
*
|
|
5
|
+
* Both the Service Worker and the Jobs worker install this, because both write the chunk store
|
|
6
|
+
* and either can be the one that hits the wall — the Jobs worker most often, since the large
|
|
7
|
+
* writes are its. Before it did, it evicted whichever cache the browser happened to list first.
|
|
8
|
+
*/
|
|
9
|
+
export declare function coldestIdlePackage(exceptPkgId: string, lookups?: {
|
|
10
|
+
packagesByAge?: () => Promise<Array<{
|
|
11
|
+
pkgId: string;
|
|
12
|
+
}>>;
|
|
13
|
+
busyPackages?: () => Promise<Set<string>>;
|
|
14
|
+
}): Promise<string | null>;
|
|
15
|
+
/** Points the chunk store's eviction at the `packages` table, and cleans the table up after it. */
|
|
16
|
+
export declare function installEvictionPolicy(onEvicted?: (pkgId: string) => void): void;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A forward index of a zip: the entries readable from its local headers alone, in the order the
|
|
3
|
+
* bytes arrive, without the central directory that sits at the end of the file.
|
|
4
|
+
*
|
|
5
|
+
* A host that ignores `Range` forces the whole archive down before the central directory can be
|
|
6
|
+
* read, and with it every entry — including the ones that arrived in the first second. Walking
|
|
7
|
+
* the local headers as they stream in recovers those entries early, so a package laid out
|
|
8
|
+
* libraries-first (the PHP exporter's order) can boot while its media is still downloading.
|
|
9
|
+
*
|
|
10
|
+
* Two things the format does that a forward walk has to survive:
|
|
11
|
+
*
|
|
12
|
+
* - **Data descriptors** (general-purpose bit 3). A writer that streams cannot know an entry's
|
|
13
|
+
* compressed size before it has written the data, so it writes zeros in the local header and the
|
|
14
|
+
* real sizes *after* the data. h5p.com's exporter does this for every entry. There is no length
|
|
15
|
+
* to skip by, so the data is scanned for the descriptor signature and a candidate is accepted
|
|
16
|
+
* only when the compressed size it carries equals the number of bytes seen since the data began
|
|
17
|
+
* — a 64-bit coincidence for a false positive. A writer that omits the optional signature leaves
|
|
18
|
+
* the entry unresolvable; the scanner then stops and the central directory decides at the end.
|
|
19
|
+
* - **zip64**. Sizes of `0xFFFFFFFF` in the header defer to the zip64 extra field, and an entry
|
|
20
|
+
* whose header carries that field writes 8-byte sizes in its descriptor.
|
|
21
|
+
*
|
|
22
|
+
* Nothing here is trusted further than the central directory would be: names go through the same
|
|
23
|
+
* normalisation as everything else, and the snapshot is replaced by the real index once the whole
|
|
24
|
+
* archive is present.
|
|
25
|
+
*/
|
|
26
|
+
export interface ForwardEntry {
|
|
27
|
+
/** The name as written in the archive. Normalised by the reader, not here. */
|
|
28
|
+
name: string;
|
|
29
|
+
directory: boolean;
|
|
30
|
+
method: number;
|
|
31
|
+
encrypted: boolean;
|
|
32
|
+
crc32: number;
|
|
33
|
+
compressedSize: number;
|
|
34
|
+
uncompressedSize: number;
|
|
35
|
+
headerOffset: number;
|
|
36
|
+
dataStart: number;
|
|
37
|
+
}
|
|
38
|
+
export type ForwardStop =
|
|
39
|
+
/** The central directory began: every entry has been seen and the index is complete. */
|
|
40
|
+
'central-directory'
|
|
41
|
+
/** Bytes where a local header should be were not one. */
|
|
42
|
+
| 'bad-signature'
|
|
43
|
+
/** A descriptor entry ran to the end of the input without its descriptor being found. */
|
|
44
|
+
| 'unresolved-descriptor';
|
|
45
|
+
export interface ForwardIndexSnapshot {
|
|
46
|
+
/** Entries whose bytes have all arrived, in archive order. Nothing here is still in transit. */
|
|
47
|
+
entries: ForwardEntry[];
|
|
48
|
+
/** The offset up to which the archive is accounted for: the end of the last entry in `entries`. */
|
|
49
|
+
parsedTo: number;
|
|
50
|
+
/** The central directory was reached, so `entries` is the whole archive. */
|
|
51
|
+
done: boolean;
|
|
52
|
+
stopped: ForwardStop | null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Feeds on the bytes of an archive in order and collects entries as their local headers pass.
|
|
56
|
+
* Chunks may be of any size and may split a header, a descriptor or a name anywhere.
|
|
57
|
+
*/
|
|
58
|
+
export declare class LocalHeaderScanner {
|
|
59
|
+
readonly entries: ForwardEntry[];
|
|
60
|
+
parsedTo: number;
|
|
61
|
+
done: boolean;
|
|
62
|
+
stopped: ForwardStop | null;
|
|
63
|
+
/** Bytes not yet consumed, carried between pushes. Small: a header prefix or a descriptor tail. */
|
|
64
|
+
private buffer;
|
|
65
|
+
/** Absolute offset of `buffer[0]`. */
|
|
66
|
+
private position;
|
|
67
|
+
private state;
|
|
68
|
+
push(chunk: Uint8Array): void;
|
|
69
|
+
/** The input ended. A descriptor still being looked for can now never be found. */
|
|
70
|
+
finish(): void;
|
|
71
|
+
snapshot(): ForwardIndexSnapshot;
|
|
72
|
+
private consume;
|
|
73
|
+
private drain;
|
|
74
|
+
/** Returns false when more bytes are needed or the scan has ended. */
|
|
75
|
+
private readHeader;
|
|
76
|
+
/**
|
|
77
|
+
* Looks for the descriptor that ends `entry`. Returns false when more bytes are needed.
|
|
78
|
+
*
|
|
79
|
+
* A candidate is the signature followed by a compressed size equal to the bytes seen since the
|
|
80
|
+
* data began. Bytes that cannot start a candidate that fits are consumed; the last few are kept,
|
|
81
|
+
* since a descriptor can straddle two chunks.
|
|
82
|
+
*/
|
|
83
|
+
private findDescriptor;
|
|
84
|
+
private record;
|
|
85
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PackageRecord } from './protocol';
|
|
2
|
+
export declare function getPackage(pkgId: string): Promise<PackageRecord | undefined>;
|
|
3
|
+
export declare function putPackage(record: PackageRecord): Promise<void>;
|
|
4
|
+
export declare function deletePackage(pkgId: string): Promise<void>;
|
|
5
|
+
export declare function allPackages(): Promise<PackageRecord[]>;
|
|
6
|
+
export declare function touchPackage(pkgId: string): Promise<void>;
|
|
7
|
+
export declare function updatePackage(pkgId: string, patch: Partial<PackageRecord>): Promise<PackageRecord | undefined>;
|
|
8
|
+
/** Packages ordered coldest first. Eviction walks this list. */
|
|
9
|
+
export declare function packagesByAge(): Promise<PackageRecord[]>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Lock names, one prefix per package: `h5p:<pkgId>:<what>`.
|
|
3
|
+
*
|
|
4
|
+
* The Jobs worker holds one per download or extraction for as long as it writes, and the element
|
|
5
|
+
* holds `playing`, shared, for as long as a package is loaded. Eviction reads
|
|
6
|
+
* `navigator.locks.query()` and never picks a package with anything held under its prefix. That
|
|
7
|
+
* is what makes a write in one tab, or a learner mid-video in another, safe from a quota squeeze
|
|
8
|
+
* caused by a package loading in a third — the `packages` table knows what is cold, but only the
|
|
9
|
+
* locks know what is in use right now, across every context on the origin.
|
|
10
|
+
*/
|
|
11
|
+
export declare function packageLockName(pkgId: string, what: string): string;
|
|
12
|
+
export declare function packageLockPrefix(pkgId: string): string;
|
|
13
|
+
/** Packages with a lock held under their prefix right now, from every tab and worker on the origin. */
|
|
14
|
+
export declare function busyPackages(): Promise<Set<string>>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function extensionOf(entryName: string): string;
|
|
2
|
+
export declare function contentTypeOf(entryName: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* True for entries the H5P runtime parses rather than streams. They are never chunked: a
|
|
5
|
+
* partially available script is worse than a slow one.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isTextEntry(entryName: string): boolean;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an MP4 keeps its index, and how much of one we have to inflate to find out.
|
|
3
|
+
*
|
|
4
|
+
* A player cannot decode a single frame without `moov`: it holds the sample tables that say where
|
|
5
|
+
* every frame lives. `-movflags +faststart` moves it to the front of the file; without it — and
|
|
6
|
+
* it is not the default — it lands after `mdat`, which for a feature-length video means the last
|
|
7
|
+
* few kilobytes of a few hundred megabytes. Served over `Range` that costs one small request. Out
|
|
8
|
+
* of a deflated zip entry, where there is no restart point to seek to, it costs the whole file.
|
|
9
|
+
*
|
|
10
|
+
* So this decides which of the two a given entry is, off the first few dozen bytes of output.
|
|
11
|
+
*/
|
|
12
|
+
/** Boxes that may legitimately precede the one that decides the question. */
|
|
13
|
+
export type MoovPosition =
|
|
14
|
+
/** `moov` came first: the player can start on a prefix. */
|
|
15
|
+
'front'
|
|
16
|
+
/** `mdat` came first, so the index is behind it: nothing plays until the whole entry exists. */
|
|
17
|
+
| 'trailing'
|
|
18
|
+
/** Not enough bytes yet — ask again with more. */
|
|
19
|
+
| 'incomplete'
|
|
20
|
+
/** Not an MP4, malformed, or the walk ran past its budget. Treated as `front`, the safe guess. */
|
|
21
|
+
| 'unknown';
|
|
22
|
+
/** Past this much output, stop walking and serve progressively rather than stall the load. */
|
|
23
|
+
export declare const MOOV_SCAN_LIMIT: number;
|
|
24
|
+
export declare function isIndexedMovie(name: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Walks the top-level box list until `moov` or `mdat` decides it.
|
|
27
|
+
*
|
|
28
|
+
* The naive form of this check — "is the second box `mdat`?" — is wrong often enough to matter:
|
|
29
|
+
* `ftyp`, `free`, `skip`, `wide`, `pdin` and `uuid` all legally sit in front, and a `uuid` holding
|
|
30
|
+
* an XMP blob runs to tens of kilobytes. The package that prompted all of this is laid out
|
|
31
|
+
* `ftyp`, `uuid`, `mdat`, so the second box is `uuid` and the shortcut reports the wrong answer
|
|
32
|
+
* on the one file it most needs to get right.
|
|
33
|
+
*/
|
|
34
|
+
export declare function findMoov(head: Uint8Array, limit?: number): MoovPosition;
|
|
35
|
+
/**
|
|
36
|
+
* Reads as much of a stream as the walk needs and cancels the rest.
|
|
37
|
+
*
|
|
38
|
+
* Cancelling matters: the stream behind this is one ranged request spanning the entire entry, so
|
|
39
|
+
* leaving it open would pull the whole video to answer a question about its first 64 bytes.
|
|
40
|
+
*/
|
|
41
|
+
export declare function scanMoov(stream: ReadableStream<Uint8Array>, limit?: number): Promise<MoovPosition>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package identity. The id is derived from what identifies the bytes, not from a counter, so the
|
|
3
|
+
* same URL (with the same validator) or the same picked file finds its cached chunks again on a
|
|
4
|
+
* later visit.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Id for a remote package. The validator (`ETag`, falling back to `Last-Modified`) is folded in
|
|
8
|
+
* when the host exposes one, so a republished archive at the same URL gets a new id instead of
|
|
9
|
+
* colliding with the stale chunks of the old one.
|
|
10
|
+
*/
|
|
11
|
+
export declare function remotePkgId(url: string, validator?: string | null): Promise<string>;
|
|
12
|
+
/**
|
|
13
|
+
* Id for a picked file. Name, size and mtime are all a browser exposes. The descriptor row dies
|
|
14
|
+
* with the session because a `File` handle cannot be persisted, but the chunks survive and are
|
|
15
|
+
* found again when the same file is picked.
|
|
16
|
+
*/
|
|
17
|
+
export declare function filePkgId(file: {
|
|
18
|
+
name: string;
|
|
19
|
+
size: number;
|
|
20
|
+
lastModified: number;
|
|
21
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shapes crossing the three contexts: the page (element), the Service Worker and the Jobs
|
|
3
|
+
* worker. Everything here is structured-cloneable.
|
|
4
|
+
*/
|
|
5
|
+
/** How the bytes of an archive are reached. Persisted, so the worker can rebuild it after a restart. */
|
|
6
|
+
export type SourceDescriptor = {
|
|
7
|
+
/** Host honours `Range`: read straight from the network, store nothing but extracted entries. */
|
|
8
|
+
type: 'range-http';
|
|
9
|
+
url: string;
|
|
10
|
+
size: number;
|
|
11
|
+
validator?: string | null;
|
|
12
|
+
} | {
|
|
13
|
+
/** Host has CORS but ignores `Range`: the archive is downloaded into the chunk store first. */
|
|
14
|
+
type: 'chunked';
|
|
15
|
+
url: string;
|
|
16
|
+
/** `null` until the download finishes for a host that exposes no length. */
|
|
17
|
+
size: number | null;
|
|
18
|
+
validator?: string | null;
|
|
19
|
+
} | {
|
|
20
|
+
/** A `File` from a picker. The handle cannot be persisted; the page re-sends it on request. */
|
|
21
|
+
type: 'file';
|
|
22
|
+
name: string;
|
|
23
|
+
size: number;
|
|
24
|
+
lastModified: number;
|
|
25
|
+
};
|
|
26
|
+
/** The two descriptors a URL can produce. Both carry a validator; a picked file has none. */
|
|
27
|
+
export type RemoteSourceDescriptor = Exclude<SourceDescriptor, {
|
|
28
|
+
type: 'file';
|
|
29
|
+
}>;
|
|
30
|
+
/** What a row has been through. Written for whoever is looking at the table; read by nothing. */
|
|
31
|
+
export type PackageStatus = 'registered' | 'indexed';
|
|
32
|
+
/**
|
|
33
|
+
* Where to look for libraries a package does not carry. `hub` resolves per content type against
|
|
34
|
+
* the official H5P content-type server; anything else is the URL of a `.h5p` that carries them.
|
|
35
|
+
*/
|
|
36
|
+
export type LibrarySource = 'hub' | {
|
|
37
|
+
url: string;
|
|
38
|
+
};
|
|
39
|
+
/** The official H5P content-type server. It answers with CORS and honours `Range`. */
|
|
40
|
+
export declare const HUB_CONTENT_TYPE_URL = "https://api.h5p.org/v1/content-types/";
|
|
41
|
+
/** What an archive declared but does not contain. */
|
|
42
|
+
export interface MissingLibraries {
|
|
43
|
+
/** `mainLibrary` from `h5p.json`, which is what the hub is keyed on. */
|
|
44
|
+
mainLibrary?: string;
|
|
45
|
+
/** Folder names as the runtime will ask for them, e.g. `H5P.InteractiveVideo-1.27`. */
|
|
46
|
+
folders: string[];
|
|
47
|
+
/** True when the archive carries no libraries at all — the usual content-only export. */
|
|
48
|
+
all: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** One row of the IndexedDB `packages` table. */
|
|
51
|
+
export interface PackageRecord {
|
|
52
|
+
pkgId: string;
|
|
53
|
+
source: SourceDescriptor;
|
|
54
|
+
/**
|
|
55
|
+
* A second package whose libraries fill this one's gaps. Stored rather than held in memory so
|
|
56
|
+
* a restarted worker reattaches it without asking the page again.
|
|
57
|
+
*/
|
|
58
|
+
libraryPkgId?: string;
|
|
59
|
+
/**
|
|
60
|
+
* `libraries` marks an archive registered only to supply library folders to another package.
|
|
61
|
+
* It is never played, so it is not held to carrying everything its own `h5p.json` declares.
|
|
62
|
+
*/
|
|
63
|
+
role?: 'content' | 'libraries';
|
|
64
|
+
/** Absolute URLs of the frame assets, resolved by the element and used to synthesize the frame. */
|
|
65
|
+
frameAssets: FrameAssets;
|
|
66
|
+
/** Extra CSP host-sources the host page vouches for. See the `allow-origins` attribute. */
|
|
67
|
+
allowOrigins?: string[];
|
|
68
|
+
status: PackageStatus;
|
|
69
|
+
title?: string;
|
|
70
|
+
lastPlayed: number;
|
|
71
|
+
/** The element version that wrote the row. Read by nothing today; a future migration keys on it. */
|
|
72
|
+
version: string;
|
|
73
|
+
}
|
|
74
|
+
export interface FrameAssets {
|
|
75
|
+
/** h5p-standalone `main.bundle.js` — the loader that walks dependencies and boots the runtime. */
|
|
76
|
+
mainJs: string;
|
|
77
|
+
/** h5p-standalone `frame.bundle.js` — h5p.js, jQuery and the core runtime. */
|
|
78
|
+
frameJs: string;
|
|
79
|
+
/** h5p-standalone `styles/h5p.css`. */
|
|
80
|
+
frameCss: string;
|
|
81
|
+
}
|
|
82
|
+
export type ErrorCode = 'no-cors' | 'no-worker' | 'network' | 'quota' | 'bad-archive' | 'runtime';
|
|
83
|
+
export declare class PlayerError extends Error {
|
|
84
|
+
code: ErrorCode;
|
|
85
|
+
/** Set when the failure is a package that declared libraries it does not carry. */
|
|
86
|
+
missingLibraries?: MissingLibraries;
|
|
87
|
+
constructor(code: ErrorCode, message: string, options?: {
|
|
88
|
+
cause?: unknown;
|
|
89
|
+
missingLibraries?: MissingLibraries;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
export type ToWorkerMessage = {
|
|
93
|
+
type: 'ping';
|
|
94
|
+
} | {
|
|
95
|
+
type: 'register';
|
|
96
|
+
record: PackageRecord;
|
|
97
|
+
} | {
|
|
98
|
+
type: 'index';
|
|
99
|
+
pkgId: string;
|
|
100
|
+
}
|
|
101
|
+
/** Answer to a `need-file` request: the page hands back the `File` the worker lost on restart. */
|
|
102
|
+
| {
|
|
103
|
+
type: 'file';
|
|
104
|
+
pkgId: string;
|
|
105
|
+
file: File;
|
|
106
|
+
}
|
|
107
|
+
/** Points a package at another one, already registered and indexed, for its libraries. */
|
|
108
|
+
| {
|
|
109
|
+
type: 'attach-libraries';
|
|
110
|
+
pkgId: string;
|
|
111
|
+
libraryPkgId: string;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* An entry a cold load cannot serve without a background inflate: large and deflated, so there is
|
|
115
|
+
* no byte in it a `Range` request can reach without the whole stream up to that point.
|
|
116
|
+
*
|
|
117
|
+
* These are named in the `indexed` reply so the work can be started before the content asks for
|
|
118
|
+
* it. That is the only lever there is on a non-faststart mp4, whose index sits at the very end:
|
|
119
|
+
* the first frame is undecodable until the last byte lands, so the wait cannot be shortened, only
|
|
120
|
+
* moved somewhere the learner is not watching it.
|
|
121
|
+
*/
|
|
122
|
+
export interface PrefetchEntry {
|
|
123
|
+
entry: string;
|
|
124
|
+
/** Uncompressed size — the number that decides whether starting it early is worth the bytes. */
|
|
125
|
+
size: number;
|
|
126
|
+
}
|
|
127
|
+
export type WorkerReply = {
|
|
128
|
+
ok: true;
|
|
129
|
+
type: 'pong';
|
|
130
|
+
version: string;
|
|
131
|
+
} | {
|
|
132
|
+
ok: true;
|
|
133
|
+
type: 'indexed';
|
|
134
|
+
pkgId: string;
|
|
135
|
+
entryCount: number;
|
|
136
|
+
title?: string;
|
|
137
|
+
prefetch?: PrefetchEntry[];
|
|
138
|
+
/** Built from the forward index of an archive still downloading, not its central directory. */
|
|
139
|
+
partial?: boolean;
|
|
140
|
+
/** For a partial index: whether everything the runtime needs to boot has arrived. */
|
|
141
|
+
ready?: boolean;
|
|
142
|
+
} | {
|
|
143
|
+
ok: true;
|
|
144
|
+
type: 'ack';
|
|
145
|
+
} | {
|
|
146
|
+
ok: false;
|
|
147
|
+
code: ErrorCode;
|
|
148
|
+
message: string;
|
|
149
|
+
missingLibraries?: MissingLibraries;
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Sent to the frame client, which relays it to its parent element. The Service Worker cannot run
|
|
153
|
+
* a job itself: browsers kill a worker event after a few minutes and a killed inflate cannot
|
|
154
|
+
* resume, so every long extraction is handed back to the page.
|
|
155
|
+
*/
|
|
156
|
+
export type FromWorkerMessage = {
|
|
157
|
+
type: 'need-job';
|
|
158
|
+
pkgId: string;
|
|
159
|
+
entry: string;
|
|
160
|
+
} | {
|
|
161
|
+
type: 'need-file';
|
|
162
|
+
pkgId: string;
|
|
163
|
+
};
|
|
164
|
+
export type ToJobsMessage = {
|
|
165
|
+
type: 'download';
|
|
166
|
+
pkgId: string;
|
|
167
|
+
source: SourceDescriptor;
|
|
168
|
+
} | {
|
|
169
|
+
type: 'extract';
|
|
170
|
+
pkgId: string;
|
|
171
|
+
entry: string;
|
|
172
|
+
source: SourceDescriptor;
|
|
173
|
+
file?: File;
|
|
174
|
+
} | {
|
|
175
|
+
type: 'abort';
|
|
176
|
+
pkgId: string;
|
|
177
|
+
};
|
|
178
|
+
export type FromJobsMessage = {
|
|
179
|
+
type: 'progress';
|
|
180
|
+
pkgId: string;
|
|
181
|
+
entry?: string;
|
|
182
|
+
loaded: number;
|
|
183
|
+
total: number | null;
|
|
184
|
+
} | {
|
|
185
|
+
type: 'done';
|
|
186
|
+
pkgId: string;
|
|
187
|
+
entry?: string;
|
|
188
|
+
size: number;
|
|
189
|
+
} | {
|
|
190
|
+
type: 'failed';
|
|
191
|
+
pkgId: string;
|
|
192
|
+
entry?: string;
|
|
193
|
+
code: ErrorCode;
|
|
194
|
+
message: string;
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* H5P's own iframe-resizing protocol, spoken between the content and whatever embeds it. The
|
|
198
|
+
* frame sends these to `window.parent`; the element answers them. See `h5p-resizer.js` in
|
|
199
|
+
* h5p-php-library, which is the reference implementation of this side.
|
|
200
|
+
*/
|
|
201
|
+
export interface H5PResizerMessage {
|
|
202
|
+
context: 'h5p';
|
|
203
|
+
action: 'hello' | 'prepareResize' | 'resize' | 'resizePrepared' | 'ready';
|
|
204
|
+
scrollHeight?: number;
|
|
205
|
+
clientHeight?: number;
|
|
206
|
+
}
|
|
207
|
+
export type FromFrameMessage = {
|
|
208
|
+
channel: 'h5p-player';
|
|
209
|
+
type: 'ready';
|
|
210
|
+
pkgId: string;
|
|
211
|
+
} | {
|
|
212
|
+
channel: 'h5p-player';
|
|
213
|
+
type: 'xapi';
|
|
214
|
+
pkgId: string;
|
|
215
|
+
statement: unknown;
|
|
216
|
+
verb?: string;
|
|
217
|
+
} | {
|
|
218
|
+
channel: 'h5p-player';
|
|
219
|
+
type: 'finished';
|
|
220
|
+
pkgId: string;
|
|
221
|
+
statement: unknown;
|
|
222
|
+
} | {
|
|
223
|
+
channel: 'h5p-player';
|
|
224
|
+
type: 'error';
|
|
225
|
+
pkgId: string;
|
|
226
|
+
message: string;
|
|
227
|
+
} | {
|
|
228
|
+
channel: 'h5p-player';
|
|
229
|
+
type: 'relay';
|
|
230
|
+
pkgId: string;
|
|
231
|
+
payload: FromWorkerMessage;
|
|
232
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** A byte range with an inclusive end, the form HTTP uses. */
|
|
2
|
+
export interface ByteRange {
|
|
3
|
+
start: number;
|
|
4
|
+
end: number;
|
|
5
|
+
}
|
|
6
|
+
export type RangeResult = ByteRange | 'none' | 'unsatisfiable';
|
|
7
|
+
/**
|
|
8
|
+
* Parses a `Range` header against a known resource size. Only single `bytes` ranges are handled;
|
|
9
|
+
* a multi-range request is treated as absent, which is a legal response (the server may always
|
|
10
|
+
* answer `200` with the whole body).
|
|
11
|
+
*
|
|
12
|
+
* Returns `'none'` when there is no usable range and `'unsatisfiable'` when the range starts
|
|
13
|
+
* beyond the end of the resource, which the caller must answer with `416`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseRange(header: string | null | undefined, size: number): RangeResult;
|
|
16
|
+
export declare function contentRange(range: ByteRange, size: number): string;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Reader, type CreateReadableOptions } from '@zip.js/zip.js';
|
|
2
|
+
import { type RemoteSourceDescriptor, type SourceDescriptor } from './protocol';
|
|
3
|
+
import type { ByteRange } from './range';
|
|
4
|
+
/**
|
|
5
|
+
* Source adapters. One interface — a size and a byte-range read — over the three ways an archive
|
|
6
|
+
* can be reached. The Service Worker and the Jobs worker both build handles from the persisted
|
|
7
|
+
* descriptor, so neither has to know how the package was opened.
|
|
8
|
+
*/
|
|
9
|
+
export interface SourceHandle {
|
|
10
|
+
readonly descriptor: SourceDescriptor;
|
|
11
|
+
/** Total size in bytes. Known for every handle by the time one is built. */
|
|
12
|
+
readonly size: number;
|
|
13
|
+
/** Reads an inclusive byte range into memory. Callers keep these bounded. */
|
|
14
|
+
read(range: ByteRange): Promise<Uint8Array>;
|
|
15
|
+
/** Streams an inclusive byte range. Used to serve large stored entries without buffering. */
|
|
16
|
+
stream(range: ByteRange): Promise<ReadableStream<Uint8Array>>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Decides how an archive can be reached.
|
|
20
|
+
*
|
|
21
|
+
* A single `GET` with `Range: bytes=0-0` answers both questions that matter: a host without CORS
|
|
22
|
+
* rejects it at the network layer, and a host that ignores `Range` answers `200` with the whole
|
|
23
|
+
* body (which is why the body is cancelled as soon as the headers arrive — a `HEAD` would prove
|
|
24
|
+
* neither, and is often blocked outright).
|
|
25
|
+
*/
|
|
26
|
+
export declare function probeSource(url: string, signal?: AbortSignal): Promise<RemoteSourceDescriptor>;
|
|
27
|
+
/**
|
|
28
|
+
* Builds a handle from a persisted descriptor. A `file` descriptor needs the `File` itself, which
|
|
29
|
+
* only the page holds — the caller fetches it from the page before calling this.
|
|
30
|
+
*/
|
|
31
|
+
export declare function openSource(pkgId: string, descriptor: SourceDescriptor, file?: Blob, options?: {
|
|
32
|
+
/**
|
|
33
|
+
* Open a chunked archive that is still downloading. Only the forward index can address such
|
|
34
|
+
* a handle — its size is what has arrived so far — so this is for the reader built from that,
|
|
35
|
+
* never for zip.js, which needs the end of the file.
|
|
36
|
+
*/
|
|
37
|
+
partial?: boolean;
|
|
38
|
+
}): Promise<SourceHandle>;
|
|
39
|
+
/**
|
|
40
|
+
* Presents a `SourceHandle` as a zip.js reader. zip.js only ever asks for the central directory
|
|
41
|
+
* and local headers through this, so reads stay small even for a multi-gigabyte archive.
|
|
42
|
+
*/
|
|
43
|
+
export declare class SourceReader extends Reader<SourceHandle> {
|
|
44
|
+
private readonly handle;
|
|
45
|
+
size: number;
|
|
46
|
+
constructor(handle: SourceHandle);
|
|
47
|
+
readUint8Array(index: number, length: number): Promise<Uint8Array>;
|
|
48
|
+
/**
|
|
49
|
+
* Streams a span in one read instead of paging through it.
|
|
50
|
+
*
|
|
51
|
+
* zip.js only calls `readUint8Array` when a reader does not provide this, and it then walks the
|
|
52
|
+
* entry in 64 kB steps. Over a `File` each step is a free `slice()`; over HTTP each one is a
|
|
53
|
+
* request, so inflating a 218 MB video meant about 3,500 round trips to the origin — slow
|
|
54
|
+
* enough to look like a hang, and enough to get throttled. One ranged request covers it.
|
|
55
|
+
*/
|
|
56
|
+
createReadable(options?: CreateReadableOptions): ReadableStream<Uint8Array>;
|
|
57
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ByteRange } from './range';
|
|
2
|
+
/**
|
|
3
|
+
* Restricts a byte stream to an inclusive range without buffering it. Used to answer a `Range`
|
|
4
|
+
* request from an entry the cache holds in one piece: the body is sliced as it flows rather than
|
|
5
|
+
* read into memory and cut.
|
|
6
|
+
*/
|
|
7
|
+
export declare function sliceStream(source: ReadableStream<Uint8Array>, range: ByteRange): ReadableStream<Uint8Array>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FrameAssets } from '../shared/protocol';
|
|
2
|
+
export declare function sanitizeOrigins(values: readonly string[] | undefined): string[];
|
|
3
|
+
export interface FrameDocumentOptions {
|
|
4
|
+
pkgId: string;
|
|
5
|
+
/** Absolute URL of the package root on the virtual file server, without a trailing slash. */
|
|
6
|
+
virtualRoot: string;
|
|
7
|
+
assets: FrameAssets;
|
|
8
|
+
nonce: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Extra origins the host vouches for, for content that reaches somewhere this package cannot
|
|
12
|
+
* know about — a tenant's own Panopto or Echo360 host, an in-house CDN.
|
|
13
|
+
*/
|
|
14
|
+
allowOrigins?: string[];
|
|
15
|
+
}
|
|
16
|
+
export declare function buildContentSecurityPolicy(options: FrameDocumentOptions): string;
|
|
17
|
+
export declare function buildFrameDocument(options: FrameDocumentOptions): string;
|
|
18
|
+
/** A fresh nonce per response: reusing one across responses would defeat the point of having it. */
|
|
19
|
+
export declare function createNonce(): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Service Worker half of the player: a virtual file server over a zip that is never
|
|
3
|
+
* extracted to disk, plus the frame document it synthesizes for the runtime to live in.
|
|
4
|
+
*
|
|
5
|
+
* It is deliberately stateless. Browsers terminate a worker between events, so every handler
|
|
6
|
+
* rebuilds what it needs from the `packages` table and the chunk store, and anything that cannot
|
|
7
|
+
* finish inside one event — downloading an archive, inflating a large entry — is handed to the
|
|
8
|
+
* page-side Jobs worker instead.
|
|
9
|
+
*/
|
|
10
|
+
export interface MountOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Override the route base. Only useful when a host mounts the handlers into a worker whose
|
|
13
|
+
* scope is not where the routes should live.
|
|
14
|
+
*/
|
|
15
|
+
scope?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function mountH5P(worker: ServiceWorkerGlobalScope, options?: MountOptions): void;
|