@cinecrew/cinecrew-player 0.1.3 β†’ 0.1.5

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/README.md CHANGED
@@ -52,8 +52,8 @@ flowchart LR
52
52
 
53
53
  | Area | Included capabilities | Designed for |
54
54
  |---|---|---|
55
- | 🎞️ **Playback** | On-demand and live media; URLs and local URIs; HLS and MPEG-TS paths on web; native VLC path; embedded YouTube playback | Movies, episodes, trailers, and channels |
56
- | πŸŽ›οΈ **Player controls** | Play/pause, seek, restart, mute, aspect ratio, lock, video-only/audio-only modes, audio tracks, playback speed, fullscreen, back | A complete control surface without hard-wiring your app navigation |
55
+ | 🎞️ **Playback** | On-demand and live direct media URLs and local URIs; HLS and MPEG-TS paths on web; native VLC path | Movies, episodes, and channels |
56
+ | πŸŽ›οΈ **Player controls** | Play/pause, seek, restart, mute, aspect ratio, lock, audio-only mode, audio tracks, playback speed, fullscreen, back | A complete control surface without hard-wiring your app navigation |
57
57
  | 🎨 **Branding** | Theme colors, radius, platform styles, replaceable icons, custom panel render slots | Match your app without forking the player |
58
58
  | πŸ“‘ **Live TV extensions** | Inline preview component; optional chat and EPG panels; recording adapter hooks | Channel browsing and live-viewing workflows |
59
59
  | πŸ”Œ **App integration** | Per-action callbacks, imperative ref API, source resolver, progress/presence/events hooks, sleep timer callback | Keep account, IPTV, analytics, and storage logic in your app |
@@ -70,12 +70,12 @@ flowchart LR
70
70
  | πŸ”’ | `lock` | Lock / unlock controls | Prevent accidental touches |
71
71
  | πŸ”Š | `mute` | Mute / unmute | Volume prop also sets initial level |
72
72
  | πŸ–ΌοΈ | `aspectRatio` | Fit / fill / stretch | Available choices depend on renderer |
73
- | πŸ”‡ | `videoOnly` | Video-only mode | Keeps video presentation while muting audio |
73
+ | πŸ”‡ | `videoOnly` | Legacy video-only visibility key | Kept for compatibility; no separate video-only button is rendered |
74
74
  | 🎧 | `audioOnly` | Audio-only presentation | Playback continues behind the audio card |
75
75
  | 🎚️ | `audioTracks` | Audio-track selection | Depends on exposed tracks / platform engine |
76
76
  | ⏩ | `playbackRate` | Playback speed | On-demand experience |
77
77
  | β›Ά | `fullscreen` | Fullscreen / promote preview | Native full-player presentation is platform-specific |
78
- | ⏺️ | `recording` | Recording controls | Requires an app recording adapter or callback |
78
+ | ⏺️ | `recording` | Recording controls | Web uses built-in MediaRecorder when supported; native uses an app recording adapter |
79
79
  | πŸ’¬ | `liveChat` | Live chat panel | Requires an adapter, render slot, or callback |
80
80
  | πŸ“… | `epg` | Electronic program guide | Requires an adapter, render slot, or callback |
81
81
  | ⏱️ | `seek` | Seek bar | Meaningful for seekable media |
@@ -89,7 +89,7 @@ flowchart LR
89
89
  |---|---|
90
90
  | 🎨 Colors and shape | `theme`: accent, background, control, surface, error colors, border radius, and native palette |
91
91
  | πŸͺ„ Icons | `icons`: provide a glyph/string, React node, or icon component; omitted icons keep CineCrew defaults |
92
- | 🧠 Per-control behavior | `actions`: override only the actions your app wants to own; built-in behavior remains the default otherwise |
92
+ | 🧠 Per-control behavior | `actions`: observe completed player actions in your app without replacing built-in behavior |
93
93
  | 🧱 Chat and EPG drawers | `drawerMode`, `drawerStyle`, and optional app-owned `renderLiveChat` / `renderEpg` views |
94
94
  | πŸ”— Source handling | `resolveSource` for share pages or host-specific resolution; direct media sources pass through unchanged |
95
95
  | πŸ“ Layout | `style`, web `className`, inline preview geometry, and `InlineLivePlayer` height |
@@ -101,7 +101,7 @@ flowchart LR
101
101
 
102
102
  | Host | Public entry | Rendering / engine path | Key considerations |
103
103
  |---|---|---|---|
104
- | 🌐 React in browser | `@cinecrew/cinecrew-player` or `@cinecrew/cinecrew-player/react` | DOM player; browser media, hls.js, patched mpegts.js; YouTube embed | Browser codec support and origin CORS still apply |
104
+ | 🌐 React in browser | `@cinecrew/cinecrew-player` or `@cinecrew/cinecrew-player/react` | DOM player; browser media, hls.js, patched mpegts.js | Browser codec support and origin CORS still apply |
105
105
  | πŸ–₯️ Electron | `@cinecrew/cinecrew-player/electron` | Same web renderer inside Electron Chromium | Chromium’s codec and network rules still apply |
106
106
  | πŸ€– React Native Android | `@cinecrew/cinecrew-player/native` or `@cinecrew/cinecrew-player/react-native` | Native React Native surface with bundled VLC adapter; Expo Video fallback where available | Native dependencies must be compiled into the app |
107
107
  | πŸ“± React Native iOS | `@cinecrew/cinecrew-player/native` or `@cinecrew/cinecrew-player/react-native` | Native React Native surface with bundled VLC adapter; Expo Video fallback where available | Native dependencies must be compiled into the app |
