@vyonio/player 0.0.0-stage → 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ ## 0.2.1 — 2026-10-15
4
+
5
+ ### Fixed
6
+
7
+ - Prefer standard or WebKit element fullscreen when available, then native video
8
+ fullscreen on supported iOS video elements, retaining Plyr's CSS fallback.
9
+ - Request fullscreen synchronously from an explicit user action. Opening,
10
+ metadata, playing and autoplay do not enter fullscreen automatically.
11
+ - Recover from unsupported or rejected fullscreen entry; restore inline controls
12
+ on exit and clean up listeners and fullscreen state on destroy/remount.
13
+ Delayed WebKit exit and element entry completing after teardown are covered.
14
+ - Respect disabled or omitted fullscreen controls, including keyboard and
15
+ double-click actions.
16
+ - Remove the unconditional 192px mount minimum. Preserve explicit host sizing
17
+ even when SDK CSS loads last, intrinsic sizing for width-only hosts, source
18
+ aspect ratios, letterboxing, and responsive controls without remounting.
19
+
20
+ ### Validation and limitations
21
+
22
+ - 196 unit/release-script tests and 36 offline browser regression tests passed.
23
+ Browser tests use real Plyr and the built SDK stylesheet in headless Chrome;
24
+ fullscreen capability APIs are simulated. All five requested viewport widths
25
+ (320, 375, 390, 844 and 1400px), rotation, and lifecycle races are covered.
26
+ - Build and independent tarball consumer installation/type-check/build passed.
27
+ - Physical iPhone fullscreen with a supported protected video is **not verified**.
28
+ Apple iPhone Simulator was not used. This release does not claim FairPlay/DRM
29
+ device certification. Native fullscreen uses system controls and cannot render
30
+ HTML badges, the popularity histogram or custom HTML overlays.
31
+
32
+ ### Consumer migration
33
+
34
+ Upgrade the locked `@vyonio/player` dependency to `0.2.1`. Remove the Rails
35
+ `nativeVideoFullscreen()` click-interception helper and its lifecycle wiring.
36
+ After verifying sizing with the released package, remove the SDK-specific
37
+ `min-height: 0` workaround. Keep application container sizing, modal scrolling
38
+ and course-navigation fixes.
package/README.md CHANGED
@@ -1,3 +1,347 @@
1
- # Temporary Holding Version
1
+ # @vyonio/player
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Shaka Player engine with Plyr chrome for Vyonio DRM video playback. Plyr styles and the control-icon SVG sprite ship with the package, so playback does not depend on the Plyr CDN or require it in the host's Content Security Policy. If Plyr fails to initialize, the player falls back to native `<video controls>`.
4
+
5
+ > **Release availability:** source checkouts can be ahead of npm. Check available
6
+ > versions with `npm view @vyonio/player versions`. For an unpublished checkout,
7
+ > use `npm run verify:package` or a local `npm pack` tarball. Customer setup is in
8
+ > the [public npm guide](https://developers.vyonio.com/npm-player/) and
9
+ > [shared backend guide](https://developers.vyonio.com/protected-playback/).
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install @vyonio/player
15
+ ```
16
+
17
+ The ESM package bundles Shaka and Plyr and exports TypeScript declarations.
18
+ Import `@vyonio/player/style.css` once in your application. The default API is
19
+ `https://api.vyonio.com`; set `apiBase` only when using another environment.
20
+
21
+ For a clean repository checkout, first run `npm ci --prefix packages/player`,
22
+ then `npm ci --prefix <app>` for each app you use (`client`, `course`, `player`,
23
+ `dash`). App build and dev scripts build the shared package before starting;
24
+ they do not install dependencies. Run `npm --prefix packages/player run verify:package` to pack, install, type-check, and build an independent consumer.
25
+ `npm pack` runs the library build through `prepack`. Local verification prepares
26
+ a release artifact; publishing npm and hosted artifacts is a separate step.
27
+
28
+ ## Usage
29
+
30
+ ### Public video
31
+
32
+ ```js
33
+ import { VyonioPlayer } from '@vyonio/player';
34
+ import '@vyonio/player/style.css';
35
+
36
+ const { player, destroy } = await VyonioPlayer.mount(element, {
37
+ apiBase: 'http://localhost:3000',
38
+ embedUuid: '550e8400-e29b-41d4-a716-446655440000',
39
+ autoplay: false,
40
+ muted: false,
41
+ loop: false,
42
+ controls: true,
43
+ controlItems: ['play-large', 'play', 'progress', 'current-time', 'fullscreen'],
44
+ settingsItems: ['quality'],
45
+ language: 'cs',
46
+ accentColor: '#7c3aed',
47
+ badge: {
48
+ text: 'AI-assisted',
49
+ position: 'top-right',
50
+ style: 'accent'
51
+ },
52
+ startSeconds: 0
53
+ });
54
+ ```
55
+
56
+ The optional playback settings are intended for hosted embeds as well as
57
+ direct integrations. Browsers generally require `muted: true` when autoplay is
58
+ enabled. A rejected autoplay attempt leaves the player ready for manual play.
59
+ `controlItems` accepts Plyr control names and is ignored when `controls` is
60
+ false. `settingsItems` accepts `quality` and `speed`; it defaults to
61
+ `['quality']`, so playback speed is opt-in. An empty array removes the settings
62
+ menu, and unavailable quality choices hide automatically. `language` accepts
63
+ `en` and `cs` and defaults to English. Unsupported languages fall back to
64
+ English. `accentColor` accepts only a six-digit hex color. Badge text is rendered
65
+ as plain text inside Plyr's fullscreen container and is limited to 64
66
+ characters; positions are the four corners and styles are `dark`, `light`, or
67
+ `accent`.
68
+
69
+ ### Fullscreen and responsive hosts
70
+
71
+ Fullscreen uses element fullscreen when the mounted video's document supports
72
+ it, then native video fullscreen (`fullscreen.iosNative`) when available, and
73
+ Plyr's full-window CSS fallback otherwise. Entry happens only on an explicit
74
+ fullscreen action; mounting, metadata, playing and autoplay do not request it.
75
+ If native entry fails, the existing CSS fallback remains usable. Exiting keeps
76
+ inline playback and controls available. `controls: false` or a `controlItems`
77
+ list without `fullscreen` also disables keyboard and double-click entry.
78
+ Consumers do not need a click interceptor on Plyr's private DOM selectors.
79
+
80
+ Native iPhone fullscreen uses system video controls. HTML badges, the popularity
81
+ histogram and other HTML overlays are not rendered in that system view; custom
82
+ quality/settings UI is available again after returning inline. Do not depend on
83
+ HTML badges for mandatory watermarking. See [Plyr's fullscreen options](https://github.com/sampotts/plyr#options)
84
+ and [Apple's native video API](https://developer.apple.com/documentation/webkitjs/htmlvideoelement/1633500-webkitenterfullscreen).
85
+
86
+ The mount fills a sized host, including heights below 192px, with no minimum
87
+ height override required. Host rules win over SDK size defaults even when the
88
+ SDK stylesheet is loaded last. A width-only host uses the video's intrinsic
89
+ aspect ratio once metadata is available (16:9 before that). Media uses
90
+ `object-fit: contain`, so differing host/media ratios produce intentional
91
+ letterboxing. Resizing and orientation changes do not require a remount.
92
+
93
+ ```css
94
+ /* Choose a host size appropriate to your layout. */
95
+ .video-slot {
96
+ width: 100%;
97
+ aspect-ratio: 16 / 9;
98
+ border-radius: 0.75rem;
99
+ overflow: hidden;
100
+ }
101
+ ```
102
+
103
+ ### Verify fullscreen and sizing changes
104
+
105
+ ```sh
106
+ # From packages/player, with Node 22.12+ and installed dependencies:
107
+ npm test
108
+ # Uses installed Google Chrome. To provision it when absent:
109
+ # npx playwright install chrome
110
+ npm run test:browser
111
+ npm run verify:package
112
+ ```
113
+
114
+ Browser regressions build and load the shipped `dist/style.css` after host CSS
115
+ with real Plyr markup. They cover direct/nested 16:9 hosts at viewport widths
116
+ 320, 375, 390, 844 and 1400px, explicit 120px height, width-only hosts, metadata
117
+ aspect-ratio changes, portrait/landscape resizing, visible usable controls,
118
+ capability selection, synchronous entry, fallback, exit and teardown races.
119
+ They block non-local requests and need no live token/media/DRM/analytics/payment
120
+ services. Browser fullscreen APIs are simulated; desktop mobile emulation does
121
+ not establish that native iPhone fullscreen or protected playback works.
122
+
123
+ Device acceptance remains outstanding: verify on a **physical iPhone** using a supported
124
+ protected video: open/play stays inline; tapping fullscreen opens system video
125
+ fullscreen; exit restores inline video and working controls; repeat entry,
126
+ rotation, closing while fullscreen, source switching and remounting work.
127
+ Record device/iOS/Safari versions and the protected asset/DRM mode without
128
+ recording secrets. Physical iPhone verification has **not** been completed in
129
+ this change.
130
+
131
+ See the shipped [changelog](./CHANGELOG.md) for the 0.2.1 fixes and known
132
+ verification limits. The 0.2.1 patch follows the [repository release procedure](https://gitlab.com/buildio/playground/vyonio.com/-/blob/main/docs/player-release.md);
133
+ 0.2.0 remains immutable. Package version files and the `player-v0.2.1` tag are
134
+ recorded by that procedure only after publication and registry verification.
135
+
136
+ After upgrading the Rails consumer's locked dependency to 0.2.1, remove
137
+ `nativeVideoFullscreen()` and its lifecycle wiring, then remove its SDK-specific
138
+ `min-height: 0` workaround after sizing is verified against the released package.
139
+ Keep normal container sizing, modal scrolling and course-navigation fixes in
140
+ the application.
141
+
142
+ ### Popularity histogram
143
+
144
+ Pass equal-duration popularity buckets to render a YouTube-like engagement
145
+ curve above the seek bar (white top stroke, fade-to-transparent fill). The
146
+ overlay is non-interactive and only appears while the progress control is
147
+ hovered or scrubbed:
148
+
149
+ ```js
150
+ await VyonioPlayer.mount(element, {
151
+ apiBase: 'http://localhost:3000',
152
+ embedUuid: '550e8400-e29b-41d4-a716-446655440000',
153
+ popularityHistogram: {
154
+ buckets: [12, 18, 31, 24, 15]
155
+ }
156
+ });
157
+ ```
158
+
159
+ Each bucket spans an equal portion of the video duration, and heights are
160
+ normalized against the largest value. The player also accepts the same array
161
+ from `popularity_histogram.buckets` in the playback descriptor when a backend
162
+ provides it. Explicit mount-option buckets take precedence. The current Vyonio
163
+ Platform descriptor does not supply popularity buckets; aggregation and API
164
+ delivery are a separate backend feature.
165
+
166
+ HTML badges stay visible in normal Plyr fullscreen, but cannot be guaranteed in
167
+ native video fallback, Picture in Picture, AirPlay, casting, or downloaded
168
+ media. Use a burned-in watermark for compliance-critical or tamper-resistant
169
+ marking.
170
+
171
+ Fetches the video descriptor from `{apiBase}/v1/embed/{embedUuid}`, attaches a `<video>` element with Plyr controls (or native controls on Plyr failure), configures Widevine DRM, and loads the DASH manifest. Safari/FairPlay is scaffolded and throws until Apple FPS credentials are deployed.
172
+
173
+ The returned handle also exposes media controls used by the hosted iframe API:
174
+
175
+ ```js
176
+ const handle = await VyonioPlayer.mount(element, options);
177
+
178
+ await handle.play();
179
+ handle.pause();
180
+ handle.seek(30);
181
+ handle.setMuted(true);
182
+ console.log(handle.getState());
183
+ ```
184
+
185
+ `seek(seconds)` accepts a nonnegative finite number and clamps it to the known
186
+ duration. `getState()` returns `currentTime`, `duration`, `paused`, `ended`, and
187
+ `muted`. The underlying `HTMLVideoElement` is available as `mediaElement` for
188
+ advanced event handling.
189
+
190
+ If the descriptor includes a `telemetry` endpoint and signed context, the
191
+ player automatically emits batched `start`, `heartbeat`, `pause`, `seek`,
192
+ `end`, `leave`, and `error` events. Telemetry is best-effort: failures and an
193
+ absent telemetry descriptor never interrupt playback. Pipeline credentials are
194
+ held by the Platform API and are never exposed to the browser.
195
+
196
+ Every mounted player uses a fresh random session UUID. Heartbeat, pause, end,
197
+ and leave events close continuous watched spans with millisecond start/end,
198
+ duration, watched time, buffering time, and playback rate. A seek closes the
199
+ preceding span and records only its origin and destination, so skipped content
200
+ is not counted as watched. Telemetry contains no user/member ID, stable device
201
+ ID, fingerprint, page/referrer URL, or cross-session cookie.
202
+
203
+ ### Protected video (playback JWT)
204
+
205
+ For a same-origin customer endpoint:
206
+
207
+ ```js
208
+ const handle = await VyonioPlayer.mount(element, {
209
+ embedUuid: 'your-embed-uuid',
210
+ tokenEndpoint: '/api/playback-token',
211
+ tokenHeaders: { 'X-CSRF-Token': csrfToken }
212
+ });
213
+ ```
214
+
215
+ The endpoint receives `POST` JSON `{ embedUuid }` with same-origin credentials
216
+ and must authenticate the viewer and recheck entitlement on every request.
217
+ Return JSON `{ token, expires_in: 600 }` with `Cache-Control: no-store`.
218
+ Keep the Vyonio API key only on your server. Customers never need Vyonio's JWT signing secret.
219
+ `tokenEndpoint` and `getPlaybackToken` are mutually exclusive. Endpoint URLs
220
+ must be same-origin; `tokenHeaders` cannot include Authorization. Token
221
+ acquisition times out after 15 seconds and rejects with `PlaybackTokenError`
222
+ (`PLAYBACK_TOKEN_FAILED` or `PLAYBACK_TOKEN_TIMEOUT`).
223
+
224
+ Pass `tokenEndpoint` or `getPlaybackToken` to attach a short-lived playback JWT before the descriptor fetch. The player:
225
+
226
+ - Sends `Authorization: Bearer <token>` on the descriptor request and all Shaka network requests (via a request filter)
227
+ - Appends `access_token=<token>` to the DASH manifest URL (for AirPlay / devices that cannot send headers)
228
+ - Replaces `access_token` on subsequent platform manifest, segment, and license request URLs after each token refresh
229
+ - Schedules automatic refresh from the JWT `exp` claim, **120 seconds before expiry**
230
+ - Starts refresh scheduling only after the manifest loads successfully
231
+ - On refresh failure, marks the mount and video elements with `data-state="error"`, emits a `vyonio-player-error` event from the mount element, calls optional `onError`, then destroys the player and removes the video element
232
+
233
+ ```js
234
+ const { player, destroy } = await VyonioPlayer.mount(element, {
235
+ apiBase: 'http://localhost:3000',
236
+ embedUuid: '550e8400-e29b-41d4-a716-446655440000',
237
+ onError: ({ code, message, error }) => {
238
+ // code is PLAYBACK_AUTH_REFRESH_FAILED for token refresh failures.
239
+ showPlaybackError(message);
240
+ },
241
+ getPlaybackToken: async () => {
242
+ const res = await fetch('/api/playback-token', {
243
+ method: 'POST',
244
+ credentials: 'same-origin',
245
+ cache: 'no-store',
246
+ redirect: 'error',
247
+ headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken },
248
+ body: JSON.stringify({ embedUuid: '550e8400-e29b-41d4-a716-446655440000' })
249
+ });
250
+ if (!res.ok) throw new Error(`Token mint failed: ${res.status}`);
251
+ const { token } = await res.json();
252
+ return token;
253
+ }
254
+ });
255
+ ```
256
+
257
+ The `vyonio-player-error` event is a `CustomEvent` with the same `{ code, message, error }` object in `event.detail`. This lets applications surface an authentication or entitlement error even when they do not provide `onError`.
258
+
259
+ When both authentication options are omitted, behavior matches the public path (no Bearer header, no manifest query param, no refresh timer).
260
+
261
+ ## Playback descriptor: captions and chapters
262
+
263
+ The embed descriptor (`GET {apiBase}/v1/embed/{embedUuid}`) may include:
264
+
265
+ ```json
266
+ {
267
+ "captions": [
268
+ {
269
+ "language": "cs",
270
+ "label": "Čeština",
271
+ "kind": "subtitles",
272
+ "vtt_url": "https://media.example/caption_tracks/1/vtt",
273
+ "default": true
274
+ }
275
+ ],
276
+ "chapters": [
277
+ { "title": "Intro", "start_seconds": 0 },
278
+ { "title": "Main", "start_seconds": 42 }
279
+ ]
280
+ }
281
+ ```
282
+
283
+ - **`captions`**: optional array of sidecar WebVTT tracks. Each entry supplies `language`, `label`, `kind` (`subtitles` or `captions`), authenticated `vtt_url`, and optional `default` (boolean). When `getPlaybackToken` is used, the player appends `access_token` to each `vtt_url` the same way it does for the DASH manifest.
284
+ - **`chapters`**: optional array of chapter cues with `title` and `start_seconds`. The player builds an in-memory chapters WebVTT track for Plyr's chapter menu; cue end times are the next chapter start or video duration.
285
+
286
+ When either field is absent or empty, the corresponding Plyr control stays idle.
287
+
288
+ ## Tests
289
+
290
+ ```bash
291
+ npm test
292
+ ```
293
+
294
+ ## React and SSR
295
+
296
+ The browser engine uses DOM globals. Import `@vyonio/player` dynamically inside
297
+ a client effect; do not import the engine in a server-evaluated module. Import
298
+ the CSS once through your framework's supported global CSS entry. Cancel stale
299
+ mounts, destroy their late handles, and call `destroy()` on cleanup. The
300
+ [public npm guide](https://developers.vyonio.com/npm-player/#react-and-server-rendering)
301
+ contains the complete lifecycle example.
302
+
303
+ See the [runnable customer example](../../examples/protected-playback/README.md)
304
+ for the same-origin `{ embedUuid }` request, CSRF `tokenHeaders`, server-only key,
305
+ current entitlement checks, and Node/PHP host replacement points. Its login is
306
+ local demonstration authentication, not a production identity system. Protected
307
+ access can be used with clear or DRM media; this package preserves your existing
308
+ account DRM configuration and does not provision FairPlay.
309
+
310
+ ## Maintainer releases
311
+
312
+ From the repository root, use Node 22.12+ and your own npm account with Vyonio
313
+ publish access. Commit package changes first, leave the Git index empty, and sign
314
+ in once with `npm login`. Use a named branch with Git `user.name`/`user.email`
315
+ configured and no merge/rebase in progress.
316
+
317
+ ```sh
318
+ # First publication of the existing package version:
319
+ npm --prefix packages/player run release -- current --dry-run
320
+ npm --prefix packages/player run release -- current
321
+
322
+ # Later bug-fix release:
323
+ npm --prefix packages/player run release -- patch --dry-run
324
+ npm --prefix packages/player run release -- patch
325
+ ```
326
+
327
+ Choose `minor` for a feature release or `major` for breaking changes. These commands
328
+ accept stable versions only; prereleases need a separately reviewed release.
329
+ The command builds a temporary copy of tracked package source, installs locked
330
+ dependencies, runs all package tests, and verifies the tarball in an independent
331
+ TypeScript/browser consumer. A real release asks for confirmation and uses npm's
332
+ normal login/2FA flow. Only that verified tarball is published. It then checks
333
+ registry integrity and installs the exact version from npm.
334
+
335
+ Dry runs, cancellation and failed preparation leave your working version files
336
+ unchanged, and create no commits or tags. After publication and registry
337
+ installation verification, a bumped release commits only the manifest and
338
+ lockfile, then creates an annotated `player-vX.Y.Z` tag on that exact commit.
339
+ `current` tags the existing matching-version commit without adding an empty
340
+ commit. The annotation records the npm artifact integrity. Existing local/origin
341
+ tags are never overwritten. Unrelated unstaged work stays outside the release
342
+ commit; any staged work blocks release.
343
+
344
+ Review the local release commit and tag, then push them through your normal Git
345
+ workflow. The command does not push tags or deploy hosted/docs assets. Read the
346
+ [release runbook](../../docs/player-release.md) for first-release setup, Git/tag
347
+ recovery and post-release documentation.