@cinecrew/cinecrew-player 0.1.2 β 0.1.4
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 +31 -20
- package/package.json +1 -1
- package/src/native/InlineLivePlayer.js +8 -7
- package/src/native/MediaPlayerView.js +59 -26
- package/src/native/customization.js +2 -1
- package/src/native/media/LiveChatDrawer.js +16 -25
- package/src/native/media/YouTubeVideoPlayer.web.js +2 -0
- package/src/native/media/player/PlayerBottomBar.js +5 -13
- package/src/native/media/player/PlayerTopBar.js +1 -1
- package/src/native/media/player/playerConstants.js +3 -3
- package/src/native/media/web/useWebAc3AudioPlayback.web.js +9 -3
- package/src/native/media/web/useWebVideoAspectRatio.js +18 -3
- package/src/utils/invokePlayerAction.js +24 -0
- package/src/utils/progressBarTime.js +46 -0
- package/src/utils/webRecording.js +147 -0
- package/src/web/index.js +615 -128
- package/src/web/styles.css +291 -51
- package/types/index.d.ts +14 -1
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ flowchart LR
|
|
|
53
53
|
| Area | Included capabilities | Designed for |
|
|
54
54
|
|---|---|---|
|
|
55
55
|
| ποΈ **Playback** | On-demand and live media; URLs and local URIs; HLS and MPEG-TS paths on web; native VLC path; embedded YouTube playback | Movies, episodes, trailers, and channels |
|
|
56
|
-
| ποΈ **Player controls** | Play/pause, seek, restart, mute, aspect ratio, lock,
|
|
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 |
|
|
@@ -343,13 +343,15 @@ In short: CineCrewβs intended distinction is **one app-facing player package f
|
|
|
343
343
|
| `muted` | `boolean` | `false` | Initial mute state. |
|
|
344
344
|
| `volume` | `number` | `1` | Initial volume from `0` to `1`. |
|
|
345
345
|
| `playbackRate` | `number` | `1` | Initial playback speed; the on-demand speed control can change it afterward. |
|
|
346
|
+
| `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
|
+
| `defaultAspectRatio` | `string` | `'FIT'` | Initial and source-reset aspect mode. Must match an item in `aspectRatios` to appear selected. |
|
|
346
348
|
| `controls` | `PlayerControls` | defaults below | Show/hide individual control buttons. |
|
|
347
349
|
| `features` | feature flags | `{}` | Optional player features, including web stream diagnostics with `{ diagnostics: true }`. |
|
|
348
|
-
| `actions` | `PlayerActions` | `{}` |
|
|
350
|
+
| `actions` | `PlayerActions` | `{}` | Observe built-in actions. The player performs its core action first, then invokes the callback with the resulting action payload. |
|
|
349
351
|
| `integrations` | `PlayerIntegrations` | `{}` | Inject user identity, chat, EPG, recording, analytics, and presence services. |
|
|
350
352
|
| `drawerMode` | `'overlay' \| 'resize'` | `'overlay'` | Web/Electron drawer behavior: overlay the video or resize it to make room for chat, EPG, and diagnostics. |
|
|
351
353
|
| `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
|
|
354
|
+
| `messagePageSize` | `number` | `50` | Number of live-chat messages fetched per page; older messages load automatically when the list is scrolled to the top. |
|
|
353
355
|
| `theme` | `PlayerTheme` | built-in theme | Customize player colors, borders, and shape. |
|
|
354
356
|
| `icons` | `PlayerIcons` | built-in icons | Override any control icon by key. |
|
|
355
357
|
| `style` | platform style | β | Outer player style. On web this is a CSS style object; native uses React Native style props. |
|
|
@@ -365,13 +367,15 @@ In short: CineCrewβs intended distinction is **one app-facing player package f
|
|
|
365
367
|
| `playlist` | `object[]` | β | Episode list used for automatic next-episode behavior. |
|
|
366
368
|
| `shuffle` | `boolean` | `false` | Select a random next episode when the current episode ends. |
|
|
367
369
|
| `onClose`, `onBack` | callbacks | β | Player lifecycle/navigation callbacks. |
|
|
370
|
+
| `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. |
|
|
368
372
|
| `onReady`, `onProgress`, `onPlaying`, `onBuffering`, `onError`, `onEnded`, `onPlaybackRoute` | callbacks | β | Playback lifecycle callbacks. Progress payloads are platform-specific native/browser events. |
|
|
369
373
|
| `onNextEpisode`, `onCwRefresh` | callbacks | β | Episode advancement and post-close refresh hooks. |
|
|
370
374
|
| `renderLiveChat`, `renderEpg` | render functions | β | Web custom-panel render slots. On native, use the chat/EPG integration adapters. |
|
|
371
375
|
| `initialShowLiveChat`, `liveChatNonce` | `boolean`, `number` | `false`, `0` | Open or re-open the live-chat panel (when available). |
|
|
372
376
|
| `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
377
|
|
|
374
|
-
`features={{ diagnostics: true }}` enables the diagnostics button and built-in stream status panel on web; `controls.diagnostics` can hide it
|
|
378
|
+
`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
379
|
|
|
376
380
|
### Player source
|
|
377
381
|
|
|
@@ -406,7 +410,7 @@ type PlayerSource = string | {
|
|
|
406
410
|
| `onActivate` | `() => void` | β | Called when an inactive preview poster is selected. |
|
|
407
411
|
| `onFullscreen` | `() => void` | β | Called to promote/open the full player. |
|
|
408
412
|
| `controls` | `Pick<PlayerControls, 'playPause' \| 'mute' \| 'fullscreen'>` | all shown | Toggle its compact controls. |
|
|
409
|
-
| `actions` | matching `PlayerActions` subset | built-in |
|
|
413
|
+
| `actions` | matching `PlayerActions` subset | built-in | Observe play/pause, mute, or fullscreen actions after their built-in behavior runs. |
|
|
410
414
|
| `initialMuted` | `boolean` | `true` | Initial preview mute state. |
|
|
411
415
|
| `theme`, `icons`, `style` | `PlayerTheme`, `PlayerIcons`, platform style | defaults | Customize preview colors, controls, and layout. |
|
|
412
416
|
| `onError`, `onPlaying` | callbacks | β | Playback lifecycle callbacks. |
|
|
@@ -447,20 +451,22 @@ Every control can be hidden with `false`. Defaults are designed to be useful out
|
|
|
447
451
|
| `lock` | Lock/unlock touch controls. |
|
|
448
452
|
| `mute` | Mute/unmute. |
|
|
449
453
|
| `aspectRatio` | Fit/fill/stretch and available aspect choices. |
|
|
450
|
-
| `videoOnly` |
|
|
454
|
+
| `videoOnly` | Legacy compatibility setting; the player does not render a separate video-only button. |
|
|
451
455
|
| `audioOnly` | Show the audio-only card while playback continues. |
|
|
452
456
|
| `audioTracks` | Audio-track picker when tracks are exposed. |
|
|
453
457
|
| `playbackRate` | On-demand playback speed. |
|
|
454
458
|
| `fullscreen` | Fullscreen button on web and inline previews. Native player opens full-screen. |
|
|
455
|
-
| `recording` | Recording controls; requires
|
|
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 }}
|
|
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. |
|
|
460
|
+
| `liveChat` | Chat drawer/panel; requires a chat adapter or render slot. |
|
|
461
|
+
| `epg` | EPG drawer/panel; requires an EPG adapter or render slot. |
|
|
462
|
+
| `diagnostics` | Stream diagnostics button; enable with `features={{ diagnostics: true }}`. |
|
|
459
463
|
| `seek` | On-demand seek bar. |
|
|
460
464
|
|
|
461
465
|
## Actions and callbacks
|
|
462
466
|
|
|
463
|
-
Callbacks passed to `actions`
|
|
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.
|
|
468
|
+
|
|
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.
|
|
464
470
|
|
|
465
471
|
```tsx
|
|
466
472
|
const playerRef = React.useRef(null);
|
|
@@ -469,17 +475,18 @@ const playerRef = React.useRef(null);
|
|
|
469
475
|
ref={playerRef}
|
|
470
476
|
source={source}
|
|
471
477
|
actions={{
|
|
472
|
-
onRestart: (
|
|
473
|
-
|
|
474
|
-
onMute: ({ muted }, {
|
|
475
|
-
onAspectRatioChange: ({ aspectRatio }, {
|
|
478
|
+
onRestart: () => analytics.track('player_restart'),
|
|
479
|
+
onPlayPause: ({ isPlaying }) => analytics.track('player_play_pause', { isPlaying }),
|
|
480
|
+
onMute: ({ muted }) => analytics.track('player_mute', { muted }),
|
|
481
|
+
onAspectRatioChange: ({ aspectRatio }) => analytics.track('player_aspect_ratio', { aspectRatio }),
|
|
476
482
|
}}
|
|
483
|
+
onBack={() => navigation.goBack()}
|
|
477
484
|
/>
|
|
478
485
|
```
|
|
479
486
|
|
|
480
487
|
Available action keys: `onBack`, `onPlayPause`, `onSeek`, `onRestart`, `onLock`, `onMute`, `onAspectRatioChange`, `onVideoOnlyChange`, `onAudioOnlyChange`, `onAudioTrackChange`, `onPlaybackRateChange`, `onFullscreen`, `onRecordingStart`, `onRecordingPause`, `onRecordingResume`, `onRecordingStop`, `onLiveChatOpen`, `onEpgOpen`, and `onDiagnosticsOpen`.
|
|
481
488
|
|
|
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.
|
|
489
|
+
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
490
|
|
|
484
491
|
## Integrations
|
|
485
492
|
|
|
@@ -517,10 +524,14 @@ Integrations are optional. The package has no CineCrew account, database, or wor
|
|
|
517
524
|
/>
|
|
518
525
|
```
|
|
519
526
|
|
|
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
|
|
527
|
+
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
528
|
|
|
522
529
|
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
530
|
|
|
531
|
+
### Web recording
|
|
532
|
+
|
|
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.
|
|
534
|
+
|
|
524
535
|
## Themes and icons
|
|
525
536
|
|
|
526
537
|
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.
|
package/package.json
CHANGED
|
@@ -13,6 +13,7 @@ import { WebVideoPlayer } from './media/WebVideoPlayer';
|
|
|
13
13
|
import { ElectronVideoPlayer } from './media/ElectronVideoPlayer';
|
|
14
14
|
import { isAndroid, isElectron, isIOS, isWeb } from '../utils/runtimePlatform';
|
|
15
15
|
import { USER_AGENT } from './media/player/playerConstants';
|
|
16
|
+
import { invokePlayerAction } from '../utils/invokePlayerAction.js';
|
|
16
17
|
|
|
17
18
|
function getArtwork(channel) {
|
|
18
19
|
return channel?.logoUrl || channel?.logo || channel?.stream_icon || channel?.posterUrl || channel?.image || '';
|
|
@@ -82,8 +83,7 @@ function InlineControlButton({
|
|
|
82
83
|
accessibilityLabel: label,
|
|
83
84
|
onPress: () => {
|
|
84
85
|
const callback = actions?.[actionName];
|
|
85
|
-
|
|
86
|
-
else fallback?.();
|
|
86
|
+
invokePlayerAction(fallback, callback, payload, { player: null });
|
|
87
87
|
},
|
|
88
88
|
style: [styles.button, { backgroundColor: palette.controlBackground }, active && { borderColor: palette.accentColor, borderWidth: 1 }],
|
|
89
89
|
}, React.createElement(PlayerIcon, { name: icon, size: 19, color: palette.controlColor }));
|
|
@@ -117,7 +117,7 @@ function InlinePlayerOverlay({
|
|
|
117
117
|
palette,
|
|
118
118
|
});
|
|
119
119
|
const muteLabel = muted ? 'Unmute' : 'Mute';
|
|
120
|
-
const muteIcon = muted ? '
|
|
120
|
+
const muteIcon = muted ? 'mute' : 'unmute';
|
|
121
121
|
const playbackLabel = paused ? 'Play' : 'Pause';
|
|
122
122
|
const playbackIcon = paused ? 'play' : 'pause';
|
|
123
123
|
|
|
@@ -213,15 +213,16 @@ function InlineLivePlayerView({
|
|
|
213
213
|
}, [onPlaying]);
|
|
214
214
|
|
|
215
215
|
const performAction = (name, fallback, payload) => {
|
|
216
|
-
|
|
217
|
-
return fallback?.();
|
|
216
|
+
return invokePlayerAction(fallback, actions?.[name], payload, { player: null });
|
|
218
217
|
};
|
|
219
218
|
|
|
220
219
|
const togglePlay = (event) => {
|
|
221
220
|
event?.stopPropagation?.();
|
|
222
221
|
if (!shouldRenderVideo) {
|
|
223
|
-
|
|
224
|
-
|
|
222
|
+
performAction('onPlayPause', () => {
|
|
223
|
+
onActivate?.();
|
|
224
|
+
setInternallyPaused(false);
|
|
225
|
+
}, true);
|
|
225
226
|
return;
|
|
226
227
|
}
|
|
227
228
|
performAction('onPlayPause', () => setInternallyPaused((value) => !value), !pausedNow);
|
|
@@ -23,6 +23,8 @@ import { PlayerIcon } from './customization';
|
|
|
23
23
|
import { isWeb, isAndroid, isElectron } from '../utils/runtimePlatform';
|
|
24
24
|
import { getFontSize, getFontWeight } from '../utils/layoutUtils';
|
|
25
25
|
import { isLocalMediaUri } from '../utils/mediaUtils';
|
|
26
|
+
import { invokePlayerAction } from '../utils/invokePlayerAction.js';
|
|
27
|
+
import { emitProgressBarTime } from '../utils/progressBarTime.js';
|
|
26
28
|
import {
|
|
27
29
|
USER_AGENT,
|
|
28
30
|
calculateScreenAspectRatio,
|
|
@@ -69,6 +71,17 @@ function pickShuffleCandidate(candidates, season, episode) {
|
|
|
69
71
|
return candidates[(hash >>> 0) % candidates.length];
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
function normalizeAspectOptions(values) {
|
|
75
|
+
if (!Array.isArray(values) || values.length === 0) return ASPECT_OPTIONS;
|
|
76
|
+
const unique = new Map();
|
|
77
|
+
values.forEach((item) => {
|
|
78
|
+
const value = typeof item === 'string' ? item : item?.value;
|
|
79
|
+
if (!value || typeof value !== 'string') return;
|
|
80
|
+
unique.set(value, { value, label: typeof item === 'string' ? item : item.label || value });
|
|
81
|
+
});
|
|
82
|
+
return unique.size ? [...unique.values()] : ASPECT_OPTIONS;
|
|
83
|
+
}
|
|
84
|
+
|
|
72
85
|
// Gesture events can arrive much faster than the native player can consume
|
|
73
86
|
// volume updates. Coalesce them to display-rate updates for the UI and a
|
|
74
87
|
// slower native volume cadence so a drag stays smooth without audio crackle.
|
|
@@ -360,7 +373,7 @@ function InlinePreviewTopActions({ controls, muted, videoOnlyMode, handleMuteAct
|
|
|
360
373
|
handleMuteAction();
|
|
361
374
|
}}
|
|
362
375
|
>
|
|
363
|
-
<PlayerIcon name={muted || videoOnlyMode ? '
|
|
376
|
+
<PlayerIcon name={muted || videoOnlyMode ? 'mute' : 'unmute'} size={19} color="#FFF" />
|
|
364
377
|
</TouchableOpacity>
|
|
365
378
|
) : null}
|
|
366
379
|
</View>
|
|
@@ -590,7 +603,7 @@ function FullscreenControlsPanel(props) {
|
|
|
590
603
|
duration={props.duration}
|
|
591
604
|
isAudioOnlyFeatureEnabled={props.isAudioOnlyFeatureEnabled}
|
|
592
605
|
isAudioOnly={props.isAudioOnly}
|
|
593
|
-
|
|
606
|
+
aspectRatios={props.aspectRatios}
|
|
594
607
|
controls={props.controls}
|
|
595
608
|
showAspectPicker={props.showAspectPicker}
|
|
596
609
|
aspectRatio={props.aspectRatio}
|
|
@@ -611,7 +624,6 @@ function FullscreenControlsPanel(props) {
|
|
|
611
624
|
onToggleAudioPicker={props.onToggleAudioPicker}
|
|
612
625
|
onSelectAudioTrack={props.handleAudioTrackAction}
|
|
613
626
|
onToggleFullscreen={props.handleFullscreenAction}
|
|
614
|
-
onToggleVideoOnly={props.handleVideoOnlyAction}
|
|
615
627
|
/>
|
|
616
628
|
</>
|
|
617
629
|
);
|
|
@@ -790,6 +802,9 @@ export const MediaPlayerView = ({
|
|
|
790
802
|
liveChatNonce = 0,
|
|
791
803
|
messagePageSize = 50,
|
|
792
804
|
drawerStyle,
|
|
805
|
+
aspectRatios,
|
|
806
|
+
defaultAspectRatio = DEFAULT_ASPECT_RATIO,
|
|
807
|
+
onAspectRatioChange,
|
|
793
808
|
genre,
|
|
794
809
|
categoryName,
|
|
795
810
|
controls = {},
|
|
@@ -805,6 +820,7 @@ export const MediaPlayerView = ({
|
|
|
805
820
|
style,
|
|
806
821
|
onReady,
|
|
807
822
|
onProgress,
|
|
823
|
+
onProgressBarChange,
|
|
808
824
|
onPlaying,
|
|
809
825
|
onBuffering,
|
|
810
826
|
onError,
|
|
@@ -830,12 +846,16 @@ export const MediaPlayerView = ({
|
|
|
830
846
|
const vlcRef = useRef(null);
|
|
831
847
|
const playerRef = useRef(null);
|
|
832
848
|
const invokeAction = useCallback((name, fallback, payload) => {
|
|
833
|
-
const callback =
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
849
|
+
const callback = name === 'onAspectRatioChange'
|
|
850
|
+
? actions?.[name] || onAspectRatioChange
|
|
851
|
+
: actions?.[name];
|
|
852
|
+
return invokePlayerAction(
|
|
853
|
+
fallback,
|
|
854
|
+
callback,
|
|
855
|
+
payload,
|
|
856
|
+
{ player: playerApiRef?.current || null },
|
|
857
|
+
);
|
|
858
|
+
}, [actions, onAspectRatioChange, playerApiRef]);
|
|
839
859
|
const handlePlayerHostRef = useCallback((node) => {
|
|
840
860
|
playerRef.current = node;
|
|
841
861
|
onPlayerHostRef?.(node);
|
|
@@ -981,8 +1001,9 @@ export const MediaPlayerView = ({
|
|
|
981
1001
|
useEffect(() => {
|
|
982
1002
|
isLockedRef.current = isLocked;
|
|
983
1003
|
}, [isLocked]);
|
|
984
|
-
const [aspectRatio, setAspectRatio] = useState(DEFAULT_ASPECT_RATIO);
|
|
985
|
-
const aspectRatioRef = useRef(DEFAULT_ASPECT_RATIO);
|
|
1004
|
+
const [aspectRatio, setAspectRatio] = useState(defaultAspectRatio || DEFAULT_ASPECT_RATIO);
|
|
1005
|
+
const aspectRatioRef = useRef(defaultAspectRatio || DEFAULT_ASPECT_RATIO);
|
|
1006
|
+
const aspectOptions = useMemo(() => normalizeAspectOptions(aspectRatios), [aspectRatios]);
|
|
986
1007
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
987
1008
|
|
|
988
1009
|
const [audioTracks, setAudioTracks] = useState([]);
|
|
@@ -1112,7 +1133,7 @@ export const MediaPlayerView = ({
|
|
|
1112
1133
|
let targetRatio = val;
|
|
1113
1134
|
if (val === 'FIT') {
|
|
1114
1135
|
targetRatio = 'FIT';
|
|
1115
|
-
} else if (val === 'FILL_SCREEN') {
|
|
1136
|
+
} else if (val === 'FILL_SCREEN' || val === 'FILL') {
|
|
1116
1137
|
targetRatio = calculateScreenAspectRatio(windowWidth, windowHeight);
|
|
1117
1138
|
}
|
|
1118
1139
|
|
|
@@ -1148,6 +1169,7 @@ export const MediaPlayerView = ({
|
|
|
1148
1169
|
const isSeeking = useRef(false);
|
|
1149
1170
|
const bufferingTimerRef = useRef(null);
|
|
1150
1171
|
const seekCompletedAt = useRef(0);
|
|
1172
|
+
const lastProgressBarSecondRef = useRef(null);
|
|
1151
1173
|
const lastKnownTimeRef = useRef(0);
|
|
1152
1174
|
const lastKnownDurRef = useRef(Number(durationSecs) || 0);
|
|
1153
1175
|
const hasResumedRef = useRef(false);
|
|
@@ -1336,6 +1358,7 @@ export const MediaPlayerView = ({
|
|
|
1336
1358
|
setIsPlaying(!initialPaused);
|
|
1337
1359
|
isPlayingRef.current = !initialPaused;
|
|
1338
1360
|
setVideoOnlyMode(!!videoOnly);
|
|
1361
|
+
lastProgressBarSecondRef.current = null;
|
|
1339
1362
|
setPlaybackUrl(streamUrl || '');
|
|
1340
1363
|
setIsLocked(false);
|
|
1341
1364
|
setIsAudioOnly(!!initialAudioOnly);
|
|
@@ -1374,14 +1397,14 @@ export const MediaPlayerView = ({
|
|
|
1374
1397
|
// Reset aspect ratio & zoom state every time player opens or streamUrl changes
|
|
1375
1398
|
useEffect(() => {
|
|
1376
1399
|
if (visible) {
|
|
1377
|
-
setAspectRatio(DEFAULT_ASPECT_RATIO);
|
|
1378
|
-
aspectRatioRef.current = DEFAULT_ASPECT_RATIO;
|
|
1400
|
+
setAspectRatio(defaultAspectRatio || DEFAULT_ASPECT_RATIO);
|
|
1401
|
+
aspectRatioRef.current = defaultAspectRatio || DEFAULT_ASPECT_RATIO;
|
|
1379
1402
|
setZoomScale(1);
|
|
1380
1403
|
zoomScaleRef.current = 1;
|
|
1381
1404
|
setZoomBadgeText('');
|
|
1382
1405
|
setSeekRipple(null);
|
|
1383
1406
|
}
|
|
1384
|
-
}, [visible, streamUrl]);
|
|
1407
|
+
}, [visible, streamUrl, defaultAspectRatio]);
|
|
1385
1408
|
|
|
1386
1409
|
// Auto-hide controls 4 seconds after inactivity
|
|
1387
1410
|
const scheduleHide = useCallback(() => {
|
|
@@ -1575,8 +1598,9 @@ export const MediaPlayerView = ({
|
|
|
1575
1598
|
setSliderPos(0);
|
|
1576
1599
|
lastKnownTimeRef.current = 0;
|
|
1577
1600
|
seekCompletedAt.current = Date.now();
|
|
1601
|
+
emitProgressBarTime(0, onProgressBarChange, lastProgressBarSecondRef, { force: true });
|
|
1578
1602
|
if (!showControls) setShowControls(true);
|
|
1579
|
-
}, [showControls]);
|
|
1603
|
+
}, [onProgressBarChange, showControls]);
|
|
1580
1604
|
|
|
1581
1605
|
// Episode finished: auto-advance to the next episode (sequential) or to a
|
|
1582
1606
|
// random episode (shuffle mode). Last episode in the playlist stops playback.
|
|
@@ -1775,6 +1799,7 @@ export const MediaPlayerView = ({
|
|
|
1775
1799
|
lastKnownTimeRef,
|
|
1776
1800
|
setSliderPosition: setSliderPos,
|
|
1777
1801
|
});
|
|
1802
|
+
if (progress) emitProgressBarTime(progress.seconds, onProgressBarChange, lastProgressBarSecondRef);
|
|
1778
1803
|
};
|
|
1779
1804
|
|
|
1780
1805
|
const handleNativeOpen = (event) => {
|
|
@@ -1882,10 +1907,14 @@ export const MediaPlayerView = ({
|
|
|
1882
1907
|
setSliderPos(target);
|
|
1883
1908
|
seekCompletedAt.current = Date.now();
|
|
1884
1909
|
debouncedSaveProgress();
|
|
1910
|
+
emitProgressBarTime(target, onProgressBarChange, lastProgressBarSecondRef, { force: true });
|
|
1885
1911
|
}, { seconds: Number(seconds) || 0, currentTime: lastKnownTimeRef.current },
|
|
1886
|
-
), [invokeAction, handleSeekTo, debouncedSaveProgress]);
|
|
1912
|
+
), [invokeAction, handleSeekTo, debouncedSaveProgress, onProgressBarChange]);
|
|
1887
1913
|
const handleSeekByAction = (deltaSeconds) => invokeAction(
|
|
1888
|
-
'onSeek', () =>
|
|
1914
|
+
'onSeek', () => {
|
|
1915
|
+
handleSeekBy(deltaSeconds);
|
|
1916
|
+
emitProgressBarTime(lastKnownTimeRef.current, onProgressBarChange, lastProgressBarSecondRef, { force: true });
|
|
1917
|
+
}, { deltaSeconds: Number(deltaSeconds) || 0, currentTime: lastKnownTimeRef.current },
|
|
1889
1918
|
);
|
|
1890
1919
|
const handleMuteAction = useCallback(() => invokeAction(
|
|
1891
1920
|
'onMute', toggleMute, { muted: !mutedRef.current },
|
|
@@ -1912,14 +1941,17 @@ export const MediaPlayerView = ({
|
|
|
1912
1941
|
event?.stopPropagation?.();
|
|
1913
1942
|
return invokeAction(name, fallback, { title, streamUrl, mediaId });
|
|
1914
1943
|
}, [invokeAction, title, streamUrl, mediaId]);
|
|
1915
|
-
const handlePanelAction = useCallback((tab) =>
|
|
1916
|
-
|
|
1917
|
-
(
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1944
|
+
const handlePanelAction = useCallback((tab) => {
|
|
1945
|
+
const isOpen = !(showLiveChat && drawerTab === tab);
|
|
1946
|
+
return invokeAction(
|
|
1947
|
+
getPanelActionName(tab),
|
|
1948
|
+
() => {
|
|
1949
|
+
if (isOpen) { setDrawerTab(tab); setShowLiveChat(true); }
|
|
1950
|
+
else setShowLiveChat(false);
|
|
1951
|
+
},
|
|
1952
|
+
{ tab, isOpen },
|
|
1953
|
+
);
|
|
1954
|
+
}, [invokeAction, showLiveChat, drawerTab]);
|
|
1923
1955
|
|
|
1924
1956
|
useEffect(() => {
|
|
1925
1957
|
if (!playerApiRef) return undefined;
|
|
@@ -2591,6 +2623,7 @@ export const MediaPlayerView = ({
|
|
|
2591
2623
|
videoOnlyMode,
|
|
2592
2624
|
showAspectPicker,
|
|
2593
2625
|
aspectRatio,
|
|
2626
|
+
aspectRatios: aspectOptions,
|
|
2594
2627
|
showSpeedPicker,
|
|
2595
2628
|
playbackRate,
|
|
2596
2629
|
showAudioPicker,
|
|
@@ -40,5 +40,6 @@ export function PlayerIcon({ name, pack = 'community', size = 20, color = '#FFFF
|
|
|
40
40
|
return React.createElement(Text, { style: [{ fontSize: size, color }, style], accessibilityElementsHidden: true }, custom);
|
|
41
41
|
}
|
|
42
42
|
const IconSet = pack === 'material' ? MaterialIcons : MaterialCommunityIcons;
|
|
43
|
-
|
|
43
|
+
const defaultIconNames = { mute: 'volume-off', unmute: 'volume-high' };
|
|
44
|
+
return React.createElement(IconSet, { name: typeof custom === 'string' ? custom : (defaultIconNames[name] || name), size, color, style });
|
|
44
45
|
}
|
|
@@ -124,7 +124,6 @@ function LiveChatPanel({
|
|
|
124
124
|
messages,
|
|
125
125
|
flatListRef,
|
|
126
126
|
renderMessageItem,
|
|
127
|
-
hasMoreMessages,
|
|
128
127
|
loadOlderMessages,
|
|
129
128
|
loadingOlderMessages,
|
|
130
129
|
chatError,
|
|
@@ -161,19 +160,16 @@ function LiveChatPanel({
|
|
|
161
160
|
contentContainerStyle={styles.messagesList}
|
|
162
161
|
showsVerticalScrollIndicator
|
|
163
162
|
keyboardShouldPersistTaps="handled"
|
|
164
|
-
ListHeaderComponent={
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
accessibilityRole="button"
|
|
170
|
-
>
|
|
171
|
-
{loadingOlderMessages ? <ActivityIndicator size="small" color="#00E5FF" /> : null}
|
|
172
|
-
<Text style={styles.loadMoreMessagesText}>
|
|
173
|
-
{loadingOlderMessages ? 'Loading older messagesβ¦' : 'See more messages'}
|
|
174
|
-
</Text>
|
|
175
|
-
</TouchableOpacity>
|
|
163
|
+
ListHeaderComponent={loadingOlderMessages ? (
|
|
164
|
+
<View style={styles.loadingOlderMessages}>
|
|
165
|
+
<ActivityIndicator size="small" color="#00E5FF" />
|
|
166
|
+
<Text style={styles.loadingOlderMessagesText}>Loading older messagesβ¦</Text>
|
|
167
|
+
</View>
|
|
176
168
|
) : null}
|
|
169
|
+
onScroll={(event) => {
|
|
170
|
+
if (event.nativeEvent.contentOffset.y <= 24) loadOlderMessages();
|
|
171
|
+
}}
|
|
172
|
+
scrollEventThrottle={16}
|
|
177
173
|
ListEmptyComponent={<Text style={styles.epgStatusText}>No messages yet. Start the conversation.</Text>}
|
|
178
174
|
/>
|
|
179
175
|
)}
|
|
@@ -670,7 +666,6 @@ export const LiveChatDrawer = ({
|
|
|
670
666
|
messages={messages}
|
|
671
667
|
flatListRef={flatListRef}
|
|
672
668
|
renderMessageItem={renderMessageItem}
|
|
673
|
-
hasMoreMessages={hasMoreMessages}
|
|
674
669
|
loadOlderMessages={loadOlderMessages}
|
|
675
670
|
loadingOlderMessages={loadingOlderMessages}
|
|
676
671
|
chatError={chatError}
|
|
@@ -994,22 +989,18 @@ const styles = StyleSheet.create({
|
|
|
994
989
|
paddingVertical: 8,
|
|
995
990
|
gap: 8,
|
|
996
991
|
},
|
|
997
|
-
|
|
998
|
-
minHeight:
|
|
992
|
+
loadingOlderMessages: {
|
|
993
|
+
minHeight: 36,
|
|
999
994
|
flexDirection: 'row',
|
|
1000
995
|
alignItems: 'center',
|
|
1001
996
|
justifyContent: 'center',
|
|
1002
|
-
gap:
|
|
1003
|
-
|
|
1004
|
-
borderRadius: 12,
|
|
1005
|
-
borderWidth: StyleSheet.hairlineWidth,
|
|
1006
|
-
borderColor: 'rgba(0, 229, 255, 0.35)',
|
|
1007
|
-
backgroundColor: 'rgba(0, 229, 255, 0.08)',
|
|
997
|
+
gap: 7,
|
|
998
|
+
paddingVertical: 6,
|
|
1008
999
|
},
|
|
1009
|
-
|
|
1000
|
+
loadingOlderMessagesText: {
|
|
1010
1001
|
color: '#00E5FF',
|
|
1011
|
-
fontSize:
|
|
1012
|
-
fontWeight: '
|
|
1002
|
+
fontSize: 12,
|
|
1003
|
+
fontWeight: '600',
|
|
1013
1004
|
},
|
|
1014
1005
|
chatErrorText: {
|
|
1015
1006
|
color: '#FF7A8A',
|
|
@@ -109,10 +109,12 @@ export const YouTubeVideoPlayer = forwardRef(function YouTubeVideoPlayer({
|
|
|
109
109
|
enablejsapi: 1,
|
|
110
110
|
fs: 0,
|
|
111
111
|
iv_load_policy: 3,
|
|
112
|
+
modestbranding: 1,
|
|
112
113
|
mute: latestPropsRef.current.muted ? 1 : 0,
|
|
113
114
|
origin: window.location.origin,
|
|
114
115
|
playsinline: 1,
|
|
115
116
|
rel: 0,
|
|
117
|
+
showinfo: 0,
|
|
116
118
|
},
|
|
117
119
|
events: {
|
|
118
120
|
onReady: (event) => {
|
|
@@ -21,7 +21,7 @@ function SeekControls({ isLive, controls, insets, scale, isSeeking, sliderPos, c
|
|
|
21
21
|
);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function AspectRatioControl({ controls, open, aspectRatio, onToggle, onSelect }) {
|
|
24
|
+
function AspectRatioControl({ controls, open, aspectRatio, aspectRatios = ASPECT_OPTIONS, onToggle, onSelect }) {
|
|
25
25
|
const palette = usePlayerColors();
|
|
26
26
|
if (controls.aspectRatio === false) return null;
|
|
27
27
|
const selectedStyle = { backgroundColor: palette.surfaceColor, borderWidth: 1.5, borderColor: palette.accentColor };
|
|
@@ -29,7 +29,7 @@ function AspectRatioControl({ controls, open, aspectRatio, onToggle, onSelect })
|
|
|
29
29
|
<View style={styles.speedButtonContainer}>
|
|
30
30
|
{open ? <View style={[styles.speedPickerPopup, { right: 'auto', left: 0, backgroundColor: palette.surfaceColor, borderColor: palette.borderColor }]}>
|
|
31
31
|
<Text style={[styles.speedPickerTitle, { color: palette.mutedColor }]}>Aspect Ratio</Text>
|
|
32
|
-
{
|
|
32
|
+
{aspectRatios.map((option) => {
|
|
33
33
|
const selected = aspectRatio === option.value;
|
|
34
34
|
return <TouchableOpacity key={option.label} style={styles.speedOption} onPress={(event) => { event.stopPropagation(); onSelect(option.value); }} activeOpacity={0.7}>
|
|
35
35
|
<PlayerIcon name="check" size={15} color={selected ? palette.accentColor : 'transparent'} />
|
|
@@ -50,12 +50,6 @@ function AudioOnlyModeControl({ isAudioOnly, onToggle }) {
|
|
|
50
50
|
return <TouchableOpacity style={[styles.speedButton, { backgroundColor: palette.controlBackground }, selectedStyle]} onPress={(event) => { event.stopPropagation(); onToggle(); }} accessibilityLabel="Audio-Only Mode"><PlayerIcon name="headphones" size={18} color={isAudioOnly ? palette.accentColor : palette.controlColor} /></TouchableOpacity>;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
function VideoOnlyControl({ isVideoOnly, onToggle }) {
|
|
54
|
-
const palette = usePlayerColors();
|
|
55
|
-
const selectedStyle = isVideoOnly ? { backgroundColor: palette.surfaceColor, borderWidth: 1.5, borderColor: palette.accentColor } : null;
|
|
56
|
-
return <TouchableOpacity style={[styles.speedButton, { backgroundColor: palette.controlBackground }, selectedStyle]} onPress={(event) => { event.stopPropagation(); onToggle?.(); }} accessibilityLabel={isVideoOnly ? 'Enable audio' : 'Video only'}><PlayerIcon name="video" size={18} color={isVideoOnly ? palette.accentColor : palette.controlColor} /></TouchableOpacity>;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
53
|
function SpeedControl({ enabled, open, playbackRate, onToggle, onSelect }) {
|
|
60
54
|
const palette = usePlayerColors();
|
|
61
55
|
if (!enabled) return null;
|
|
@@ -81,7 +75,7 @@ function SpeedControl({ enabled, open, playbackRate, onToggle, onSelect }) {
|
|
|
81
75
|
|
|
82
76
|
function AudioTrackMenu({ open, audioTracks, selectedAudioTrack, onSelect, palette }) {
|
|
83
77
|
if (!open) return null;
|
|
84
|
-
if (audioTracks.length
|
|
78
|
+
if (audioTracks.length === 0) return <View style={[styles.speedPickerPopup, { backgroundColor: palette.surfaceColor, borderColor: palette.borderColor }]}><Text style={[styles.speedOptionText, { color: palette.controlColor }]}>No audio tracks available</Text></View>;
|
|
85
79
|
return (
|
|
86
80
|
<View style={[styles.speedPickerPopup, { backgroundColor: palette.surfaceColor, borderColor: palette.borderColor }]}>
|
|
87
81
|
<Text style={[styles.speedPickerTitle, { color: palette.mutedColor }]}>Audio</Text>
|
|
@@ -129,6 +123,7 @@ export const PlayerBottomBar = ({
|
|
|
129
123
|
isAudioOnly,
|
|
130
124
|
showAspectPicker,
|
|
131
125
|
aspectRatio,
|
|
126
|
+
aspectRatios = ASPECT_OPTIONS,
|
|
132
127
|
showSpeedPicker,
|
|
133
128
|
playbackRate,
|
|
134
129
|
showAudioPicker,
|
|
@@ -147,8 +142,6 @@ export const PlayerBottomBar = ({
|
|
|
147
142
|
onSelectAudioTrack,
|
|
148
143
|
onToggleFullscreen,
|
|
149
144
|
controls = {},
|
|
150
|
-
isVideoOnly = false,
|
|
151
|
-
onToggleVideoOnly,
|
|
152
145
|
}) => {
|
|
153
146
|
return (
|
|
154
147
|
<View
|
|
@@ -167,8 +160,7 @@ export const PlayerBottomBar = ({
|
|
|
167
160
|
<View style={styles.bottomControlsRow} pointerEvents="box-none">
|
|
168
161
|
<View style={styles.leftActionsContainer}>
|
|
169
162
|
{isAudioOnlyFeatureEnabled && controls.audioOnly !== false ? <AudioOnlyModeControl isAudioOnly={isAudioOnly} onToggle={onToggleAudioOnly} /> : null}
|
|
170
|
-
<AspectRatioControl controls={controls} open={showAspectPicker} aspectRatio={aspectRatio} onToggle={onToggleAspectPicker} onSelect={onSelectAspectRatio} />
|
|
171
|
-
{controls.videoOnly ? <VideoOnlyControl isVideoOnly={isVideoOnly} onToggle={onToggleVideoOnly} /> : null}
|
|
163
|
+
<AspectRatioControl controls={controls} open={showAspectPicker} aspectRatio={aspectRatio} aspectRatios={aspectRatios} onToggle={onToggleAspectPicker} onSelect={onSelectAspectRatio} />
|
|
172
164
|
</View>
|
|
173
165
|
|
|
174
166
|
{/* RIGHT ACTIONS: Playback Speed, Audio, Fullscreen */}
|
|
@@ -60,7 +60,7 @@ function PlaybackSessionControls({ isLive, controls, muted, onRestart, onMute, o
|
|
|
60
60
|
return (
|
|
61
61
|
<>
|
|
62
62
|
{!isLive && controls.restart !== false ? <TouchableOpacity style={styles.pill} onPress={(event) => { event.stopPropagation(); onRestart(); }} hitSlop={12}><PlayerIcon name="restart" size={20} color={palette.controlColor} /></TouchableOpacity> : null}
|
|
63
|
-
{controls.mute !== false ? <TouchableOpacity style={styles.pill} onPress={(event) => { event.stopPropagation(); onMute(); }} hitSlop={12}><PlayerIcon name={muted ? '
|
|
63
|
+
{controls.mute !== false ? <TouchableOpacity style={styles.pill} onPress={(event) => { event.stopPropagation(); onMute(); }} hitSlop={12}><PlayerIcon name={muted ? 'mute' : 'unmute'} size={20} color={muted ? palette.errorColor : palette.controlColor} /></TouchableOpacity> : null}
|
|
64
64
|
{controls.lock !== false ? <TouchableOpacity style={styles.pill} onPress={(event) => { event.stopPropagation(); onLock(); }} hitSlop={12}><PlayerIcon name="lock-open-variant" size={20} color={palette.controlColor} /></TouchableOpacity> : null}
|
|
65
65
|
</>
|
|
66
66
|
);
|
|
@@ -38,8 +38,9 @@ export function nearestPipCorner(x, y, winW, winH, insets) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export const ASPECT_OPTIONS = [
|
|
41
|
-
{ label: 'Fit
|
|
42
|
-
{ label: 'Fill
|
|
41
|
+
{ label: 'Fit', value: 'FIT' },
|
|
42
|
+
{ label: 'Fill', value: 'FILL' },
|
|
43
|
+
{ label: 'Stretch', value: 'STRETCH' },
|
|
43
44
|
{ label: '16:9', value: '16:9' },
|
|
44
45
|
{ label: '4:3', value: '4:3' },
|
|
45
46
|
{ label: '21:9', value: '21:9' },
|
|
@@ -156,4 +157,3 @@ export function applyTrackDefaults(selected, setSelected, setTracks, tracksProp)
|
|
|
156
157
|
const active = tracksProp.find((t) => t.selected || t.active) || tracksProp[0];
|
|
157
158
|
if (active) setSelected(active.id);
|
|
158
159
|
}
|
|
159
|
-
|