@cinecrew/cinecrew-player 0.1.4 β†’ 0.1.6

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,7 +52,7 @@ 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 |
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
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 |
@@ -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,6 +335,7 @@ 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. |
346
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. |
347
340
  | `defaultAspectRatio` | `string` | `'FIT'` | Initial and source-reset aspect mode. Must match an item in `aspectRatios` to appear selected. |
348
341
  | `controls` | `PlayerControls` | defaults below | Show/hide individual control buttons. |
@@ -366,9 +359,9 @@ In short: CineCrew’s intended distinction is **one app-facing player package f
366
359
  | `mediaId`, `episodeLabel`, `season`, `episode`, `genre`, `categoryName` | metadata | β€” | Optional item metadata for the player and integrations. |
367
360
  | `playlist` | `object[]` | β€” | Episode list used for automatic next-episode behavior. |
368
361
  | `shuffle` | `boolean` | `false` | Select a random next episode when the current episode ends. |
369
- | `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. |
370
363
  | `onAspectRatioChange` | `PlayerAction` | β€” | Top-level callback invoked after the player applies the selected aspect ratio; `actions.onAspectRatioChange` takes precedence if both are supplied. |
371
- | `onProgressBarChange` | `(time: string) => void` | β€” | 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. |
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. |
372
365
  | `onReady`, `onProgress`, `onPlaying`, `onBuffering`, `onError`, `onEnded`, `onPlaybackRoute` | callbacks | β€” | Playback lifecycle callbacks. Progress payloads are platform-specific native/browser events. |
373
366
  | `onNextEpisode`, `onCwRefresh` | callbacks | β€” | Episode advancement and post-close refresh hooks. |
374
367
  | `renderLiveChat`, `renderEpg` | render functions | β€” | Web custom-panel render slots. On native, use the chat/EPG integration adapters. |
@@ -389,7 +382,7 @@ type PlayerSource = string | {
389
382
  id?: string | number;
390
383
  streamId?: string | number;
391
384
  mediaId?: string | number;
392
- type?: string; // e.g. 'mpegts', 'hls', or 'youtube' when paired with a YouTube video ID
385
+ type?: string; // e.g. 'mpegts' or 'hls'
393
386
  mimeType?: string;
394
387
  mediaType?: string;
395
388
  isLive?: boolean;
@@ -398,7 +391,7 @@ type PlayerSource = string | {
398
391
 
399
392
  ### Inline live preview props
400
393
 
401
- `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.
402
395
 
403
396
  | Prop | Type | Default | Description |
404
397
  | --- | --- | --- | --- |
