@cinecrew/cinecrew-player 0.1.4 β 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 +30 -31
- package/package.json +1 -2
- package/src/native/InlineLivePlayer.js +88 -27
- package/src/native/MediaPlayerView.js +27 -58
- package/src/native/index.js +4 -5
- package/src/native/media/WebVideoPlayer.js +13 -0
- package/src/native/media/player/AudioOnlyView.js +3 -40
- package/src/native/media/web/useWebHlsPlayback.web.js +1 -0
- package/src/utils/invokePlayerAction.js +1 -1
- package/src/utils/sourceUtils.js +0 -25
- package/src/utils/webRecording.js +50 -3
- package/src/web/index.js +61 -76
- package/src/web/styles.css +13 -2
- package/types/index.d.ts +8 -2
- package/src/native/media/YouTubeVideoPlayer.native.js +0 -120
- package/src/native/media/YouTubeVideoPlayer.web.js +0 -225
- package/src/utils/youtubeHtml.js +0 -34
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
|
|
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
|
|
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
|
-
|
|
|
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
|
|
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
|
|
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
|
|
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
|
-
>
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
###
|
|
284
|
+
### Direct media URLs and share pages
|
|
287
285
|
|
|
288
|
-
Pass a
|
|
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.
|
|
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 | β |
|
|
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` | β |
|
|
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'
|
|
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
|
|
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` | β |
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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';
|
|
@@ -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(
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
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,
|
|
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 &&
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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
|
-
|
|
1454
|
+
undefined,
|
|
1482
1455
|
{ title, streamUrl, mediaId },
|
|
1483
|
-
), [invokeAction,
|
|
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,
|
|
1574
|
+
emitProgressBarTime(0, progressBarCallback, lastProgressBarSecondRef, { force: true });
|
|
1602
1575
|
if (!showControls) setShowControls(true);
|
|
1603
|
-
}, [
|
|
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,
|
|
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,
|
|
1883
|
+
emitProgressBarTime(target, progressBarCallback, lastProgressBarSecondRef, { force: true });
|
|
1911
1884
|
}, { seconds: Number(seconds) || 0, currentTime: lastKnownTimeRef.current },
|
|
1912
|
-
), [invokeAction, handleSeekTo, debouncedSaveProgress,
|
|
1885
|
+
), [invokeAction, handleSeekTo, debouncedSaveProgress, progressBarCallback]);
|
|
1913
1886
|
const handleSeekByAction = (deltaSeconds) => invokeAction(
|
|
1914
1887
|
'onSeek', () => {
|
|
1915
1888
|
handleSeekBy(deltaSeconds);
|
|
1916
|
-
emitProgressBarTime(lastKnownTimeRef.current,
|
|
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:
|
|
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,
|
|
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,
|