diff --git a/docs/hosting-packages.md b/docs/hosting-packages.md
index bccf1a8..ea14d1d 100644
--- a/docs/hosting-packages.md
+++ b/docs/hosting-packages.md
@@ -87,22 +87,30 @@ For large packages, also support ranged requests and expose their headers:
```text
Accept-Ranges: bytes
-Access-Control-Expose-Headers: Content-Range, Accept-Ranges, Content-Length
+Access-Control-Expose-Headers: Content-Range, Accept-Ranges, Content-Length, ETag
```
With them, a large package starts playing after a few kilobytes. Without them, it is downloaded whole before anything plays, which still works. A simple range such as `bytes=0-99` needs no CORS preflight, so a host that ignores `OPTIONS` requests is not a problem.
-GitHub Pages meets both requirements as it comes. The player's [setup guide](https://github.com/missing-elements/h5p-offline-player/blob/main/h5p-player-setup.md#requirements) describes exactly what it checks.
+An `ETag` or `Last-Modified` header lets the player tell a file republished at the same address from the old one, and a visitor who comes back reads almost nothing of the package again. The frame can read `Last-Modified` from any host, and an `ETag` only when the host lists it in `Access-Control-Expose-Headers`, as above.
+
+GitHub Pages meets both requirements as it comes. The player's [setup guide](https://github.com/missing-elements/h5p-offline-player/blob/main/docs/setup.md#requirements) lists exactly what it needs.
## Packages and library files
An H5P package normally includes the libraries it needs. Exports from H5P.com and h5p.org usually contain only `content/` and expect the site they came from to provide the libraries.
-Embed My plays those too. The player keeps a bundle of the H5P hub's libraries for every content type the hub serves, about 14 MB, served from the player origin and downloaded once per browser, and a package that ships without its libraries takes them from there. It asks no other site for libraries. See [Privacy and security](privacy-and-security.md).
+Embed My plays those too. The player keeps a pack of the H5P hub's libraries for every content type the hub serves, one file per library, served from the player origin. A package that ships without its libraries downloads only the ones it needs from there, and the browser keeps each one for the next package that uses it. It asks no other site for libraries. See [Privacy and security](privacy-and-security.md).
+
+A package written for an older version of a library plays too, and so does one that carries an older version. The pack has the newest version of each library, the player plays that one, and before the activity starts it converts the content with the library's own upgrade steps, as Moodle, WordPress and the other H5P plugins do when a site installs a newer version. If a step fails, the activity plays as it was written. The lines under the preview name the libraries that played newer than the package carries them. To play them exactly as exported instead, add `&refresh-libraries=off` to the end of the `src` in the snippet.
+
+If the preview still reports missing libraries, the package needs a library the pack does not carry: a new major version the H5P hub does not serve yet, or a library written for a single site. Export the activity as a complete package, including libraries, when your authoring tool offers that option. The player's [libraries guide](https://github.com/missing-elements/h5p-offline-player/blob/main/docs/libraries.md) explains how library sources work.
+
+### Content made for newer libraries
-A package written for an older version of a library plays too. The bundle carries the newest version of each library, and before the activity starts the player converts the content with that library's own upgrade steps, as Moodle, WordPress and the other H5P plugins do when a site installs a newer version. If a step fails, the activity plays as it was written.
+H5P.com gets a library's new version weeks before the H5P hub serves it, so an activity made there in those weeks can name a version that nobody else has yet. The player plays it at the newest version of that library the pack has, and the lines under the preview say which.
-If the preview still reports missing libraries, the package needs a library the bundle does not carry. Usually it is newer than the bundle: the bundle is rebuilt from the H5P hub every week, and a later release usually carries it. Less often it is a library written for a single site. Export the activity as a complete package, including libraries, when your authoring tool offers that option. The player's [libraries guide](https://github.com/missing-elements/h5p-offline-player/blob/main/docs/libraries.md) explains how library sources work.
+Most new versions only add fields, which the older version ignores. Where one changed the shape of the content, a part of the activity can be missing or drawn wrong, so play through every part of it. To refuse such a package instead, add `&downgrade-libraries=off` to the end of the `src` in the snippet. The pack follows the H5P hub, so once the hub serves the newer version, a later release of the player plays the activity as made.
## How big can a package be?
diff --git a/docs/privacy-and-security.md b/docs/privacy-and-security.md
index 6170653..6b2227a 100644
--- a/docs/privacy-and-security.md
+++ b/docs/privacy-and-security.md
@@ -12,11 +12,11 @@ Embed only packages you trust, as you would any script.
## When the frame is opened on its own
-The frame's address can also be opened directly, as a link, which [Troubleshooting](troubleshooting.md) suggests for some app browsers. Opened that way, a package from another site would share the player origin's own storage with every other package opened like that, so the page plays it as data: none of the package's own scripts run, its libraries come from the player's bundle, and its content is checked against those libraries' field definitions before it is shown, as the H5P plugins for Moodle, WordPress and Drupal check an upload. A package that cannot play without its own copy of a library waits for a click, and the page names the site it comes from before running it.
+The frame's address can also be opened directly, as a link, which [Troubleshooting](troubleshooting.md) suggests for some app browsers. Opened that way, a package from another site would share the player origin's own storage with every other package opened like that, so the page plays it as data: none of the package's own scripts run, its libraries come from the player's own library pack, and its content is checked against those libraries' field definitions before it is shown, as the H5P plugins for Moodle, WordPress and Drupal check an upload. A package that cannot play without its own copy of a library waits for a click, and the page names the site it comes from before running it.
## What the frame can do to your page
-The snippet gives the frame fullscreen, and the microphone and camera for the content types that record or film: Audio Recorder, Speak the Words and AR Scavenger. The browser asks the visitor before the frame uses either, and for an activity that does neither you can take `microphone; camera` out of the `allow` attribute. The frame gets no location. It cannot read or change your page. But browsers let any iframe navigate the whole tab once the visitor has clicked inside it, so a hostile package could send a visitor who clicks on it to another address. That is one more reason to embed only packages you trust. A site that wants to rule it out can add `sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads"` to the iframe; test the activity afterwards, because a sandbox also stops the frame's own "Open it on its own" link.
+The snippet gives the frame fullscreen, and the microphone and camera. Audio Recorder and Speak the Words record with the microphone; the camera is for AR Scavenger, which the player [does not run](troubleshooting.md#an-ar-scavenger-activity-shows-only-its-title). The browser asks the visitor before the frame uses either, and for an activity that does neither you can take `microphone; camera` out of the `allow` attribute. The frame gets no location. It cannot read or change your page. But browsers let any iframe navigate the whole tab once the visitor has clicked inside it, so a hostile package could send a visitor who clicks on it to another address. That is one more reason to embed only packages you trust. A site that wants to rule it out can add `sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads"` to the iframe; test the activity afterwards, because a sandbox also stops the frame's own "Open it on its own" link.
The script line, `h5p-resizer.js`, is the one part of the snippet that runs on your page itself, with the same rights as your own scripts. It is about sixty lines, it only sets an iframe's height when that frame reports it, and you can read it at the address in the snippet. Leave it out and the frame keeps the height in the `style` attribute.
@@ -24,7 +24,7 @@ The script line, `h5p-resizer.js`, is the one part of the snippet that runs on y
No learner records. The embed page never turns on the player's resume feature, so answers and progress are not saved anywhere, and reloading the page starts the activity over. Embed My stores nothing on its side either.
-The visitor's browser does cache the package's files, and keeps a list of the packages played there with each one's address, title, licence details, language and when it was last played (Cache Storage and IndexedDB on the player's origin, kept separate per embedding site), so the next load is faster. The list shows which activities were opened on that device. Clearing the browser's site data removes both.
+The visitor's browser does cache the package's files and the files of the player's library pack it used, and keeps a list of the packages played there with each one's address, title, licence details, language and when it was last played (Cache Storage and IndexedDB on the player's origin, kept separate per embedding site), so the next load is faster. The list shows which activities were opened on that device. Clearing the browser's site data removes both.
## Cookies and tracking
@@ -34,7 +34,7 @@ The pages and the script are served by GitHub Pages. Like any web host, GitHub s
H5P content itself may contact services named by the package, such as YouTube, Vimeo, Google Fonts, MathJax, or an organization-hosted media service. Review a package and its third-party content before publishing it.
-The frame asks no one else for libraries. A package exported without its libraries takes them from a bundle on the player origin, and from nowhere else.
+The frame asks no one else for libraries. A package exported without its libraries takes the ones it needs from the library pack on the player origin, and from nowhere else.
## Data protection
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index e828e9e..392492a 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -13,7 +13,11 @@ Check that:
## The activity works in my LMS but not in Embed My
-An export without its libraries normally plays anyway: the player takes them from its own bundle of the H5P hub's libraries, and upgrades content written for older library versions before it starts. If the preview still names missing libraries, the package needs one the bundle does not carry: usually a version newer than the bundle, less often one written for a single site. Re-export a complete package, including libraries. See [Packages and library files](hosting-packages.md#packages-and-library-files).
+An export without its libraries normally plays anyway: the player takes the ones it needs from its own pack of the H5P hub's libraries, and upgrades content written for older library versions before it starts. Content made for a newer version than the pack has plays at the newest one it has, and the preview says so. If the preview still names missing libraries, the package needs one the pack does not carry: a new major version, or a library written for a single site. Re-export a complete package, including libraries. See [Packages and library files](hosting-packages.md#packages-and-library-files).
+
+## An AR Scavenger activity shows only its title
+
+The player does not run AR Scavenger. It writes its 3D and AR scripts into the page as inline scripts, which the frame's security policy refuses, so the activity shows its title and no scene, here and wherever the player runs.
## The activity is cut off or leaves blank space
@@ -37,7 +41,7 @@ The site's Content Security Policy must allow `frame-src https://embed-my.github
## Other problems
-The player's [troubleshooting table](https://github.com/missing-elements/h5p-offline-player/blob/main/h5p-player-setup.md#troubleshooting) covers error codes and less common symptoms in detail, such as formulas shown as raw LaTeX or a missing copyright button.
+The player's [troubleshooting guide](https://github.com/missing-elements/h5p-offline-player/blob/main/docs/troubleshooting.md) covers error codes and less common symptoms in detail, such as formulas shown as raw LaTeX or a missing copyright button.
## Reporting a problem
diff --git a/e2e/site.spec.ts b/e2e/site.spec.ts
index ab21ea6..af465c0 100644
--- a/e2e/site.spec.ts
+++ b/e2e/site.spec.ts
@@ -49,7 +49,10 @@ test('the home page previews a package and writes its snippet', async ({ page })
await expect(checks.first()).toContainText('Do you know what just happened? · Question Set · CC0 1.0 · by missing-elements')
await expect(checks.filter({ hasText: 'Reachable from a browser' })).toHaveAttribute('data-tone', 'ok')
await expect(checks.filter({ hasText: /Streams in place|Downloaded whole/ })).toContainText('MB')
- await expect(checks.filter({ hasText: 'Carries its libraries' })).toHaveAttribute('data-tone', 'ok')
+ // The quiz carries its libraries. A player that plays them from its own pack (player origin with
+ // `pack`) plays the older ones at the pack's newer versions and says so, which is worth knowing,
+ // not a fault; one that attaches a whole bundle plays them as exported.
+ await expect(checks.filter({ hasText: 'Carries its libraries' })).toHaveAttribute('data-tone', /^(ok|info)$/)
await expect(checks.filter({ hasText: /Ready in \d+\.\d s/ })).toHaveCount(1)
expect(problems, problems.join('\n')).toEqual([])
@@ -62,7 +65,7 @@ test('the lines under the preview say where missing libraries came from', async
await expect(page.locator('#stage')).toHaveAttribute('data-state', 'ready', { timeout: 60_000 })
const line = page.locator('#checks li').filter({ hasText: 'Exported without its libraries' })
await expect(line).toHaveAttribute('data-tone', 'info')
- await expect(line).toContainText("the player's bundle")
+ await expect(line).toContainText("the player added the ones it needs from its library pack")
await expect(line.getByRole('link', { name: 'About library files' })).toHaveAttribute('href', '/docs/hosting-packages#packages-and-library-files')
})
diff --git a/index.html b/index.html
index c3cfd93..77cf850 100644
--- a/index.html
+++ b/index.html
@@ -324,7 +324,7 @@