@@ -112,7 +112,7 @@ flowchart LR
112
112
  | MP4 / browser-native media | βœ… Browser media element | βœ… Native engine | A directly playable URL or local URI |
113
113
  | HLS (`.m3u8`) | βœ… hls.js / native HLS where available | βœ… Native engine | Origin access, valid playlist/segments, compatible codecs |
114
114
  | MPEG-TS (`.ts`) | βœ… Bundled patched MPEG-TS client, when browser conditions permit | βœ… VLC path | Browser codecs and CORS; native module availability |
115
- | YouTube watch / Shorts / `youtu.be` | βœ… Embedded YouTube player | βœ… Embedded native WebView | Video must allow embedding; network access to YouTube |
115
+ | Hosted-video or share pages | βš™οΈ Resolve to a direct media URL with `resolveSource` | βš™οΈ Resolve to a direct media URL with `resolveSource` | A watch/share page is not a media stream |
116
116
  | Local files | βœ… Platform-supported local/blob URI | βœ… Platform-supported file URI | App obtains and passes the platform-readable URI |
117
117
  | Share pages / cloud-drive pages | βš™οΈ Optional `resolveSource` | βš™οΈ Optional `resolveSource` | Your app resolves authentication and obtains a playable media URL |
118
118
 
@@ -123,7 +123,7 @@ flowchart LR
123
123
  | Layer | Technologies in this package |
124
124
  |---|---|
125
125
  | UI & API | React Β· React Native Β· TypeScript declarations |
126
- | Web playback | HTML video Β· hls.js Β· patched mpegts.js Β· YouTube IFrame API |
126
+ | Web playback | HTML video Β· hls.js Β· patched mpegts.js |
127
127
  | Native playback | VLC adapter bundled in `@cinecrew/cinecrew-player` Β· `expo-video` fallback |
128
128
  | Native UI / utilities | React Native Β· Expo config plugins Β· safe-area context Β· SVG Β· community slider |
129
129
  | Optional media utilities | Mediabunny / AC3 parsing support in relevant web playback paths |
@@ -160,9 +160,9 @@ The items below are planned for more consistent, user-facing support across plat
160
160
 
161
161
  ## Demos
162
162
 
