@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 +56 -46
- package/package.json +1 -2
- package/src/native/InlineLivePlayer.js +95 -33
- package/src/native/MediaPlayerView.js +75 -73
- package/src/native/index.js +4 -5
- package/src/native/media/LiveChatDrawer.js +16 -25
- package/src/native/media/WebVideoPlayer.js +13 -0
- package/src/native/media/player/AudioOnlyView.js +3 -40
- package/src/native/media/player/PlayerBottomBar.js +4 -12
- package/src/native/media/player/playerConstants.js +3 -3
- package/src/native/media/web/useWebAc3AudioPlayback.web.js +9 -3
- package/src/native/media/web/useWebHlsPlayback.web.js +1 -0
- package/src/native/media/web/useWebVideoAspectRatio.js +7 -2
- package/src/utils/invokePlayerAction.js +24 -0
- package/src/utils/progressBarTime.js +46 -0
- package/src/utils/sourceUtils.js +0 -25
- package/src/utils/webRecording.js +194 -0
- package/src/web/index.js +523 -156
- package/src/web/styles.css +142 -23
- package/types/index.d.ts +21 -2
- package/src/native/media/YouTubeVideoPlayer.native.js +0 -120
- package/src/native/media/YouTubeVideoPlayer.web.js +0 -223
- package/src/utils/youtubeHtml.js +0 -34
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
|
|
56
|
-
| ποΈ **Player controls** | Play/pause, seek, restart, mute, aspect ratio, lock,
|
|
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` |
|
|
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 |
|
|
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`:
|
|
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
|
|
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,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` | `{}` |
|
|
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
|
|
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 | β |
|
|
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
|
|
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'
|
|
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
|
|
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` | β |
|
|
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 |
|
|
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` |
|
|
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;
|
|
456
|
-
| `liveChat` | Chat drawer/panel; requires a chat adapter
|
|
457
|
-
| `epg` | EPG drawer/panel; requires an EPG adapter
|
|
458
|
-
| `diagnostics` | Stream diagnostics button; enable with `features={{ diagnostics: true }}
|
|
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
|
-
|
|
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: (
|
|
473
|
-
|
|
474
|
-
onMute: ({ muted }, {
|
|
475
|
-
onAspectRatioChange: ({ 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
|
|
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.
|
|
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
|
+
"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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
224
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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' },
|