@@ -408,7 +401,7 @@ type PlayerSource = string | {
408
401
  | `poster`, `posterChannel` | string / object | β€” | Still image or channel object used when preview is paused/inactive. |
409
402
  | `paused`, `isActive` | `boolean` | `false`, `true` | Control whether this preview should render/play its stream. |
410
403
  | `onActivate` | `() => void` | β€” | Called when an inactive preview poster is selected. |
411
- | `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. |
412
405
  | `controls` | `Pick<PlayerControls, 'playPause' \| 'mute' \| 'fullscreen'>` | all shown | Toggle its compact controls. |
413
406
  | `actions` | matching `PlayerActions` subset | built-in | Observe play/pause, mute, or fullscreen actions after their built-in behavior runs. |
414
407
  | `initialMuted` | `boolean` | `true` | Initial preview mute state. |
@@ -456,7 +449,7 @@ Every control can be hidden with `false`. Defaults are designed to be useful out
456
449
  | `audioTracks` | Audio-track picker when tracks are exposed. |
457
450
  | `playbackRate` | On-demand playback speed. |
458
451
  | `fullscreen` | Fullscreen button on web and inline previews. Native player opens full-screen. |
459
- | `recording` | Recording controls; web has a built-in MediaRecorder flow where supported, while native requires an app recording adapter. Browser security requires local media or a source that permits capture (typically same-origin or CORS-enabled); the player reports when a source cannot be captured. |
452
+ | `recording` | Recording controls; web has a built-in MediaRecorder flow where supported, while native requires an app recording adapter. |
460
453
  | `liveChat` | Chat drawer/panel; requires a chat adapter or render slot. |
461
454
  | `epg` | EPG drawer/panel; requires an EPG adapter or render slot. |
462
455
  | `diagnostics` | Stream diagnostics button; enable with `features={{ diagnostics: true }}`. |
@@ -464,9 +457,9 @@ Every control can be hidden with `false`. Defaults are designed to be useful out
464
457
 
465
458
  ## Actions and callbacks
466
459
 
467
- Callbacks passed to `actions` are notifications, not replacements: the player executes the built-in control behavior first, then calls the matching callback. This keeps core playback working while allowing your app to show a snackbar, update analytics, or synchronize app state. Each callback receives an action payload and a context with the imperative `player` API (and the web video element where available). For app navigation, use `onBack` / `onClose` as appropriate. If a notification callback throws, the player logs that callback error without undoing or blocking the already-completed player action. The callback should not repeat the player commandβ€”the control has already performed it.
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.
468
461
 
469
- `onProgressBarChange` is separate from the platform-specific `onProgress` event: 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.
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.
470
463
 
471
464
  ```tsx
472
465
  const playerRef = React.useRef(null);
@@ -530,7 +523,13 @@ The EPG drawer uses `integrations.epg.loadListings`, which returns entries with
530
523
 
531
524
  ### Web recording
532
525
 
533
- 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. YouTube embeds are not recordable through this built-in path. 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 browser only permits capture of the media element's original frame, so the recording keeps its source aspect ratio. Use `integrations.recording` to supply a different recording implementation or use a native adapter.
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.
534
533
 
535
534
  ## Themes and icons
536
535
 
@@ -559,7 +558,7 @@ Icon keys: `play`, `pause`, `restart`, `lock`, `unlock`, `mute`, `unmute`, `aspe
559
558
 
560
559
  ## Sources and link resolution
561
560
 
562
- 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:
563
562
 
564
563
  ```tsx
565
564
  <CineCrewPlayer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cinecrew/cinecrew-player",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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';
@@ -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,
@@ -235,10 +288,11 @@ function InlineLivePlayerView({
235
288
 
236
289
  const openFullscreen = (event) => {
237
290
  event?.stopPropagation?.();
238
- performAction('onFullscreen', () => {
239
- if (typeof onFullscreen === 'function') onFullscreen();
240
- else setFullscreen((value) => !value);
241
- }, { isFullscreen: !fullscreen, source: sourceObject, title });
291
+ performAction('onFullscreen', () => setFullscreen((value) => !value), {
292
+ isFullscreen: !fullscreen,
293
+ source: sourceObject,
294
+ title,
295
+ });
242
296
  };
243
297
 
244
298
  const nativeMediaOptions = useMemo(() => [
@@ -273,31 +327,37 @@ function InlineLivePlayerView({
273
327
  onPlaying: handlePlaying,
274
328
  onError: handleError,
275
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
+ });
276
344
 
277
345
  return React.createElement(
278
346
  PlayerCustomizationProvider,
279
347
  { icons, theme },
280
- React.createElement(View, { style: [styles.frame, { height, backgroundColor: palette.surfaceColor }, style] },
281
- player ? React.createElement(View, { pointerEvents: 'none', style: StyleSheet.absoluteFill }, player) : null,
282
- !shouldRenderVideo && artwork ? React.createElement(Image, { source: { uri: artwork }, resizeMode: 'contain', style: styles.poster }) : null,
283
- !shouldRenderVideo && !artwork ? React.createElement(View, { style: styles.emptyPoster }, React.createElement(PlayerIcon, { name: 'television-play', size: 48, color: palette.accentColor })) : null,
284
- shouldRenderVideo && loading && !error ? React.createElement(View, { pointerEvents: 'none', style: styles.loading }, React.createElement(ActivityIndicator, { size: 'large', color: palette.accentColor })) : null,
285
- error ? React.createElement(View, { pointerEvents: 'none', style: styles.error }, React.createElement(Text, { style: [styles.errorText, { color: palette.controlColor }] }, error)) : null,
286
- React.createElement(InlinePlayerOverlay, {
287
- showControls,
288
- controls: { ...controls, actions },
289
- palette,
290
- title,
291
- muted,
292
- paused: pausedNow,
293
- source: streamUrl,
294
- fullscreen,
295
- onToggleControls: () => setShowControls((value) => !value),
296
- onMute: toggleMute,
297
- onPlay: togglePlay,
298
- onFullscreen: openFullscreen,
299
- }),
300
- ),
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
+ }),
301
361
  );
302
362
  }
303
363
 
@@ -305,6 +365,7 @@ export const InlineLivePlayer = React.memo(InlineLivePlayerView);
305
365
 
306
366
  const styles = StyleSheet.create({
307
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' },
308
369
  video: { width: '100%', height: '100%' },
309
370
  poster: { ...StyleSheet.absoluteFillObject, width: '100%', height: '100%' },
310
371
  emptyPoster: { ...StyleSheet.absoluteFillObject, alignItems: 'center', justifyContent: 'center' },
@@ -15,7 +15,6 @@ import {
15
15
  import VLCPlayer from '../../packages/react-native-vlc-media-player/VLCPlayer.js';
16
16
  import { WebVideoPlayer } from './media/WebVideoPlayer';
17
17
  import { ElectronVideoPlayer } from './media/ElectronVideoPlayer';
18
- import { YouTubeVideoPlayer } from './media/YouTubeVideoPlayer';
19
18
  import { LiveChatDrawer } from './media/LiveChatDrawer';
20
19
  import { LiveRecordingNotice, LiveRecordingOverlay } from './media/LiveRecordingOverlay';
21
20
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -217,49 +216,16 @@ function handleVerticalGestureMove(touches, gestureState) {
217
216
  gestureState.commitVolume.current(volume);
218
217
  }
219
218
 
220
- function handleYouTubePlaybackState(state, playback) {
221
- if (state === 'playing') {
222
- playback.isPlayingRef.current = true;
223
- playback.setIsPlaying(true);
224
- } else if (state === 'paused' || state === 'ended') {
225
- playback.isPlayingRef.current = false;
226
- playback.setIsPlaying(false);
227
- }
228
- }
229
-
230
219
  function PlatformMediaSurface(props) {
231
220
  const {
232
- youtubeVideoId, playerStreamUrl, vlcRef, isPlaying, muted, videoOnlyMode,
221
+ playerStreamUrl, vlcRef, isPlaying, muted, videoOnlyMode,
233
222
  volume, playbackRate, aspectRatio, title, posterUrl, isLive, isAudioOnly,
234
223
  selectedAudioTrack, handleTracksChanged, handleProgress, handleNativePlaying,
235
224
  handleWebBuffering, handleEpisodeEnded, handleWebError, togglePlayPause,
236
225
  handleSeekByAction, handlePlaybackRoute, exoFallback, useExoForAndroidLocalMedia,
237
226
  nativeSource, computedAspectRatio, handleNativeLoadStart,
238
- handleNativeOpen, handleNativeBuffering, isPlayingRef, setIsPlaying,
227
+ handleNativeOpen, handleNativeBuffering,
239
228
  } = props;
240
- if (youtubeVideoId) {
241
- return (
242
- <YouTubeVideoPlayer
243
- key={`youtube-${youtubeVideoId}`}
244
- ref={vlcRef}
245
- videoId={youtubeVideoId}
246
- paused={!isPlaying}
247
- muted={muted || videoOnlyMode}
248
- volume={volume / 100}
249
- playbackRate={playbackRate}
250
- onReady={(event) => {
251
- props.clearBufferingIndicator();
252
- props.handleNativeOpen(event);
253
- }}
254
- onProgress={handleProgress}
255
- onPlaying={handleNativePlaying}
256
- onStateChange={(state) => handleYouTubePlaybackState(state, { isPlayingRef, setIsPlaying })}
257
- onBuffering={handleWebBuffering}
258
- onEnded={handleEpisodeEnded}
259
- onError={handleWebError}
260
- />
261
- );
262
- }
263
229
  if (isElectron()) {
264
230
  return (
265
231
  <ElectronVideoPlayer
@@ -707,9 +673,10 @@ function FullscreenChatLayer(props) {
707
673
  }
708
674
 
709
675
  function FullscreenRecordingLayer(props) {
676
+ const recordingEligible = props.isLive;
710
677
  return (
711
678
  <>
712
- {!props.isAudioOnly && props.isLive && (props.isScreenRecorderEnabled || props.recStatus !== 'idle') ? (
679
+ {!props.isAudioOnly && recordingEligible && (props.isScreenRecorderEnabled || props.recStatus !== 'idle') ? (
713
680
  <LiveRecordingOverlay
714
681
  status={props.recStatus}
715
682
  elapsedMs={props.recElapsedMs}
@@ -721,7 +688,7 @@ function FullscreenRecordingLayer(props) {
721
688
  onStop={props.handleStopRecording}
722
689
  />
723
690
  ) : null}
724
- {!props.isAudioOnly && props.isLive && props.isScreenRecorderEnabled ? (
691
+ {!props.isAudioOnly && recordingEligible && props.isScreenRecorderEnabled ? (
725
692
  <LiveRecordingNotice notice={props.recNotice} colors={props.colors} onDismiss={props.onDismissNotice} />
726
693
  ) : null}
727
694
  </>
@@ -773,7 +740,7 @@ function getInlinePreviewPositionStyle(rect, isValid) {
773
740
  export const MediaPlayerView = ({
774
741
  visible,
775
742
  streamUrl,
776
- youtubeVideoId,
743
+ onBack,
777
744
  title,
778
745
  mediaType = 'live',
779
746
  onClose,
@@ -820,6 +787,7 @@ export const MediaPlayerView = ({
820
787
  style,
821
788
  onReady,
822
789
  onProgress,
790
+ showProgressBar = true,
823
791
  onProgressBarChange,
824
792
  onPlaying,
825
793
  onBuffering,
@@ -846,16 +814,16 @@ export const MediaPlayerView = ({
846
814
  const vlcRef = useRef(null);
847
815
  const playerRef = useRef(null);
848
816
  const invokeAction = useCallback((name, fallback, payload) => {
849
- const callback = name === 'onAspectRatioChange'
850
- ? actions?.[name] || onAspectRatioChange
851
- : actions?.[name];
817
+ let callback = actions?.[name];
818
+ if (name === 'onAspectRatioChange') callback = callback || onAspectRatioChange;
819
+ if (name === 'onBack') callback = callback || onBack || onClose;
852
820
  return invokePlayerAction(
853
821
  fallback,
854
822
  callback,
855
823
  payload,
856
824
  { player: playerApiRef?.current || null },
857
825
  );
858
- }, [actions, onAspectRatioChange, playerApiRef]);
826
+ }, [actions, onAspectRatioChange, onBack, onClose, playerApiRef]);
859
827
  const handlePlayerHostRef = useCallback((node) => {
860
828
  playerRef.current = node;
861
829
  onPlayerHostRef?.(node);
@@ -1212,6 +1180,11 @@ export const MediaPlayerView = ({
1212
1180
 
1213
1181
  const isLive = mediaType === 'live' || mediaType === 'channel';
1214
1182
  const isInlinePreview = !!inlinePreview;
1183
+ const progressBarVisible = showProgressBar !== false
1184
+ && controls.seek !== false
1185
+ && !isLive
1186
+ && !isInlinePreview;
1187
+ const progressBarCallback = progressBarVisible ? onProgressBarChange : undefined;
1215
1188
 
1216
1189
  useEffect(() => {
1217
1190
  if (!visible || !initialShowLiveChat || !isLive || !isLiveCommentsEnabled) return;
@@ -1478,9 +1451,9 @@ export const MediaPlayerView = ({
1478
1451
 
1479
1452
  const handleBackAction = useCallback(() => invokeAction(
1480
1453
  'onBack',
1481
- handleClose,
1454
+ undefined,
1482
1455
  { title, streamUrl, mediaId },
1483
- ), [invokeAction, handleClose, title, streamUrl, mediaId]);
1456
+ ), [invokeAction, title, streamUrl, mediaId]);
1484
1457
 
1485
1458
  useEffect(() => {
1486
1459
  if (!visible || isWeb()) return undefined;
@@ -1598,9 +1571,9 @@ export const MediaPlayerView = ({
1598
1571
  setSliderPos(0);
1599
1572
  lastKnownTimeRef.current = 0;
1600
1573
  seekCompletedAt.current = Date.now();
1601
- emitProgressBarTime(0, onProgressBarChange, lastProgressBarSecondRef, { force: true });
1574
+ emitProgressBarTime(0, progressBarCallback, lastProgressBarSecondRef, { force: true });
1602
1575
  if (!showControls) setShowControls(true);
1603
- }, [onProgressBarChange, showControls]);
1576
+ }, [progressBarCallback, showControls]);
1604
1577
 
1605
1578
  // Episode finished: auto-advance to the next episode (sequential) or to a
1606
1579
  // random episode (shuffle mode). Last episode in the playlist stops playback.
@@ -1799,7 +1772,7 @@ export const MediaPlayerView = ({
1799
1772
  lastKnownTimeRef,
1800
1773
  setSliderPosition: setSliderPos,
1801
1774
  });
1802
- if (progress) emitProgressBarTime(progress.seconds, onProgressBarChange, lastProgressBarSecondRef);
1775
+ if (progress) emitProgressBarTime(progress.seconds, progressBarCallback, lastProgressBarSecondRef);
1803
1776
  };
1804
1777
 
1805
1778
  const handleNativeOpen = (event) => {
@@ -1907,13 +1880,13 @@ export const MediaPlayerView = ({
1907
1880
  setSliderPos(target);
1908
1881
  seekCompletedAt.current = Date.now();
1909
1882
  debouncedSaveProgress();
1910
- emitProgressBarTime(target, onProgressBarChange, lastProgressBarSecondRef, { force: true });
1883
+ emitProgressBarTime(target, progressBarCallback, lastProgressBarSecondRef, { force: true });
1911
1884
  }, { seconds: Number(seconds) || 0, currentTime: lastKnownTimeRef.current },
1912
- ), [invokeAction, handleSeekTo, debouncedSaveProgress, onProgressBarChange]);
1885
+ ), [invokeAction, handleSeekTo, debouncedSaveProgress, progressBarCallback]);
1913
1886
  const handleSeekByAction = (deltaSeconds) => invokeAction(
1914
1887
  'onSeek', () => {
1915
1888
  handleSeekBy(deltaSeconds);
1916
- emitProgressBarTime(lastKnownTimeRef.current, onProgressBarChange, lastProgressBarSecondRef, { force: true });
1889
+ emitProgressBarTime(lastKnownTimeRef.current, progressBarCallback, lastProgressBarSecondRef, { force: true });
1917
1890
  }, { deltaSeconds: Number(deltaSeconds) || 0, currentTime: lastKnownTimeRef.current },
1918
1891
  );
1919
1892
  const handleMuteAction = useCallback(() => invokeAction(
@@ -1969,7 +1942,7 @@ export const MediaPlayerView = ({
1969
1942
  seekTo: handleSeekTo,
1970
1943
  seekBy: handleSeekBy,
1971
1944
  setPlaybackRate: (rate) => setPlaybackRate(clampNumber(rate, 0.25, 4, 1)),
1972
- back: handleClose,
1945
+ back: handleBackAction,
1973
1946
  getVideoElement: () => vlcRef.current?.getVideoElement?.() || null,
1974
1947
  getAudioTracks: () => audioTracks,
1975
1948
  };
@@ -1977,7 +1950,7 @@ export const MediaPlayerView = ({
1977
1950
  return () => {
1978
1951
  for (const key of Object.keys(api)) delete playerApiRef.current[key];
1979
1952
  };
1980
- }, [playerApiRef, togglePlayPause, handleRestart, toggleMute, handleSelectAspectRatio, handleAudioSelect, handleSeekTo, handleSeekBy, handleClose, audioTracks]);
1953
+ }, [playerApiRef, togglePlayPause, handleRestart, toggleMute, handleSelectAspectRatio, handleAudioSelect, handleSeekTo, handleSeekBy, handleBackAction, audioTracks]);
1981
1954
 
1982
1955
  const handleSpeedSelect = useCallback((speed) => invokeAction(
1983
1956
  'onPlaybackRateChange', () => {
@@ -2491,7 +2464,6 @@ export const MediaPlayerView = ({
2491
2464
 
2492
2465
  const videoPlayer = (
2493
2466
  <PlatformMediaSurface
2494
- youtubeVideoId={youtubeVideoId}
2495
2467
  playerStreamUrl={playerStreamUrl}
2496
2468
  vlcRef={vlcRef}
2497
2469
  isPlaying={isPlaying}
@@ -2520,11 +2492,8 @@ export const MediaPlayerView = ({
2520
2492
  computedAspectRatio={computedAspectRatio}
2521
2493
  handleNativeLoadStart={handleNativeLoadStart}
2522
2494
  handleNativeOpen={handleNativeOpen}
2523
- clearBufferingIndicator={clearBufferingIndicator}
2524
2495
  handleClose={handleClose}
2525
2496
  handleNativeBuffering={handleNativeBuffering}
2526
- isPlayingRef={isPlayingRef}
2527
- setIsPlaying={setIsPlaying}
2528
2497
  />
2529
2498
  );
2530
2499
  const isValidPreviewRect = isUsableInlinePreviewRect(inlinePreviewRect);
@@ -2600,7 +2569,7 @@ export const MediaPlayerView = ({
2600
2569
  isEpgEnabled,
2601
2570
  diagnosticsOverlayEnabled,
2602
2571
  muted,
2603
- controls,
2572
+ controls: { ...controls, seek: progressBarVisible },
2604
2573
  onClose: handleBackAction,
2605
2574
  handleRecordingAction,
2606
2575
  handleStartRecording,