163
- - **[React + Vite web demo](examples/web-demo)** β€” try YouTube, MPEG-TS, MP4, MKV, or a local video file. Switch between the full player (all controls and demo chat/EPG/recording adapters enabled) and the compact inline player. [Open a fresh StackBlitz copy](https://stackblitz.com/fork/github/nahushr/cinecrew-player/tree/main/examples/web-demo?startScript=dev).
163
+ - **[React + Vite web demo](examples/web-demo)** β€” try HLS, MPEG-TS, MP4, MKV, or a local video file. Switch between the full player (all controls and demo chat/EPG/recording adapters enabled) and the compact inline player. [Open a fresh StackBlitz copy](https://stackblitz.com/fork/github/nahushr/cinecrew-player/tree/main/examples/web-demo?startScript=dev).
164
164
  - **[Expo / React Native Web demo](examples/expo-web-demo)** β€” the same source tests and controls in an Expo app rendered for the web.
165
- - **[Android and iOS Expo Snack demos](examples/snack/App.js)** β€” both platform links load the same five-test native playground (YouTube, `.ts`, `.mp4`, `.mkv`, and local-file upload), with full-player and inline-player modes. Snack runs in Expo Go, which cannot load this package's custom VLC module; MPEG-TS and MKV playback should be tested in a native development build. [Expo documents this Expo Go limitation](https://docs.expo.dev/faq/#what-can-i-do-or-cannot-do-with-expo-go).
165
+ - **[Android and iOS Expo Snack demos](examples/snack/App.js)** β€” both platform links load a native playground (`.ts`, `.mp4`, `.mkv`, and local-file upload), with full-player and inline-player modes. Snack runs in Expo Go, which cannot load this package's custom VLC module; MPEG-TS and MKV playback should be tested in a native development build. [Expo documents this Expo Go limitation](https://docs.expo.dev/faq/#what-can-i-do-or-cannot-do-with-expo-go).
166
166
 
167
167
  The React/Vite demo is self-contained and installs the released `@cinecrew/cinecrew-player` package, so its StackBlitz link works from the `examples/web-demo` subdirectory. Its MPEG-TS button uses a small same-origin H.264/AAC fixture to exercise the TS parser without relying on an external server's CORS configuration. The Expo Web demo uses this repository's package source (`file:../..`) so contributors can test unreleased changes locally. Snack loads the native example from this repository's `main` branch and installs `@cinecrew/cinecrew-player` from npm. The React DOM entry resolves to the browser renderer; it does not evaluate React Native or VLC code. The Expo native entry bundles VLC into the same installed package. External media hosts must allow browser CORS requests; format/codec support also depends on the browser. MKV playback is generally more reliable through the native VLC adapter than a browser video element.
168
168
 
@@ -180,7 +180,7 @@ npm install
180
180
  npm run web
181
181
  ```
182
182
 
183
- > Direct media sources are passed through to the platform engine. A page/share URL is not necessarily a playable media source; use `resolveSource` to resolve it. The player does not rewrite protocols, proxy media, or impose host-specific CORS rules.
183
+ > The player has no built-in YouTube playback or embed mode. Pass a direct playable media URL or local URI; page/share URLs (including YouTube watch pages) must be resolved by your app to a direct media source before playback. The player does not rewrite protocols, proxy media, or impose host-specific CORS rules.
184
184
 
185
185
  ## Install
186
186
 
@@ -198,9 +198,9 @@ The package has one public player API with a renderer selected for the host. The
198
198
 
199
199
  | Host app | Import | Renderer / playback |
200
200
  | --- | --- | --- |
201
- | React DOM in a browser | `@cinecrew/cinecrew-player` or `@cinecrew/cinecrew-player/react` | HTML video, hls.js, patched mpegts.js, and the browser YouTube embed. |
201
+ | React DOM in a browser | `@cinecrew/cinecrew-player` or `@cinecrew/cinecrew-player/react` | HTML video, hls.js, and patched mpegts.js. |
202
202
  | React DOM inside Electron (macOS `.dmg`, Windows `.exe`) | `@cinecrew/cinecrew-player/electron` | Same renderer as React web, using Electron's Chromium media stack. No WebView or custom Electron IPC bridge is needed. |
203
- | React Native Android / iOS | `@cinecrew/cinecrew-player` or `@cinecrew/cinecrew-player/react-native` | React Native UI with the bundled VLC adapter and Expo video fallback; YouTube uses `react-native-webview`. |
203
+ | React Native Android / iOS | `@cinecrew/cinecrew-player` or `@cinecrew/cinecrew-player/react-native` | React Native UI with the bundled VLC adapter and Expo video fallback. |
204
204
  | React Native Web / Expo Web | `@cinecrew/cinecrew-player` or `@cinecrew/cinecrew-player/react-native-web` | React DOM adapter hosted inside the React Native Web app; uses browser playback engines and does not load native VLC or WebView code. |
205
205
 
206
206
  Metro selects the React Native entry for native builds; regular React bundlers select the React DOM entry. The explicit subpaths let you pin the renderer when preferred.
@@ -221,7 +221,7 @@ export function WatchScreen() {
221
221
  }
222
222
  ```
223
223
 
224
- For an Expo prebuild project, add the CineCrew Player config plugin and rebuild the native app (a JavaScript reload cannot add a native module). The VLC native implementation ships inside this same package; there is no second VLC package to install. `react-native-webview` is used only as the native YouTube embed surface; regular native streams use VLC / `expo-video`. Browser and Electron YouTube playback use the YouTube IFrame API instead.
224
+ For an Expo prebuild project, add the CineCrew Player config plugin and rebuild the native app (a JavaScript reload cannot add a native module). The VLC native implementation ships inside this same package; there is no second VLC package to install. Native streams use VLC / `expo-video`.
225
225
 
226
226
  ```json
227
227
  {
@@ -244,7 +244,6 @@ import { InlineLivePlayer } from '@cinecrew/cinecrew-player/native';
244
244
  height={220}
245
245
  isActive={selected}
246
246
  paused={!selected}
247
- onFullscreen={() => openFullPlayer(channelUrl)}
248
247
  />
249
248
  ```
250
249
 
@@ -259,7 +258,6 @@ import '@cinecrew/cinecrew-player/styles.css';
259
258
  title="Example channel"
260
259
  isActive={selected}
261
260
  paused={!selected}
262
- onFullscreen={() => openFullPlayer(channelUrl)}
263
261
  />
264
262
  ```
265
263
 
@@ -283,15 +281,9 @@ export function WatchScreen() {
283
281
 
284
282
  Web playback is direct from the supplied URL. HLS and MPEG-TS clients fetch playlists and segments from the stream origin, so the origin must permit those browser requests.
285
283
 
286
- ### YouTube and share links
284
+ ### Direct media URLs and share pages
287
285
 
288
- Pass a YouTube watch, Shorts, or `youtu.be` URL as the source to play it inside the player rather than opening another app:
289
-
290
- ```tsx
291
- <CineCrewPlayer source="https://www.youtube.com/watch?v=dQw4w9WgXcQ" title="Trailer" />
292
- ```
293
-
294
- The video must allow embedding. Other URLs are passed unchanged to the platform engine. A Google Drive/share page or other webpage is not itself a media stream; resolve it in your app and provide the playable URL, or use the optional `resolveSource` callback. This keeps provider authentication, CORS policy, and URL extraction under the consuming app’s control.
286
+ Pass a direct media URL or a platform-readable local file URI. Hosted-video watch pages and share pages are not supported as playable sources because they are HTML pages, not media streams. Resolve those pages in your app and provide a direct playable URL, or use the optional `resolveSource` callback. This keeps provider authentication, CORS policy, and URL extraction under the consuming app’s control.
295
287
 
296
288
  ```tsx
297
289
  <CineCrewPlayer
@@ -311,7 +303,7 @@ The video must allow embedding. Other URLs are passed unchanged to the platform
311
303
  | React Native Android / iOS | VLC native module; `expo-video` fallback when VLC is unavailable | VLC supports a broader range of containers/codecs, including common AC3 streams. Native module availability depends on the app binary. |
312
304
  | Electron renderer | Chromium `<video>`, hls.js, and patched mpegts.js | Same browser codec/CORS constraints as React web; packages with the app's `.dmg` / `.exe`. |
313
305
 
314
- Browser codec support varies. The web player reports an AC3 compatibility message only when its MPEG-TS probe identifies unsupported AC3 audio; native playback does not apply this browser-only restriction. YouTube sources use the embedded YouTube player and remain subject to the video’s embed settings.
306
+ Browser codec support varies. The web player reports an AC3 compatibility message only when its MPEG-TS probe identifies unsupported AC3 audio; native playback does not apply this browser-only restriction.
315
307
 
316
308
  ## How CineCrew Player differs from established players
317
309
 
@@ -343,13 +335,16 @@ In short: CineCrew’s intended distinction is **one app-facing player package f
343
335
  | `muted` | `boolean` | `false` | Initial mute state. |
344
336
  | `volume` | `number` | `1` | Initial volume from `0` to `1`. |
345
337
  | `playbackRate` | `number` | `1` | Initial playback speed; the on-demand speed control can change it afterward. |
338
+ | `showProgressBar` | `boolean` | `true` | Show or hide the playback seek bar. When hidden, `onProgressBarChange` is not called. |
339
+ | `aspectRatios` | `(string \| { value, label? })[]` | built-in choices | Customize the aspect-ratio menu. Values include `FIT`, `FILL`, `STRETCH`, or a ratio such as `1:1`; labels are optional. |
340
+ | `defaultAspectRatio` | `string` | `'FIT'` | Initial and source-reset aspect mode. Must match an item in `aspectRatios` to appear selected. |
346
341
  | `controls` | `PlayerControls` | defaults below | Show/hide individual control buttons. |
347
342
  | `features` | feature flags | `{}` | Optional player features, including web stream diagnostics with `{ diagnostics: true }`. |
348
- | `actions` | `PlayerActions` | `{}` | Replace the built-in behavior for individual actions. If a callback is provided, that callback owns the action. |
343
+ | `actions` | `PlayerActions` | `{}` | Observe built-in actions. The player performs its core action first, then invokes the callback with the resulting action payload. |
349
344
  | `integrations` | `PlayerIntegrations` | `{}` | Inject user identity, chat, EPG, recording, analytics, and presence services. |
350
345
  | `drawerMode` | `'overlay' \| 'resize'` | `'overlay'` | Web/Electron drawer behavior: overlay the video or resize it to make room for chat, EPG, and diagnostics. |
351
346
  | `drawerStyle` | `React.CSSProperties` / React Native `ViewStyle` | β€” | Platform-specific style overrides for the chat, EPG, and diagnostics drawer. |
352
- | `messagePageSize` | `number` | `50` | Number of live-chat messages fetched per page; older pages load from the drawer’s β€œSee more” control. |
347
+ | `messagePageSize` | `number` | `50` | Number of live-chat messages fetched per page; older messages load automatically when the list is scrolled to the top. |
353
348
  | `theme` | `PlayerTheme` | built-in theme | Customize player colors, borders, and shape. |
354
349
  | `icons` | `PlayerIcons` | built-in icons | Override any control icon by key. |
355
350
  | `style` | platform style | β€” | Outer player style. On web this is a CSS style object; native uses React Native style props. |
@@ -364,14 +359,16 @@ In short: CineCrew’s intended distinction is **one app-facing player package f
364
359
  | `mediaId`, `episodeLabel`, `season`, `episode`, `genre`, `categoryName` | metadata | β€” | Optional item metadata for the player and integrations. |
365
360
  | `playlist` | `object[]` | β€” | Episode list used for automatic next-episode behavior. |
366
361
  | `shuffle` | `boolean` | `false` | Select a random next episode when the current episode ends. |
367
- | `onClose`, `onBack` | callbacks | β€” | Player lifecycle/navigation callbacks. |
362
+ | `onClose`, `onBack` | callbacks | β€” | App-owned navigation callbacks; Back does not close the player unless your callback does so. |
363
+ | `onAspectRatioChange` | `PlayerAction` | β€” | Top-level callback invoked after the player applies the selected aspect ratio; `actions.onAspectRatioChange` takes precedence if both are supplied. |
364
+ | `onProgressBarChange` | `(time: string) => void` | β€” | When the progress bar is shown, reports the played position as zero-padded `HH:MM:SS` once per elapsed playback second, and immediately after a completed seek or restart. Scrubbing reports the committed position, not every intermediate drag update. It is not called when `showProgressBar` is false, `controls.seek` is false, or the live-player UI hides seeking. |
368
365
  | `onReady`, `onProgress`, `onPlaying`, `onBuffering`, `onError`, `onEnded`, `onPlaybackRoute` | callbacks | β€” | Playback lifecycle callbacks. Progress payloads are platform-specific native/browser events. |
369
366
  | `onNextEpisode`, `onCwRefresh` | callbacks | β€” | Episode advancement and post-close refresh hooks. |
370
367
  | `renderLiveChat`, `renderEpg` | render functions | β€” | Web custom-panel render slots. On native, use the chat/EPG integration adapters. |
371
368
  | `initialShowLiveChat`, `liveChatNonce` | `boolean`, `number` | `false`, `0` | Open or re-open the live-chat panel (when available). |
372
369
  | `inlinePreview`, `inlinePreviewRect`, `onInlinePreviewWheel`, `onPromotePreview`, `onPlayerHostRef` | preview options and callbacks | β€” | Embed/manage the player as a movable inline preview. Mainly useful for app-level player shells. |
373
370
 
374
- `features={{ diagnostics: true }}` enables the diagnostics button and built-in stream status panel on web; `controls.diagnostics` can hide it and `actions.onDiagnosticsOpen` can replace the panel action. `onFullscreen` receives `{ isFullscreen }`. All lifecycle callbacks in the table are optional; native event objects differ from browser events.
371
+ `features={{ diagnostics: true }}` enables the diagnostics button and built-in stream status panel on web; `controls.diagnostics` can hide it. `onFullscreen` receives `{ isFullscreen }` after the player requests the fullscreen change. All lifecycle callbacks in the table are optional; native event objects differ from browser events.
375
372
 
376
373
  ### Player source
377
374
 
@@ -385,7 +382,7 @@ type PlayerSource = string | {
385
382
  id?: string | number;
386
383
  streamId?: string | number;
387
384
  mediaId?: string | number;
388
- type?: string; // e.g. 'mpegts', 'hls', or 'youtube' when paired with a YouTube video ID
385
+ type?: string; // e.g. 'mpegts' or 'hls'
389
386
  mimeType?: string;
390
387
  mediaType?: string;
391
388
  isLive?: boolean;
@@ -394,7 +391,7 @@ type PlayerSource = string | {
394
391
 
395
392
  ### Inline live preview props
396
393
 
397
- `InlineLivePlayer` is exported from `@cinecrew/cinecrew-player/native` and `@cinecrew/cinecrew-player/web`. It renders a compact channel preview/poster and can promote playback to the host app's full player.
394
+ `InlineLivePlayer` is exported from `@cinecrew/cinecrew-player/native` and `@cinecrew/cinecrew-player/web`. It renders a compact channel preview/poster with its title at the bottom-left. Its fullscreen control expands the same inline playback surface; it does not promote or switch to the standard player.
398
395
 
399
396
  | Prop | Type | Default | Description |
400
397
  | --- | --- | --- | --- |
@@ -404,9 +401,9 @@ type PlayerSource = string | {
404
401
  | `poster`, `posterChannel` | string / object | β€” | Still image or channel object used when preview is paused/inactive. |
405
402
  | `paused`, `isActive` | `boolean` | `false`, `true` | Control whether this preview should render/play its stream. |
406
403
  | `onActivate` | `() => void` | β€” | Called when an inactive preview poster is selected. |
407
- | `onFullscreen` | `() => void` | β€” | Called to promote/open the full player. |
404
+ | `onFullscreen` | `() => void` | β€” | Deprecated; no longer promotes to another player. Use `actions.onFullscreen` to observe the inline player's fullscreen state after it changes. |
408
405
  | `controls` | `Pick<PlayerControls, 'playPause' \| 'mute' \| 'fullscreen'>` | all shown | Toggle its compact controls. |
409
- | `actions` | matching `PlayerActions` subset | built-in | Replace play/pause, mute, or promote behavior. |
406
+ | `actions` | matching `PlayerActions` subset | built-in | Observe play/pause, mute, or fullscreen actions after their built-in behavior runs. |
410
407
  | `initialMuted` | `boolean` | `true` | Initial preview mute state. |
411
408
  | `theme`, `icons`, `style` | `PlayerTheme`, `PlayerIcons`, platform style | defaults | Customize preview colors, controls, and layout. |
412
409
  | `onError`, `onPlaying` | callbacks | β€” | Playback lifecycle callbacks. |
@@ -447,20 +444,22 @@ Every control can be hidden with `false`. Defaults are designed to be useful out
447
444
  | `lock` | Lock/unlock touch controls. |
448
445
  | `mute` | Mute/unmute. |
449
446
  | `aspectRatio` | Fit/fill/stretch and available aspect choices. |
450
- | `videoOnly` | Mute audio while keeping video visible. |
447
+ | `videoOnly` | Legacy compatibility setting; the player does not render a separate video-only button. |
451
448
  | `audioOnly` | Show the audio-only card while playback continues. |
452
449
  | `audioTracks` | Audio-track picker when tracks are exposed. |
453
450
  | `playbackRate` | On-demand playback speed. |
454
451
  | `fullscreen` | Fullscreen button on web and inline previews. Native player opens full-screen. |
455
- | `recording` | Recording controls; requires `integrations.recording` or an action override. |
456
- | `liveChat` | Chat drawer/panel; requires a chat adapter, render slot, or action override. |
457
- | `epg` | EPG drawer/panel; requires an EPG adapter, render slot, or action override. |
458
- | `diagnostics` | Stream diagnostics button; enable with `features={{ diagnostics: true }}` or provide `actions.onDiagnosticsOpen`. |
452
+ | `recording` | Recording controls; web has a built-in MediaRecorder flow where supported, while native requires an app recording adapter. |
453
+ | `liveChat` | Chat drawer/panel; requires a chat adapter or render slot. |
454
+ | `epg` | EPG drawer/panel; requires an EPG adapter or render slot. |
455
+ | `diagnostics` | Stream diagnostics button; enable with `features={{ diagnostics: true }}`. |
459
456
  | `seek` | On-demand seek bar. |
460
457
 
461
458
  ## Actions and callbacks
462
459
 
463
- Callbacks passed to `actions` **replace** built-in behavior; this is useful when an app wants to own navigation, playback state, or a control. Each receives an action payload and a context with the imperative `player` API (and the web video element where available).
460
+ For playback controls, the player executes its core behavior first and then invokes the matching `actions` callback. This lets your app show a snackbar, update analytics, or synchronize app state. Back is intentionally different: it has no built-in close/navigation behavior. The player invokes `actions.onBack`, `onBack`, or `onClose` (in that precedence), and your app decides whether to navigate, dismiss the player, or just show a snackbar. Each callback receives an action payload and a context with the imperative `player` API (and the web video element where available). If a callback throws, the player logs the error without blocking the UI.
461
+
462
+ `onProgressBarChange` is separate from the platform-specific `onProgress` event: while the progress bar is enabled, it emits a compact time string such as `00:00:05` once per playback second. A seek emits its final target time after the player applies the seek; skipped positions are not reported as watched time. Set `showProgressBar={false}` (or `controls={{ seek: false }}`) to hide seeking and suppress these progress-bar callbacks.
464
463
 
465
464
  ```tsx
466
465
  const playerRef = React.useRef(null);
@@ -469,17 +468,18 @@ const playerRef = React.useRef(null);
469
468
  ref={playerRef}
470
469
  source={source}
471
470
  actions={{
472
- onRestart: (_payload, { player }) => player?.restart(),
473
- onBack: () => navigation.goBack(),
474
- onMute: ({ muted }, { player }) => player?.setMuted(muted),
475
- onAspectRatioChange: ({ aspectRatio }, { player }) => player?.setAspectRatio(aspectRatio),
471
+ onRestart: () => analytics.track('player_restart'),
472
+ onPlayPause: ({ isPlaying }) => analytics.track('player_play_pause', { isPlaying }),
473
+ onMute: ({ muted }) => analytics.track('player_mute', { muted }),
474
+ onAspectRatioChange: ({ aspectRatio }) => analytics.track('player_aspect_ratio', { aspectRatio }),
476
475
  }}
476
+ onBack={() => navigation.goBack()}
477
477
  />
478
478
  ```
479
479
 
480
480
  Available action keys: `onBack`, `onPlayPause`, `onSeek`, `onRestart`, `onLock`, `onMute`, `onAspectRatioChange`, `onVideoOnlyChange`, `onAudioOnlyChange`, `onAudioTrackChange`, `onPlaybackRateChange`, `onFullscreen`, `onRecordingStart`, `onRecordingPause`, `onRecordingResume`, `onRecordingStop`, `onLiveChatOpen`, `onEpgOpen`, and `onDiagnosticsOpen`.
481
481
 
482
- The ref exposes `play`, `pause`, `togglePlayPause`, `restart`, `setMuted`, `toggleMute`, `setAspectRatio`, `setAudioTrack`, `setAudioOnly`, `setVideoOnly`, `setPlaybackRate`, `seekTo`, `seekBy`, `back`, `getVideoElement`, `getAudioTracks`, and fullscreen methods where supported.
482
+ The ref exposes `play`, `pause`, `togglePlayPause`, `restart`, `setMuted`, `toggleMute`, `setAspectRatio`, `setAudioTrack`, `setAudioOnly`, `setVideoOnly`, `setPlaybackRate`, `seekTo`, `seekBy`, `back`, `setPanel`, `closePanel`, `getVideoElement`, `getAudioTracks`, and fullscreen methods where supported.
483
483
 
484
484
  ## Integrations
485
485
 
@@ -517,10 +517,20 @@ Integrations are optional. The package has no CineCrew account, database, or wor
517
517
  />
518
518
  ```
519
519
 
520
- On web and Electron, the player supplies the chat drawer UIβ€”including the composer and emoji pickerβ€”when `integrations.liveChat.loadMessages` is provided. `sendMessage` connects the built-in composer to your chat service; omit it to show a read-only chat. Messages may include `id`, `username`, `comment` (or `message`), and `timestamp` (or `createdAt`). The drawer requests the newest page with `offset: 0`, then requests older pages with the same `limit` and an increasing `offset` when β€œSee more messages” is selected. Return each page in chronological order (oldest first); return `{ messages, hasMore }` when your service can report whether older pages exist. Otherwise, a full page implies there may be more. Live chat polls for new messages at `pollIntervalMs` (defaults to five seconds).
520
+ On web and Electron, the player supplies the chat drawer UIβ€”including the composer and searchable, grouped emoji pickerβ€”when `integrations.liveChat.loadMessages` is provided. `sendMessage` connects the built-in composer to your chat service; omit it to show a read-only chat. Messages may include `id`, `username`, `comment` (or `message`), and `timestamp` (or `createdAt`). The drawer requests the newest page with `offset: 0`, then automatically requests older pages with the same `limit` and increasing `offset` as the viewer scrolls to the top. Return each page in chronological order (oldest first); return `{ messages, hasMore }` when your service can report whether older pages exist. Otherwise, a full page implies there may be more. Live chat polls for new messages at `pollIntervalMs` (defaults to five seconds); new messages scroll into view at the bottom.
521
521
 
522
522
  The EPG drawer uses `integrations.epg.loadListings`, which returns entries with `startMs` and `endMs` epoch-millisecond timestamps. Both drawers default to a semi-transparent right-side overlay, so video size does not change. Set `drawerMode="resize"` to reserve space and shrink the video; customize the drawer with `drawerStyle`. You may supply `renderLiveChat` / `renderEpg` or integration render callbacks to replace the built-in drawer contents. The native player renders its platform-native chat and EPG UI from the same adapters.
523
523
 
524
+ ### Web recording
525
+
526
+ On supported browsers, the built-in recording control captures the media video and audio tracks, shows a compact timer at the top of the video with pause/resume and stop actions, then attempts a WebM download when stopped. A `Download recording` action remains available afterward as a user-gesture retry if the browser blocks the automatic download. Native React Native apps need an `integrations.recording` implementation backed by Android MediaProjection or iOS ReplayKit (with the platform's permission flow).
527
+
528
+ For ordinary web media, aspect changes are reflected in the recording when the browser permits the player to draw the cross-origin video into a canvas; if the source does not grant canvas CORS access, the recording keeps its source aspect ratio. Use `integrations.recording` to supply a different recording implementation.
529
+
530
+ ### Audio-only and locked-screen playback
531
+
532
+ Audio-only mode fully covers the video with an opaque dark surface; its poster remains visible in the audio card. Native VLC playback is configured to continue in audio-only mode when the app is backgrounded. Native apps must also configure their platform background-audio capability where applicable.
533
+
524
534
  ## Themes and icons
525
535
 
526
536
  Defaults are used unless the caller supplies an override. Web theme properties include `accentColor`, `backgroundColor`, `controlBackground`, `controlColor`, `surfaceColor`, `errorColor`, and `borderRadius`. Native also accepts a `colors` palette object.
@@ -548,7 +558,7 @@ Icon keys: `play`, `pause`, `restart`, `lock`, `unlock`, `mute`, `unmute`, `aspe
548
558
 
549
559
  ## Sources and link resolution
550
560
 
551
- Local file URIs and direct stream URLs are passed to the selected platform player without changing `http`, `https`, `file`, or other schemes. YouTube watch/share URLs are recognized and played with the embedded YouTube player. Other sharing pages (for example, a private Google Drive page) need a consumer-provided resolver because the package cannot access the consumer’s credentials or infer every host’s download rules:
561
+ Local file URIs and direct stream URLs are passed to the selected platform player without changing `http`, `https`, `file`, or other schemes. Hosted-video watch/share pages (for example, a private cloud-drive page) need a consumer-provided resolver because the package cannot access the consumer’s credentials or infer every host’s download rules:
552
562
 
553
563
  ```tsx
554
564
  <CineCrewPlayer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cinecrew/cinecrew-player",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "Customizable cross-platform live and on-demand video player for React and React Native.",
6
6
  "license": "MIT",
@@ -113,7 +113,6 @@
113
113
  "mediabunny": "^1.59.0",
114
114
  "react-native-safe-area-context": "^5.7.0",
115
115
  "react-native-svg": "^15.15.4",
116
- "react-native-webview": "^13.16.1",
117
116
  "prop-types": "^15.8.1"
118
117
  },
119
118
  "peerDependencies": {
@@ -4,6 +4,7 @@ import {
4
4
  Image,
5
5
  Pressable,
6
6
  StyleSheet,
7
+ Modal,
7
8
  Text,
8
9
  View,
9
10
  } from 'react-native';
@@ -13,6 +14,7 @@ import { WebVideoPlayer } from './media/WebVideoPlayer';
13
14
  import { ElectronVideoPlayer } from './media/ElectronVideoPlayer';
14
15
  import { isAndroid, isElectron, isIOS, isWeb } from '../utils/runtimePlatform';
15
16
  import { USER_AGENT } from './media/player/playerConstants';
17
+ import { invokePlayerAction } from '../utils/invokePlayerAction.js';
16
18
 
17
19
  function getArtwork(channel) {
18
20
  return channel?.logoUrl || channel?.logo || channel?.stream_icon || channel?.posterUrl || channel?.image || '';
@@ -82,8 +84,7 @@ function InlineControlButton({
82
84
  accessibilityLabel: label,
83
85
  onPress: () => {
84
86
  const callback = actions?.[actionName];
85
- if (typeof callback === 'function') callback(payload, { player: null });
86
- else fallback?.();
87
+ invokePlayerAction(fallback, callback, payload, { player: null });
87
88
  },
88
89
  style: [styles.button, { backgroundColor: palette.controlBackground }, active && { borderColor: palette.accentColor, borderWidth: 1 }],
89
90
  }, React.createElement(PlayerIcon, { name: icon, size: 19, color: palette.controlColor }));
@@ -130,7 +131,7 @@ function InlinePlayerOverlay({
130
131
  showControls ? React.createElement(React.Fragment, null,
131
132
  React.createElement(View, { pointerEvents: 'box-none', style: styles.topRow },
132
133
  React.createElement(View, { style: styles.liveBadge }, React.createElement(View, { style: styles.liveDot }), React.createElement(Text, { style: styles.liveText }, 'LIVE')),
133
- React.createElement(Text, { numberOfLines: 1, style: [styles.title, { color: palette.controlColor }] }, title),
134
+ React.createElement(View, { style: { flex: 1 } }),
134
135
  button('mute', 'onMute', muteLabel, muteIcon, onMute, { muted: !muted }),
135
136
  ),
136
137
  React.createElement(View, { pointerEvents: 'box-none', style: styles.center },
@@ -140,12 +141,64 @@ function InlinePlayerOverlay({
140
141
  button('fullscreen', 'onFullscreen', 'Open full player', 'fullscreen', onFullscreen, { source, title, isFullscreen: !fullscreen }))) : null);
141
142
  }
142
143
 
144
+ function createInlinePlayerLayer(player, visible) {
145
+ if (!visible || !player) return null;
146
+ return React.createElement(View, { pointerEvents: 'none', style: StyleSheet.absoluteFill }, player);
147
+ }
148
+
149
+ function createInlineArtworkLayer(shouldRenderVideo, artwork, accentColor) {
150
+ if (shouldRenderVideo) return null;
151
+ if (artwork) {
152
+ return React.createElement(Image, { source: { uri: artwork }, resizeMode: 'contain', style: styles.poster });
153
+ }
154
+ return React.createElement(View, { style: styles.emptyPoster },
155
+ React.createElement(PlayerIcon, { name: 'television-play', size: 48, color: accentColor }));
156
+ }
157
+
158
+ function createInlineStatusLayer(loading, error, palette) {
159
+ if (error) {
160
+ return React.createElement(View, { pointerEvents: 'none', style: styles.error },
161
+ React.createElement(Text, { style: [styles.errorText, { color: palette.controlColor }] }, error));
162
+ }
163
+ if (!loading) return null;
164
+ return React.createElement(View, { pointerEvents: 'none', style: styles.loading },
165
+ React.createElement(ActivityIndicator, { size: 'large', color: palette.accentColor }));
166
+ }
167
+
168
+ function InlineLivePlayerSurface({
169
+ height,
170
+ style,
171
+ palette,
172
+ fullscreen,
173
+ setFullscreen,
174
+ player,
175
+ shouldRenderVideo,
176
+ artwork,
177
+ loading,
178
+ error,
179
+ renderOverlay,
180
+ }) {
181
+ return React.createElement(View, { style: [styles.frame, { height, backgroundColor: palette.surfaceColor }, style] },
182
+ createInlinePlayerLayer(player, !fullscreen),
183
+ createInlineArtworkLayer(shouldRenderVideo, artwork, palette.accentColor),
184
+ createInlineStatusLayer(shouldRenderVideo && loading, error, palette),
185
+ fullscreen ? null : renderOverlay(),
186
+ React.createElement(Modal, {
187
+ visible: fullscreen,
188
+ animationType: 'none',
189
+ statusBarTranslucent: true,
190
+ onRequestClose: () => setFullscreen(false),
191
+ }, React.createElement(View, { style: styles.fullscreenFrame },
192
+ createInlinePlayerLayer(player, true),
193
+ createInlineStatusLayer(loading, error, palette),
194
+ renderOverlay())));
195
+ }
196
+
143
197
  function InlineLivePlayerView({
144
198
  source,
145
199
  url,
146
200
  title = 'Live TV',
147
201
  height = 220,
148
- onFullscreen,
149
202
  paused: externalPaused,
150
203
  isActive = true,
151
204
  onActivate,
@@ -213,15 +266,16 @@ function InlineLivePlayerView({
213
266
  }, [onPlaying]);
214
267
 
215
268
  const performAction = (name, fallback, payload) => {
216
- if (typeof actions?.[name] === 'function') return actions[name](payload);
217
- return fallback?.();
269
+ return invokePlayerAction(fallback, actions?.[name], payload, { player: null });
218
270
  };
219
271
 
220
272
  const togglePlay = (event) => {
221
273
  event?.stopPropagation?.();
222
274
  if (!shouldRenderVideo) {
223
- onActivate?.();
224
- setInternallyPaused(false);
275
+ performAction('onPlayPause', () => {
276
+ onActivate?.();
277
+ setInternallyPaused(false);
278
+ }, true);
225
279
  return;
226
280
  }
227
281
  performAction('onPlayPause', () => setInternallyPaused((value) => !value), !pausedNow);
@@ -234,10 +288,11 @@ function InlineLivePlayerView({
234
288
 
235
289
  const openFullscreen = (event) => {
236
290
  event?.stopPropagation?.();
237
- performAction('onFullscreen', () => {
238
- if (typeof onFullscreen === 'function') onFullscreen();
239
- else setFullscreen((value) => !value);
240
- }, { isFullscreen: !fullscreen, source: sourceObject, title });
291
+ performAction('onFullscreen', () => setFullscreen((value) => !value), {
292
+ isFullscreen: !fullscreen,
293
+ source: sourceObject,
294
+ title,
295
+ });
241
296
  };
242
297
 
243
298
  const nativeMediaOptions = useMemo(() => [
@@ -272,31 +327,37 @@ function InlineLivePlayerView({
272
327
  onPlaying: handlePlaying,
273
328
  onError: handleError,
274
329
  });
330
+ const renderOverlay = () => React.createElement(InlinePlayerOverlay, {
331
+ showControls,
332
+ controls: { ...controls, actions },
333
+ palette,
334
+ title,
335
+ muted,
336
+ paused: pausedNow,
337
+ source: streamUrl,
338
+ fullscreen,
339
+ onToggleControls: () => setShowControls((value) => !value),
340
+ onMute: toggleMute,
341
+ onPlay: togglePlay,
342
+ onFullscreen: openFullscreen,
343
+ });
275
344
 
276
345
  return React.createElement(
277
346
  PlayerCustomizationProvider,
278
347
  { icons, theme },
279
- React.createElement(View, { style: [styles.frame, { height, backgroundColor: palette.surfaceColor }, style] },
280
- player ? React.createElement(View, { pointerEvents: 'none', style: StyleSheet.absoluteFill }, player) : null,
281
- !shouldRenderVideo && artwork ? React.createElement(Image, { source: { uri: artwork }, resizeMode: 'contain', style: styles.poster }) : null,
282
- !shouldRenderVideo && !artwork ? React.createElement(View, { style: styles.emptyPoster }, React.createElement(PlayerIcon, { name: 'television-play', size: 48, color: palette.accentColor })) : null,
283
- shouldRenderVideo && loading && !error ? React.createElement(View, { pointerEvents: 'none', style: styles.loading }, React.createElement(ActivityIndicator, { size: 'large', color: palette.accentColor })) : null,
284
- error ? React.createElement(View, { pointerEvents: 'none', style: styles.error }, React.createElement(Text, { style: [styles.errorText, { color: palette.controlColor }] }, error)) : null,
285
- React.createElement(InlinePlayerOverlay, {
286
- showControls,
287
- controls: { ...controls, actions },
288
- palette,
289
- title,
290
- muted,
291
- paused: pausedNow,
292
- source: streamUrl,
293
- fullscreen,
294
- onToggleControls: () => setShowControls((value) => !value),
295
- onMute: toggleMute,
296
- onPlay: togglePlay,
297
- onFullscreen: openFullscreen,
298
- }),
299
- ),
348
+ React.createElement(InlineLivePlayerSurface, {
349
+ height,
350
+ style,
351
+ palette,
352
+ fullscreen,
353
+ setFullscreen,
354
+ player,
355
+ shouldRenderVideo,
356
+ artwork,
357
+ loading,
358
+ error,
359
+ renderOverlay,
360
+ }),
300
361
  );
301
362
  }
302
363
 
@@ -304,6 +365,7 @@ export const InlineLivePlayer = React.memo(InlineLivePlayerView);
304
365
 
305
366
  const styles = StyleSheet.create({
306
367
  frame: { width: '100%', minHeight: 80, overflow: 'hidden', borderRadius: 14, position: 'relative', justifyContent: 'center' },
368
+ fullscreenFrame: { flex: 1, overflow: 'hidden', position: 'relative', justifyContent: 'center', backgroundColor: '#000' },
307
369
  video: { width: '100%', height: '100%' },
308
370
  poster: { ...StyleSheet.absoluteFillObject, width: '100%', height: '100%' },
309
371
  emptyPoster: { ...StyleSheet.absoluteFillObject, alignItems: 'center', justifyContent: 'center' },