react-native-video-provider 0.1.1
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/AuVideo.podspec +28 -0
- package/LICENSE +20 -0
- package/README.md +183 -0
- package/android/build.gradle +59 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/auvideo/AuVideoModule.kt +259 -0
- package/android/src/main/java/com/auvideo/AuVideoPackage.kt +35 -0
- package/android/src/main/java/com/auvideo/AuVideoSurfaceView.kt +40 -0
- package/android/src/main/java/com/auvideo/AuVideoSurfaceViewManager.kt +38 -0
- package/android/src/main/java/com/auvideo/PlayerCore.kt +366 -0
- package/android/src/main/java/com/auvideo/SurfaceRegistry.kt +26 -0
- package/android/src/main/res/layout/au_video_player_view.xml +10 -0
- package/ios/AuVideo.h +10 -0
- package/ios/AuVideo.mm +232 -0
- package/ios/AuVideoOrientation.swift +59 -0
- package/ios/AuVideoPlayerCore.swift +415 -0
- package/ios/AuVideoSurfaceRegistry.swift +30 -0
- package/ios/AuVideoSurfaceView.h +15 -0
- package/ios/AuVideoSurfaceView.mm +63 -0
- package/lib/module/AuVideoSurfaceNativeComponent.ts +8 -0
- package/lib/module/NativeAuVideo.js +5 -0
- package/lib/module/NativeAuVideo.js.map +1 -0
- package/lib/module/components/FloatingPlayer.js +119 -0
- package/lib/module/components/FloatingPlayer.js.map +1 -0
- package/lib/module/components/FullscreenPlayer.js +59 -0
- package/lib/module/components/FullscreenPlayer.js.map +1 -0
- package/lib/module/components/GestureOverlay.js +56 -0
- package/lib/module/components/GestureOverlay.js.map +1 -0
- package/lib/module/components/MiniPlayer.js +89 -0
- package/lib/module/components/MiniPlayer.js.map +1 -0
- package/lib/module/components/VideoControls.js +191 -0
- package/lib/module/components/VideoControls.js.map +1 -0
- package/lib/module/components/VideoPlayer.js +65 -0
- package/lib/module/components/VideoPlayer.js.map +1 -0
- package/lib/module/components/VideoSurface.js +35 -0
- package/lib/module/components/VideoSurface.js.map +1 -0
- package/lib/module/core/VideoManager.js +436 -0
- package/lib/module/core/VideoManager.js.map +1 -0
- package/lib/module/hooks/useFullscreen.js +19 -0
- package/lib/module/hooks/useFullscreen.js.map +1 -0
- package/lib/module/hooks/usePiP.js +17 -0
- package/lib/module/hooks/usePiP.js.map +1 -0
- package/lib/module/hooks/usePlayback.js +21 -0
- package/lib/module/hooks/usePlayback.js.map +1 -0
- package/lib/module/hooks/useVideo.js +17 -0
- package/lib/module/hooks/useVideo.js.map +1 -0
- package/lib/module/hooks/useVideoEvents.js +26 -0
- package/lib/module/hooks/useVideoEvents.js.map +1 -0
- package/lib/module/index.js +19 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/provider/VideoContext.js +14 -0
- package/lib/module/provider/VideoContext.js.map +1 -0
- package/lib/module/provider/VideoProvider.js +36 -0
- package/lib/module/provider/VideoProvider.js.map +1 -0
- package/lib/module/state/createVideoStore.js +33 -0
- package/lib/module/state/createVideoStore.js.map +1 -0
- package/lib/module/types/events.js +4 -0
- package/lib/module/types/events.js.map +1 -0
- package/lib/module/types/index.js +2 -0
- package/lib/module/types/index.js.map +1 -0
- package/lib/module/types/video.js +2 -0
- package/lib/module/types/video.js.map +1 -0
- package/lib/module/utils/Emitter.js +39 -0
- package/lib/module/utils/Emitter.js.map +1 -0
- package/lib/module/utils/formatTime.js +15 -0
- package/lib/module/utils/formatTime.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/AuVideoSurfaceNativeComponent.d.ts +8 -0
- package/lib/typescript/src/AuVideoSurfaceNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/NativeAuVideo.d.ts +85 -0
- package/lib/typescript/src/NativeAuVideo.d.ts.map +1 -0
- package/lib/typescript/src/components/FloatingPlayer.d.ts +12 -0
- package/lib/typescript/src/components/FloatingPlayer.d.ts.map +1 -0
- package/lib/typescript/src/components/FullscreenPlayer.d.ts +12 -0
- package/lib/typescript/src/components/FullscreenPlayer.d.ts.map +1 -0
- package/lib/typescript/src/components/GestureOverlay.d.ts +14 -0
- package/lib/typescript/src/components/GestureOverlay.d.ts.map +1 -0
- package/lib/typescript/src/components/MiniPlayer.d.ts +16 -0
- package/lib/typescript/src/components/MiniPlayer.d.ts.map +1 -0
- package/lib/typescript/src/components/VideoControls.d.ts +17 -0
- package/lib/typescript/src/components/VideoControls.d.ts.map +1 -0
- package/lib/typescript/src/components/VideoPlayer.d.ts +27 -0
- package/lib/typescript/src/components/VideoPlayer.d.ts.map +1 -0
- package/lib/typescript/src/components/VideoSurface.d.ts +18 -0
- package/lib/typescript/src/components/VideoSurface.d.ts.map +1 -0
- package/lib/typescript/src/core/VideoManager.d.ts +88 -0
- package/lib/typescript/src/core/VideoManager.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useFullscreen.d.ts +7 -0
- package/lib/typescript/src/hooks/useFullscreen.d.ts.map +1 -0
- package/lib/typescript/src/hooks/usePiP.d.ts +6 -0
- package/lib/typescript/src/hooks/usePiP.d.ts.map +1 -0
- package/lib/typescript/src/hooks/usePlayback.d.ts +13 -0
- package/lib/typescript/src/hooks/usePlayback.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useVideo.d.ts +13 -0
- package/lib/typescript/src/hooks/useVideo.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useVideoEvents.d.ts +14 -0
- package/lib/typescript/src/hooks/useVideoEvents.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +18 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/provider/VideoContext.d.ts +8 -0
- package/lib/typescript/src/provider/VideoContext.d.ts.map +1 -0
- package/lib/typescript/src/provider/VideoProvider.d.ts +18 -0
- package/lib/typescript/src/provider/VideoProvider.d.ts.map +1 -0
- package/lib/typescript/src/state/createVideoStore.d.ts +6 -0
- package/lib/typescript/src/state/createVideoStore.d.ts.map +1 -0
- package/lib/typescript/src/types/events.d.ts +46 -0
- package/lib/typescript/src/types/events.d.ts.map +1 -0
- package/lib/typescript/src/types/index.d.ts +3 -0
- package/lib/typescript/src/types/index.d.ts.map +1 -0
- package/lib/typescript/src/types/video.d.ts +70 -0
- package/lib/typescript/src/types/video.d.ts.map +1 -0
- package/lib/typescript/src/utils/Emitter.d.ts +15 -0
- package/lib/typescript/src/utils/Emitter.d.ts.map +1 -0
- package/lib/typescript/src/utils/formatTime.d.ts +2 -0
- package/lib/typescript/src/utils/formatTime.d.ts.map +1 -0
- package/package.json +154 -0
- package/src/AuVideoSurfaceNativeComponent.ts +8 -0
- package/src/NativeAuVideo.ts +106 -0
- package/src/components/FloatingPlayer.tsx +130 -0
- package/src/components/FullscreenPlayer.tsx +64 -0
- package/src/components/GestureOverlay.tsx +76 -0
- package/src/components/MiniPlayer.tsx +93 -0
- package/src/components/VideoControls.tsx +213 -0
- package/src/components/VideoPlayer.tsx +77 -0
- package/src/components/VideoSurface.tsx +39 -0
- package/src/core/VideoManager.ts +441 -0
- package/src/hooks/useFullscreen.ts +14 -0
- package/src/hooks/usePiP.ts +13 -0
- package/src/hooks/usePlayback.ts +26 -0
- package/src/hooks/useVideo.ts +16 -0
- package/src/hooks/useVideoEvents.ts +33 -0
- package/src/index.ts +54 -0
- package/src/provider/VideoContext.ts +12 -0
- package/src/provider/VideoProvider.tsx +41 -0
- package/src/state/createVideoStore.ts +33 -0
- package/src/types/events.ts +22 -0
- package/src/types/index.ts +11 -0
- package/src/types/video.ts +85 -0
- package/src/utils/Emitter.ts +51 -0
- package/src/utils/formatTime.ts +12 -0
package/AuVideo.podspec
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "AuVideo"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
+
s.source = { :git => "https://github.com/grdp99/react-native-au-video.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
|
|
17
|
+
s.private_header_files = "ios/**/*.h"
|
|
18
|
+
|
|
19
|
+
s.frameworks = "AVFoundation", "AVKit"
|
|
20
|
+
s.swift_version = "5.9"
|
|
21
|
+
# Swift core + ObjC++ glue in one pod: the glue imports the generated
|
|
22
|
+
# <AuVideo/AuVideo-Swift.h>, which requires the pod to define a module.
|
|
23
|
+
s.pod_target_xcconfig = {
|
|
24
|
+
"DEFINES_MODULE" => "YES",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
install_modules_dependencies(s)
|
|
28
|
+
end
|
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gurdeep Singh
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# react-native-video-provider
|
|
2
|
+
|
|
3
|
+
**One native player. Many surfaces. Zero interruptions.**
|
|
4
|
+
|
|
5
|
+
A singleton-engine video library for React Native (Android + iOS, New
|
|
6
|
+
Architecture). The app owns exactly one native playback engine (ExoPlayer /
|
|
7
|
+
AVPlayer) for its whole lifetime; React components are just *rendering
|
|
8
|
+
surfaces* the engine attaches to. Moving from a feed cell to a detail screen
|
|
9
|
+
to fullscreen to a floating window never reloads, rebuffers, or resets
|
|
10
|
+
position — exactly how YouTube, Netflix and Twitter/X players behave.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
Feed ──▶ Detail ──▶ Fullscreen (rotation unlocked) ──▶ Floating window
|
|
14
|
+
same engine · same buffer · same position
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- 🎯 **Singleton engine** — created silently once by the provider; survives
|
|
18
|
+
navigation, tab switches, unmounts, Fast Refresh
|
|
19
|
+
- 🔁 **Same-video handoff** — `setSource` with the same `id` is a no-op for
|
|
20
|
+
the engine; the player just re-parents to the new surface
|
|
21
|
+
- 📺 **Fullscreen** — built-in host that unlocks rotation while visible and
|
|
22
|
+
restores the previous orientation lock on exit
|
|
23
|
+
- 🎈 **Floating player** — built-in draggable in-app window
|
|
24
|
+
- 🖼 **Picture in Picture** — Android + iOS
|
|
25
|
+
- ⚡ **New Architecture native** — TurboModule + Fabric, typed end to end
|
|
26
|
+
- 🧠 **Zustand-powered state** — selector subscriptions, no re-render storms
|
|
27
|
+
|
|
28
|
+
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full design and
|
|
29
|
+
[docs/API.md](docs/API.md) for the complete API reference.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npm install react-native-video-provider
|
|
35
|
+
cd ios && pod install
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Requires React Native 0.80+ with the New Architecture enabled.
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
**1. Wrap the app once.** The provider silently creates the engine and mounts
|
|
43
|
+
the fullscreen/floating hosts above your app:
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import { VideoProvider } from 'react-native-video-provider';
|
|
47
|
+
|
|
48
|
+
export default function App() {
|
|
49
|
+
return (
|
|
50
|
+
<VideoProvider>
|
|
51
|
+
<Navigation />
|
|
52
|
+
</VideoProvider>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**2. Play a video anywhere:**
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { VideoPlayer } from 'react-native-video-provider';
|
|
61
|
+
|
|
62
|
+
<VideoPlayer
|
|
63
|
+
source={{ id: '123', uri: 'https://example.com/video.m3u8', title: 'Big Buck Bunny' }}
|
|
64
|
+
style={{ aspectRatio: 16 / 9 }}
|
|
65
|
+
/>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**3. Open a detail screen with the same video** — because the `id` matches,
|
|
69
|
+
the engine is untouched and playback continues from the exact frame:
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
// DetailScreen.tsx — same source id ⇒ handoff, not reload
|
|
73
|
+
<VideoPlayer source={{ id: '123', uri }} style={{ aspectRatio: 16 / 9 }} />
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**4. Fullscreen / floating / PiP from anywhere:**
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
const player = useVideo();
|
|
80
|
+
|
|
81
|
+
player.enterFullscreen(); // rotation unlocks while visible
|
|
82
|
+
player.showFloating(); // draggable in-app window
|
|
83
|
+
player.enterPiP(); // system picture-in-picture
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Surfaces (the core idea)
|
|
87
|
+
|
|
88
|
+
`<VideoSurface>` never creates a player — it registers a mount point. The
|
|
89
|
+
engine renders into at most one surface at a time; `attach(id)` re-parents
|
|
90
|
+
the native player view with no playback interruption. If you attach to a
|
|
91
|
+
surface that hasn't mounted yet (navigation in flight), the engine attaches
|
|
92
|
+
the moment it appears.
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
<VideoSurface surfaceId="feed" style={{ aspectRatio: 16 / 9 }} />
|
|
96
|
+
|
|
97
|
+
const player = useVideo();
|
|
98
|
+
player.setSource(video); // load (or hand off)
|
|
99
|
+
player.attach('feed'); // render here
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`<VideoPlayer>` is the convenience wrapper that does `setSource` + `attach` +
|
|
103
|
+
optional controls in one component.
|
|
104
|
+
|
|
105
|
+
## State & events
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
// Selector subscriptions — re-render only for what you display
|
|
109
|
+
const position = usePlayback((s) => s.position);
|
|
110
|
+
const { isFullscreen, toggle } = useFullscreen();
|
|
111
|
+
|
|
112
|
+
useVideoEvents({
|
|
113
|
+
onEnd: () => playNext(),
|
|
114
|
+
onError: (e) => console.warn(e.code, e.message),
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Platform setup
|
|
119
|
+
|
|
120
|
+
### iOS — fullscreen rotation
|
|
121
|
+
|
|
122
|
+
iOS asks the AppDelegate which orientations are allowed. Forward that to the
|
|
123
|
+
library so rotation unlocks only while fullscreen is visible (skip this if
|
|
124
|
+
your app already supports all orientations):
|
|
125
|
+
|
|
126
|
+
```swift
|
|
127
|
+
// AppDelegate.swift
|
|
128
|
+
import AuVideo
|
|
129
|
+
|
|
130
|
+
func application(_ application: UIApplication,
|
|
131
|
+
supportedInterfaceOrientationsFor window: UIWindow?)
|
|
132
|
+
-> UIInterfaceOrientationMask {
|
|
133
|
+
return AuVideoOrientation.mask(withDefault: .portrait)
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### iOS — Picture in Picture / background audio
|
|
138
|
+
|
|
139
|
+
Enable *Audio, AirPlay and Picture in Picture* in Signing & Capabilities →
|
|
140
|
+
Background Modes (adds `audio` to `UIBackgroundModes` in Info.plist).
|
|
141
|
+
|
|
142
|
+
### Android — Picture in Picture
|
|
143
|
+
|
|
144
|
+
Declare PiP support on your main activity in `AndroidManifest.xml`:
|
|
145
|
+
|
|
146
|
+
```xml
|
|
147
|
+
<activity
|
|
148
|
+
android:name=".MainActivity"
|
|
149
|
+
android:supportsPictureInPicture="true"
|
|
150
|
+
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|density" />
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
(The `orientation|screenSize` entries are part of the default RN template and
|
|
154
|
+
also keep fullscreen rotation from recreating the activity.)
|
|
155
|
+
|
|
156
|
+
## Example app
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
yarn
|
|
160
|
+
yarn example android # or: yarn example ios
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The example shows a feed → detail handoff with a live status panel, plus
|
|
164
|
+
fullscreen, floating and PiP buttons.
|
|
165
|
+
|
|
166
|
+
## Roadmap
|
|
167
|
+
|
|
168
|
+
- Queue (next/previous/playlist/autoplay)
|
|
169
|
+
- Background playback (Android MediaSessionService + notification, iOS remote
|
|
170
|
+
commands / lock-screen controls)
|
|
171
|
+
- Quality, subtitle and audio-track selection
|
|
172
|
+
- Brightness/volume swipe gestures, pinch zoom
|
|
173
|
+
- True ahead-of-time preloading (Media3 `PreloadManager`)
|
|
174
|
+
|
|
175
|
+
## Contributing
|
|
176
|
+
|
|
177
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) and the
|
|
178
|
+
[architecture doc](docs/ARCHITECTURE.md). PRs that add a second player
|
|
179
|
+
instance will be rejected on principle. 🙂
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.AuVideo = [
|
|
3
|
+
kotlinVersion: "2.0.21",
|
|
4
|
+
minSdkVersion: 24,
|
|
5
|
+
compileSdkVersion: 36,
|
|
6
|
+
media3Version: "1.8.0"
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
ext.getExtOrDefault = { prop ->
|
|
10
|
+
if (rootProject.ext.has(prop)) {
|
|
11
|
+
return rootProject.ext.get(prop)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return AuVideo[prop]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
repositories {
|
|
18
|
+
google()
|
|
19
|
+
mavenCentral()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
dependencies {
|
|
23
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
24
|
+
// noinspection DifferentKotlinGradleVersion
|
|
25
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
apply plugin: "com.android.library"
|
|
31
|
+
apply plugin: "kotlin-android"
|
|
32
|
+
|
|
33
|
+
apply plugin: "com.facebook.react"
|
|
34
|
+
|
|
35
|
+
android {
|
|
36
|
+
namespace "com.auvideo"
|
|
37
|
+
|
|
38
|
+
compileSdkVersion getExtOrDefault("compileSdkVersion")
|
|
39
|
+
|
|
40
|
+
defaultConfig {
|
|
41
|
+
minSdkVersion getExtOrDefault("minSdkVersion")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
compileOptions {
|
|
45
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
46
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
dependencies {
|
|
51
|
+
implementation "com.facebook.react:react-android"
|
|
52
|
+
|
|
53
|
+
def media3Version = getExtOrDefault("media3Version")
|
|
54
|
+
implementation "androidx.media3:media3-exoplayer:${media3Version}"
|
|
55
|
+
implementation "androidx.media3:media3-exoplayer-hls:${media3Version}"
|
|
56
|
+
implementation "androidx.media3:media3-exoplayer-dash:${media3Version}"
|
|
57
|
+
implementation "androidx.media3:media3-ui:${media3Version}"
|
|
58
|
+
implementation "androidx.core:core-ktx:1.13.1"
|
|
59
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
package com.auvideo
|
|
2
|
+
|
|
3
|
+
import android.app.PictureInPictureParams
|
|
4
|
+
import android.content.pm.ActivityInfo
|
|
5
|
+
import android.content.pm.PackageManager
|
|
6
|
+
import android.os.Build
|
|
7
|
+
import android.util.Rational
|
|
8
|
+
import androidx.core.view.WindowCompat
|
|
9
|
+
import androidx.core.view.WindowInsetsCompat
|
|
10
|
+
import androidx.core.view.WindowInsetsControllerCompat
|
|
11
|
+
import com.facebook.react.bridge.Arguments
|
|
12
|
+
import com.facebook.react.bridge.Promise
|
|
13
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
14
|
+
import com.facebook.react.bridge.ReadableMap
|
|
15
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
16
|
+
|
|
17
|
+
class AuVideoModule(reactContext: ReactApplicationContext) :
|
|
18
|
+
NativeAuVideoSpec(reactContext), PlayerCore.Listener {
|
|
19
|
+
|
|
20
|
+
private var previousOrientation: Int? = null
|
|
21
|
+
|
|
22
|
+
// ------------------------------------------------------------ lifecycle
|
|
23
|
+
|
|
24
|
+
override fun nativeInit() {
|
|
25
|
+
UiThreadUtil.runOnUiThread {
|
|
26
|
+
PlayerCore.listener = this
|
|
27
|
+
PlayerCore.initialize(reactApplicationContext)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
override fun releasePlayer() {
|
|
32
|
+
UiThreadUtil.runOnUiThread {
|
|
33
|
+
PlayerCore.release()
|
|
34
|
+
PlayerCore.listener = null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
override fun invalidate() {
|
|
39
|
+
PlayerCore.listener = null
|
|
40
|
+
super.invalidate()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// -------------------------------------------------------------- source
|
|
44
|
+
|
|
45
|
+
override fun setSource(source: ReadableMap, autoplay: Boolean) {
|
|
46
|
+
val spec = parseSource(source) ?: return
|
|
47
|
+
UiThreadUtil.runOnUiThread { PlayerCore.setSource(spec, autoplay) }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override fun preload(source: ReadableMap) {
|
|
51
|
+
val spec = parseSource(source) ?: return
|
|
52
|
+
UiThreadUtil.runOnUiThread { PlayerCore.preload(spec) }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private fun parseSource(map: ReadableMap): SourceSpec? {
|
|
56
|
+
val id = map.getString("id") ?: return null
|
|
57
|
+
val uri = map.getString("uri") ?: return null
|
|
58
|
+
val headers = HashMap<String, String>()
|
|
59
|
+
map.getMap("headers")?.let { h ->
|
|
60
|
+
val it = h.keySetIterator()
|
|
61
|
+
while (it.hasNextKey()) {
|
|
62
|
+
val key = it.nextKey()
|
|
63
|
+
h.getString(key)?.let { value -> headers[key] = value }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return SourceSpec(
|
|
67
|
+
id = id,
|
|
68
|
+
uri = uri,
|
|
69
|
+
headers = headers,
|
|
70
|
+
title = map.getString("title"),
|
|
71
|
+
artist = map.getString("artist"),
|
|
72
|
+
artworkUri = map.getString("artworkUri"),
|
|
73
|
+
startPosition = if (map.hasKey("startPosition")) map.getDouble("startPosition") else 0.0,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ------------------------------------------------------------- commands
|
|
78
|
+
|
|
79
|
+
override fun play() {
|
|
80
|
+
UiThreadUtil.runOnUiThread { PlayerCore.play() }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
override fun pause() {
|
|
84
|
+
UiThreadUtil.runOnUiThread { PlayerCore.pause() }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
override fun stop() {
|
|
88
|
+
UiThreadUtil.runOnUiThread { PlayerCore.stop() }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
override fun seekTo(position: Double) {
|
|
92
|
+
UiThreadUtil.runOnUiThread { PlayerCore.seekTo(position) }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
override fun setRate(rate: Double) {
|
|
96
|
+
UiThreadUtil.runOnUiThread { PlayerCore.setRate(rate) }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
override fun setVolume(volume: Double) {
|
|
100
|
+
UiThreadUtil.runOnUiThread { PlayerCore.setVolume(volume) }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
override fun setMuted(muted: Boolean) {
|
|
104
|
+
UiThreadUtil.runOnUiThread { PlayerCore.setMuted(muted) }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
override fun setRepeat(repeat: Boolean) {
|
|
108
|
+
UiThreadUtil.runOnUiThread { PlayerCore.setRepeat(repeat) }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
override fun setResizeMode(mode: String) {
|
|
112
|
+
UiThreadUtil.runOnUiThread { PlayerCore.setResizeMode(mode) }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
override fun getPosition(promise: Promise) {
|
|
116
|
+
UiThreadUtil.runOnUiThread { promise.resolve(PlayerCore.positionSeconds()) }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ------------------------------------------------------------- surfaces
|
|
120
|
+
|
|
121
|
+
override fun attach(surfaceId: String) {
|
|
122
|
+
UiThreadUtil.runOnUiThread { PlayerCore.attach(surfaceId) }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
override fun detach() {
|
|
126
|
+
UiThreadUtil.runOnUiThread { PlayerCore.detach() }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ----------------------------------------------------------- fullscreen
|
|
130
|
+
|
|
131
|
+
override fun enterFullscreen() {
|
|
132
|
+
UiThreadUtil.runOnUiThread {
|
|
133
|
+
val activity = currentActivity ?: return@runOnUiThread
|
|
134
|
+
if (previousOrientation == null) {
|
|
135
|
+
previousOrientation = activity.requestedOrientation
|
|
136
|
+
}
|
|
137
|
+
// Unlock rotation while fullscreen is visible, even in portrait-locked apps.
|
|
138
|
+
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
|
|
139
|
+
|
|
140
|
+
val window = activity.window
|
|
141
|
+
WindowCompat.setDecorFitsSystemWindows(window, false)
|
|
142
|
+
WindowInsetsControllerCompat(window, window.decorView).apply {
|
|
143
|
+
systemBarsBehavior =
|
|
144
|
+
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
|
145
|
+
hide(WindowInsetsCompat.Type.systemBars())
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
override fun exitFullscreen() {
|
|
151
|
+
UiThreadUtil.runOnUiThread {
|
|
152
|
+
val activity = currentActivity ?: return@runOnUiThread
|
|
153
|
+
activity.requestedOrientation =
|
|
154
|
+
previousOrientation ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
155
|
+
previousOrientation = null
|
|
156
|
+
|
|
157
|
+
val window = activity.window
|
|
158
|
+
WindowCompat.setDecorFitsSystemWindows(window, true)
|
|
159
|
+
WindowInsetsControllerCompat(window, window.decorView)
|
|
160
|
+
.show(WindowInsetsCompat.Type.systemBars())
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ------------------------------------------------------------------ pip
|
|
165
|
+
|
|
166
|
+
override fun enterPip(promise: Promise) {
|
|
167
|
+
UiThreadUtil.runOnUiThread {
|
|
168
|
+
val activity = currentActivity
|
|
169
|
+
if (
|
|
170
|
+
activity == null ||
|
|
171
|
+
Build.VERSION.SDK_INT < Build.VERSION_CODES.O ||
|
|
172
|
+
!activity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
|
|
173
|
+
) {
|
|
174
|
+
promise.resolve(false)
|
|
175
|
+
return@runOnUiThread
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
val params = PictureInPictureParams.Builder()
|
|
179
|
+
.setAspectRatio(Rational(16, 9))
|
|
180
|
+
.build()
|
|
181
|
+
val entered = activity.enterPictureInPictureMode(params)
|
|
182
|
+
if (entered) {
|
|
183
|
+
emitOnPipChange(Arguments.createMap().apply { putBoolean("active", true) })
|
|
184
|
+
}
|
|
185
|
+
promise.resolve(entered)
|
|
186
|
+
} catch (e: Exception) {
|
|
187
|
+
promise.resolve(false)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
override fun exitPip() {
|
|
193
|
+
UiThreadUtil.runOnUiThread {
|
|
194
|
+
// Android has no direct "leave PiP" API; bringing the task forward
|
|
195
|
+
// expands the PiP window back into the full activity.
|
|
196
|
+
val activity = currentActivity ?: return@runOnUiThread
|
|
197
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && activity.isInPictureInPictureMode) {
|
|
198
|
+
activity.moveTaskToBack(false)
|
|
199
|
+
emitOnPipChange(Arguments.createMap().apply { putBoolean("active", false) })
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// --------------------------------------------------- PlayerCore.Listener
|
|
205
|
+
|
|
206
|
+
override fun onStatusChange(status: String) {
|
|
207
|
+
emitOnStatusChange(Arguments.createMap().apply { putString("status", status) })
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
override fun onLoad(videoId: String, duration: Double, width: Int, height: Int) {
|
|
211
|
+
emitOnLoad(
|
|
212
|
+
Arguments.createMap().apply {
|
|
213
|
+
putString("videoId", videoId)
|
|
214
|
+
putDouble("duration", duration)
|
|
215
|
+
putDouble("width", width.toDouble())
|
|
216
|
+
putDouble("height", height.toDouble())
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
override fun onProgress(position: Double, duration: Double, buffered: Double) {
|
|
222
|
+
emitOnProgress(
|
|
223
|
+
Arguments.createMap().apply {
|
|
224
|
+
putDouble("position", position)
|
|
225
|
+
putDouble("duration", duration)
|
|
226
|
+
putDouble("buffered", buffered)
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
override fun onSeek(position: Double) {
|
|
232
|
+
emitOnSeek(Arguments.createMap().apply { putDouble("position", position) })
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
override fun onEnd() {
|
|
236
|
+
emitOnEnd()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
override fun onError(code: String, message: String) {
|
|
240
|
+
emitOnError(
|
|
241
|
+
Arguments.createMap().apply {
|
|
242
|
+
putString("code", code)
|
|
243
|
+
putString("message", message)
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
override fun onAttach(surfaceId: String) {
|
|
249
|
+
emitOnAttach(Arguments.createMap().apply { putString("surfaceId", surfaceId) })
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
override fun onDetach(surfaceId: String) {
|
|
253
|
+
emitOnDetach(Arguments.createMap().apply { putString("surfaceId", surfaceId) })
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
companion object {
|
|
257
|
+
const val NAME = NativeAuVideoSpec.NAME
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package com.auvideo
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
import com.facebook.react.uimanager.ViewManager
|
|
9
|
+
|
|
10
|
+
class AuVideoPackage : BaseReactPackage() {
|
|
11
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
12
|
+
return if (name == AuVideoModule.NAME) {
|
|
13
|
+
AuVideoModule(reactContext)
|
|
14
|
+
} else {
|
|
15
|
+
null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun createViewManagers(
|
|
20
|
+
reactContext: ReactApplicationContext
|
|
21
|
+
): List<ViewManager<*, *>> = listOf(AuVideoSurfaceViewManager())
|
|
22
|
+
|
|
23
|
+
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
|
|
24
|
+
mapOf(
|
|
25
|
+
AuVideoModule.NAME to ReactModuleInfo(
|
|
26
|
+
name = AuVideoModule.NAME,
|
|
27
|
+
className = AuVideoModule.NAME,
|
|
28
|
+
canOverrideExistingModule = false,
|
|
29
|
+
needsEagerInit = false,
|
|
30
|
+
isCxxModule = false,
|
|
31
|
+
isTurboModule = true
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
package com.auvideo
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.view.View
|
|
5
|
+
import android.widget.FrameLayout
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A dumb mount point the player view gets re-parented into. Does not create
|
|
9
|
+
* or own any player resources.
|
|
10
|
+
*/
|
|
11
|
+
class AuVideoSurfaceView(context: Context) : FrameLayout(context) {
|
|
12
|
+
|
|
13
|
+
var surfaceId: String? = null
|
|
14
|
+
set(value) {
|
|
15
|
+
if (field == value) return
|
|
16
|
+
field?.let { SurfaceRegistry.unregister(it, this) }
|
|
17
|
+
field = value
|
|
18
|
+
value?.let { SurfaceRegistry.register(it, this) }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// RN lays this view out itself; children added natively (the player view)
|
|
22
|
+
// would otherwise never get measured. Standard measure-and-layout relay.
|
|
23
|
+
private val measureAndLayout = Runnable {
|
|
24
|
+
measure(
|
|
25
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
26
|
+
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
|
27
|
+
)
|
|
28
|
+
layout(left, top, right, bottom)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
override fun requestLayout() {
|
|
32
|
+
super.requestLayout()
|
|
33
|
+
post(measureAndLayout)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override fun onViewAdded(child: View?) {
|
|
37
|
+
super.onViewAdded(child)
|
|
38
|
+
post(measureAndLayout)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package com.auvideo
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
4
|
+
import com.facebook.react.uimanager.SimpleViewManager
|
|
5
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
7
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
8
|
+
import com.facebook.react.viewmanagers.AuVideoSurfaceViewManagerDelegate
|
|
9
|
+
import com.facebook.react.viewmanagers.AuVideoSurfaceViewManagerInterface
|
|
10
|
+
|
|
11
|
+
@ReactModule(name = AuVideoSurfaceViewManager.NAME)
|
|
12
|
+
class AuVideoSurfaceViewManager :
|
|
13
|
+
SimpleViewManager<AuVideoSurfaceView>(),
|
|
14
|
+
AuVideoSurfaceViewManagerInterface<AuVideoSurfaceView> {
|
|
15
|
+
|
|
16
|
+
private val delegate = AuVideoSurfaceViewManagerDelegate(this)
|
|
17
|
+
|
|
18
|
+
override fun getDelegate(): ViewManagerDelegate<AuVideoSurfaceView> = delegate
|
|
19
|
+
|
|
20
|
+
override fun getName(): String = NAME
|
|
21
|
+
|
|
22
|
+
override fun createViewInstance(context: ThemedReactContext): AuVideoSurfaceView =
|
|
23
|
+
AuVideoSurfaceView(context)
|
|
24
|
+
|
|
25
|
+
@ReactProp(name = "surfaceId")
|
|
26
|
+
override fun setSurfaceId(view: AuVideoSurfaceView, value: String?) {
|
|
27
|
+
view.surfaceId = value
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
override fun onDropViewInstance(view: AuVideoSurfaceView) {
|
|
31
|
+
view.surfaceId = null
|
|
32
|
+
super.onDropViewInstance(view)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
companion object {
|
|
36
|
+
const val NAME = "AuVideoSurfaceView"
|
|
37
|
+
}
|
|
38
|
+
}
|