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
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
package com.auvideo
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.net.Uri
|
|
6
|
+
import android.os.Handler
|
|
7
|
+
import android.os.Looper
|
|
8
|
+
import android.view.LayoutInflater
|
|
9
|
+
import android.view.ViewGroup
|
|
10
|
+
import android.widget.FrameLayout
|
|
11
|
+
import androidx.media3.common.C
|
|
12
|
+
import androidx.media3.common.MediaItem
|
|
13
|
+
import androidx.media3.common.MediaMetadata
|
|
14
|
+
import androidx.media3.common.PlaybackException
|
|
15
|
+
import androidx.media3.common.Player
|
|
16
|
+
import androidx.media3.datasource.DefaultDataSource
|
|
17
|
+
import androidx.media3.datasource.DefaultHttpDataSource
|
|
18
|
+
import androidx.media3.exoplayer.ExoPlayer
|
|
19
|
+
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
|
20
|
+
import androidx.media3.ui.AspectRatioFrameLayout
|
|
21
|
+
import androidx.media3.ui.PlayerView
|
|
22
|
+
|
|
23
|
+
data class SourceSpec(
|
|
24
|
+
val id: String,
|
|
25
|
+
val uri: String,
|
|
26
|
+
val headers: Map<String, String>,
|
|
27
|
+
val title: String?,
|
|
28
|
+
val artist: String?,
|
|
29
|
+
val artworkUri: String?,
|
|
30
|
+
val startPosition: Double,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The ONE playback engine of the app.
|
|
35
|
+
*
|
|
36
|
+
* Owns a single ExoPlayer and a single TextureView-backed PlayerView that is
|
|
37
|
+
* re-parented between registered [AuVideoSurfaceView]s. Created lazily on
|
|
38
|
+
* first init and only destroyed by an explicit release() — React component
|
|
39
|
+
* lifecycles never touch it.
|
|
40
|
+
*
|
|
41
|
+
* Everything here must run on the main thread; public entry points marshal.
|
|
42
|
+
*/
|
|
43
|
+
@SuppressLint("UnsafeOptInUsageError")
|
|
44
|
+
object PlayerCore {
|
|
45
|
+
|
|
46
|
+
interface Listener {
|
|
47
|
+
fun onStatusChange(status: String)
|
|
48
|
+
fun onLoad(videoId: String, duration: Double, width: Int, height: Int)
|
|
49
|
+
fun onProgress(position: Double, duration: Double, buffered: Double)
|
|
50
|
+
fun onSeek(position: Double)
|
|
51
|
+
fun onEnd()
|
|
52
|
+
fun onError(code: String, message: String)
|
|
53
|
+
fun onAttach(surfaceId: String)
|
|
54
|
+
fun onDetach(surfaceId: String)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
var listener: Listener? = null
|
|
58
|
+
|
|
59
|
+
private const val PROGRESS_INTERVAL_MS = 500L
|
|
60
|
+
|
|
61
|
+
private var appContext: Context? = null
|
|
62
|
+
private var player: ExoPlayer? = null
|
|
63
|
+
private var playerView: PlayerView? = null
|
|
64
|
+
|
|
65
|
+
private var currentVideoId: String? = null
|
|
66
|
+
private var currentSurfaceId: String? = null
|
|
67
|
+
|
|
68
|
+
/** Surface we want but that hasn't registered (yet, or again). */
|
|
69
|
+
private var pendingSurfaceId: String? = null
|
|
70
|
+
|
|
71
|
+
private var loadReported = false
|
|
72
|
+
private val preloaded = HashMap<String, MediaItem>()
|
|
73
|
+
|
|
74
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
75
|
+
|
|
76
|
+
private val progressRunnable = object : Runnable {
|
|
77
|
+
override fun run() {
|
|
78
|
+
val p = player ?: return
|
|
79
|
+
emitProgress(p)
|
|
80
|
+
mainHandler.postDelayed(this, PROGRESS_INTERVAL_MS)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fun runOnMain(block: () -> Unit) {
|
|
85
|
+
if (Looper.myLooper() == Looper.getMainLooper()) block() else mainHandler.post(block)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ------------------------------------------------------------- lifecycle
|
|
89
|
+
|
|
90
|
+
/** Idempotent. */
|
|
91
|
+
fun initialize(context: Context) {
|
|
92
|
+
appContext = context.applicationContext
|
|
93
|
+
if (player != null) return
|
|
94
|
+
|
|
95
|
+
val exo = ExoPlayer.Builder(context.applicationContext).build()
|
|
96
|
+
exo.addListener(playerListener)
|
|
97
|
+
player = exo
|
|
98
|
+
|
|
99
|
+
// Inflated from XML because surface_type can only be set via attrs.
|
|
100
|
+
val view = LayoutInflater.from(context.applicationContext)
|
|
101
|
+
.inflate(R.layout.au_video_player_view, null) as PlayerView
|
|
102
|
+
view.player = exo
|
|
103
|
+
playerView = view
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fun release() {
|
|
107
|
+
stopProgress()
|
|
108
|
+
detach()
|
|
109
|
+
player?.release()
|
|
110
|
+
playerView?.player = null
|
|
111
|
+
player = null
|
|
112
|
+
playerView = null
|
|
113
|
+
currentVideoId = null
|
|
114
|
+
pendingSurfaceId = null
|
|
115
|
+
preloaded.clear()
|
|
116
|
+
listener?.onStatusChange("idle")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// --------------------------------------------------------------- source
|
|
120
|
+
|
|
121
|
+
fun setSource(source: SourceSpec, autoplay: Boolean) {
|
|
122
|
+
val exo = requirePlayer() ?: return
|
|
123
|
+
|
|
124
|
+
if (source.id == currentVideoId) {
|
|
125
|
+
// Same-video handoff: never reload; at most honor autoplay.
|
|
126
|
+
if (autoplay && !exo.isPlaying) play()
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
currentVideoId = source.id
|
|
131
|
+
loadReported = false
|
|
132
|
+
listener?.onStatusChange("loading")
|
|
133
|
+
|
|
134
|
+
val item = preloaded.remove(source.id) ?: buildMediaItem(source)
|
|
135
|
+
val mediaSource = buildMediaSourceFactory(source.headers).createMediaSource(item)
|
|
136
|
+
|
|
137
|
+
if (source.startPosition > 0) {
|
|
138
|
+
exo.setMediaSource(mediaSource, (source.startPosition * 1000).toLong())
|
|
139
|
+
} else {
|
|
140
|
+
exo.setMediaSource(mediaSource, true)
|
|
141
|
+
}
|
|
142
|
+
exo.playWhenReady = autoplay
|
|
143
|
+
exo.prepare()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fun preload(source: SourceSpec) {
|
|
147
|
+
// v0.1: pre-builds the MediaItem so attach-time setup is instant.
|
|
148
|
+
// Real ahead-of-time buffering via Media3 PreloadManager is roadmap and
|
|
149
|
+
// stays isolated behind this method.
|
|
150
|
+
preloaded[source.id] = buildMediaItem(source)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private fun buildMediaItem(source: SourceSpec): MediaItem {
|
|
154
|
+
val metadata = MediaMetadata.Builder()
|
|
155
|
+
.setTitle(source.title)
|
|
156
|
+
.setArtist(source.artist)
|
|
157
|
+
.setArtworkUri(source.artworkUri?.let(Uri::parse))
|
|
158
|
+
.build()
|
|
159
|
+
return MediaItem.Builder()
|
|
160
|
+
.setUri(Uri.parse(source.uri))
|
|
161
|
+
.setMediaId(source.id)
|
|
162
|
+
.setMediaMetadata(metadata)
|
|
163
|
+
.build()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private fun buildMediaSourceFactory(headers: Map<String, String>): DefaultMediaSourceFactory {
|
|
167
|
+
val context = requireNotNull(appContext)
|
|
168
|
+
val httpFactory = DefaultHttpDataSource.Factory()
|
|
169
|
+
.setAllowCrossProtocolRedirects(true)
|
|
170
|
+
if (headers.isNotEmpty()) {
|
|
171
|
+
httpFactory.setDefaultRequestProperties(headers)
|
|
172
|
+
}
|
|
173
|
+
return DefaultMediaSourceFactory(DefaultDataSource.Factory(context, httpFactory))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ------------------------------------------------------------- commands
|
|
177
|
+
|
|
178
|
+
fun play() {
|
|
179
|
+
val exo = player ?: return
|
|
180
|
+
// After stop() the player sits in IDLE with its item retained.
|
|
181
|
+
if (exo.playbackState == Player.STATE_IDLE && exo.mediaItemCount > 0) {
|
|
182
|
+
exo.prepare()
|
|
183
|
+
}
|
|
184
|
+
exo.play()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
fun pause() {
|
|
188
|
+
player?.pause()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
fun stop() {
|
|
192
|
+
player?.stop()
|
|
193
|
+
stopProgress()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fun seekTo(positionSec: Double) {
|
|
197
|
+
player?.seekTo((positionSec * 1000).toLong())
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
fun setRate(rate: Double) {
|
|
201
|
+
player?.setPlaybackSpeed(rate.toFloat())
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
fun setVolume(volume: Double) {
|
|
205
|
+
player?.volume = volume.toFloat().coerceIn(0f, 1f)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
fun setMuted(muted: Boolean) {
|
|
209
|
+
val exo = player ?: return
|
|
210
|
+
if (muted) {
|
|
211
|
+
exo.volume = 0f
|
|
212
|
+
} else if (exo.volume == 0f) {
|
|
213
|
+
exo.volume = 1f
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
fun setRepeat(repeat: Boolean) {
|
|
218
|
+
player?.repeatMode = if (repeat) Player.REPEAT_MODE_ONE else Player.REPEAT_MODE_OFF
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fun setResizeMode(mode: String) {
|
|
222
|
+
playerView?.resizeMode = when (mode) {
|
|
223
|
+
"cover" -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
|
|
224
|
+
"stretch" -> AspectRatioFrameLayout.RESIZE_MODE_FILL
|
|
225
|
+
else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fun positionSeconds(): Double = (player?.currentPosition ?: 0L) / 1000.0
|
|
230
|
+
|
|
231
|
+
// ------------------------------------------------------------- surfaces
|
|
232
|
+
|
|
233
|
+
fun attach(surfaceId: String) {
|
|
234
|
+
val view = SurfaceRegistry.get(surfaceId)
|
|
235
|
+
if (view == null) {
|
|
236
|
+
// Screen still mounting — attach the moment it registers.
|
|
237
|
+
pendingSurfaceId = surfaceId
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
attachTo(view, surfaceId)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fun detach() {
|
|
244
|
+
val pv = playerView ?: return
|
|
245
|
+
(pv.parent as? ViewGroup)?.removeView(pv)
|
|
246
|
+
currentSurfaceId?.let { listener?.onDetach(it) }
|
|
247
|
+
currentSurfaceId = null
|
|
248
|
+
pendingSurfaceId = null
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
fun onSurfaceAvailable(surfaceId: String, view: AuVideoSurfaceView) {
|
|
252
|
+
// Also re-attach when the active surface's view was recreated (e.g.
|
|
253
|
+
// navigating back to a screen that Fabric re-materialized).
|
|
254
|
+
if (surfaceId == pendingSurfaceId || surfaceId == currentSurfaceId) {
|
|
255
|
+
attachTo(view, surfaceId)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
fun onSurfaceUnavailable(surfaceId: String, view: AuVideoSurfaceView) {
|
|
260
|
+
val pv = playerView ?: return
|
|
261
|
+
if (currentSurfaceId == surfaceId && pv.parent === view) {
|
|
262
|
+
view.removeView(pv)
|
|
263
|
+
currentSurfaceId = null
|
|
264
|
+
// Keep playing hidden (audio); remounting the same surface re-attaches.
|
|
265
|
+
pendingSurfaceId = surfaceId
|
|
266
|
+
listener?.onDetach(surfaceId)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private fun attachTo(container: AuVideoSurfaceView, surfaceId: String) {
|
|
271
|
+
val pv = playerView ?: return
|
|
272
|
+
if (currentSurfaceId == surfaceId && pv.parent === container) {
|
|
273
|
+
pendingSurfaceId = null
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
currentSurfaceId?.let { previous -> if (previous != surfaceId) listener?.onDetach(previous) }
|
|
277
|
+
(pv.parent as? ViewGroup)?.removeView(pv)
|
|
278
|
+
container.addView(
|
|
279
|
+
pv,
|
|
280
|
+
FrameLayout.LayoutParams(
|
|
281
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
282
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
currentSurfaceId = surfaceId
|
|
286
|
+
pendingSurfaceId = null
|
|
287
|
+
listener?.onAttach(surfaceId)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// --------------------------------------------------------------- events
|
|
291
|
+
|
|
292
|
+
private val playerListener = object : Player.Listener {
|
|
293
|
+
override fun onPlaybackStateChanged(state: Int) {
|
|
294
|
+
val exo = player ?: return
|
|
295
|
+
when (state) {
|
|
296
|
+
Player.STATE_BUFFERING -> listener?.onStatusChange("buffering")
|
|
297
|
+
Player.STATE_READY -> {
|
|
298
|
+
if (!loadReported) {
|
|
299
|
+
loadReported = true
|
|
300
|
+
val size = exo.videoSize
|
|
301
|
+
val duration =
|
|
302
|
+
if (exo.duration == C.TIME_UNSET) 0.0 else exo.duration / 1000.0
|
|
303
|
+
listener?.onLoad(currentVideoId ?: "", duration, size.width, size.height)
|
|
304
|
+
}
|
|
305
|
+
listener?.onStatusChange(if (exo.isPlaying) "playing" else "paused")
|
|
306
|
+
}
|
|
307
|
+
Player.STATE_ENDED -> {
|
|
308
|
+
stopProgress()
|
|
309
|
+
emitProgress(exo)
|
|
310
|
+
listener?.onStatusChange("ended")
|
|
311
|
+
listener?.onEnd()
|
|
312
|
+
}
|
|
313
|
+
Player.STATE_IDLE -> listener?.onStatusChange("idle")
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
318
|
+
val exo = player ?: return
|
|
319
|
+
if (isPlaying) {
|
|
320
|
+
listener?.onStatusChange("playing")
|
|
321
|
+
startProgress()
|
|
322
|
+
} else {
|
|
323
|
+
stopProgress()
|
|
324
|
+
if (exo.playbackState == Player.STATE_READY) {
|
|
325
|
+
listener?.onStatusChange("paused")
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
override fun onPositionDiscontinuity(
|
|
331
|
+
oldPosition: Player.PositionInfo,
|
|
332
|
+
newPosition: Player.PositionInfo,
|
|
333
|
+
reason: Int,
|
|
334
|
+
) {
|
|
335
|
+
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
|
|
336
|
+
listener?.onSeek(newPosition.positionMs / 1000.0)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
override fun onPlayerError(error: PlaybackException) {
|
|
341
|
+
listener?.onError(error.errorCodeName, error.message ?: "Playback error")
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private fun startProgress() {
|
|
346
|
+
stopProgress()
|
|
347
|
+
mainHandler.postDelayed(progressRunnable, PROGRESS_INTERVAL_MS)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private fun stopProgress() {
|
|
351
|
+
mainHandler.removeCallbacks(progressRunnable)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private fun emitProgress(exo: ExoPlayer) {
|
|
355
|
+
val duration = if (exo.duration == C.TIME_UNSET) 0.0 else exo.duration / 1000.0
|
|
356
|
+
val position = exo.currentPosition / 1000.0
|
|
357
|
+
val buffered = ((exo.bufferedPosition - exo.currentPosition).coerceAtLeast(0L)) / 1000.0
|
|
358
|
+
listener?.onProgress(position, duration, buffered)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private fun requirePlayer(): ExoPlayer? {
|
|
362
|
+
val context = appContext
|
|
363
|
+
if (player == null && context != null) initialize(context)
|
|
364
|
+
return player
|
|
365
|
+
}
|
|
366
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
package com.auvideo
|
|
2
|
+
|
|
3
|
+
import java.lang.ref.WeakReference
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Maps surface ids to their mounted native views. Weak references only —
|
|
7
|
+
* an unmounted screen can never be leaked by the registry.
|
|
8
|
+
*/
|
|
9
|
+
object SurfaceRegistry {
|
|
10
|
+
private val views = HashMap<String, WeakReference<AuVideoSurfaceView>>()
|
|
11
|
+
|
|
12
|
+
fun register(surfaceId: String, view: AuVideoSurfaceView) {
|
|
13
|
+
views[surfaceId] = WeakReference(view)
|
|
14
|
+
PlayerCore.onSurfaceAvailable(surfaceId, view)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fun unregister(surfaceId: String, view: AuVideoSurfaceView) {
|
|
18
|
+
val registered = views[surfaceId]?.get()
|
|
19
|
+
if (registered == null || registered === view) {
|
|
20
|
+
views.remove(surfaceId)
|
|
21
|
+
PlayerCore.onSurfaceUnavailable(surfaceId, view)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fun get(surfaceId: String): AuVideoSurfaceView? = views[surfaceId]?.get()
|
|
26
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- TextureView so the player re-parents and animates cleanly (floating
|
|
3
|
+
window, feed cells); surface_type is only settable via attrs. -->
|
|
4
|
+
<androidx.media3.ui.PlayerView
|
|
5
|
+
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
6
|
+
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
7
|
+
android:layout_width="match_parent"
|
|
8
|
+
android:layout_height="match_parent"
|
|
9
|
+
app:surface_type="texture_view"
|
|
10
|
+
app:use_controller="false" />
|
package/ios/AuVideo.h
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#import <AuVideoSpec/AuVideoSpec.h>
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TurboModule glue. All real playback logic lives in the Swift core
|
|
5
|
+
* (AuVideoPlayerCore); this class marshals calls onto the main thread and
|
|
6
|
+
* forwards engine events to JS via the codegen emitters.
|
|
7
|
+
*/
|
|
8
|
+
@interface AuVideo : NativeAuVideoSpecBase <NativeAuVideoSpec>
|
|
9
|
+
|
|
10
|
+
@end
|
package/ios/AuVideo.mm
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#import "AuVideo.h"
|
|
2
|
+
|
|
3
|
+
#import <AVKit/AVKit.h>
|
|
4
|
+
|
|
5
|
+
#if __has_include(<AuVideo/AuVideo-Swift.h>)
|
|
6
|
+
#import <AuVideo/AuVideo-Swift.h>
|
|
7
|
+
#else
|
|
8
|
+
#import "AuVideo-Swift.h"
|
|
9
|
+
#endif
|
|
10
|
+
|
|
11
|
+
using JS::NativeAuVideo::NativeVideoSource;
|
|
12
|
+
|
|
13
|
+
static AuVideoSourceSpec *AuVideoParseSource(NativeVideoSource &source)
|
|
14
|
+
{
|
|
15
|
+
NSMutableDictionary<NSString *, NSString *> *headers = [NSMutableDictionary new];
|
|
16
|
+
if ([source.headers() isKindOfClass:[NSDictionary class]]) {
|
|
17
|
+
[(NSDictionary *)source.headers()
|
|
18
|
+
enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
|
|
19
|
+
if ([key isKindOfClass:[NSString class]] && [value isKindOfClass:[NSString class]]) {
|
|
20
|
+
headers[key] = value;
|
|
21
|
+
}
|
|
22
|
+
}];
|
|
23
|
+
}
|
|
24
|
+
return [[AuVideoSourceSpec alloc] initWithVideoId:source.id_()
|
|
25
|
+
uri:source.uri()
|
|
26
|
+
headers:headers
|
|
27
|
+
title:source.title()
|
|
28
|
+
artist:source.artist()
|
|
29
|
+
artworkUri:source.artworkUri()
|
|
30
|
+
startPosition:source.startPosition().value_or(0)];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@interface AuVideo () <AuVideoCoreDelegate>
|
|
34
|
+
@end
|
|
35
|
+
|
|
36
|
+
@implementation AuVideo
|
|
37
|
+
|
|
38
|
+
+ (NSString *)moduleName
|
|
39
|
+
{
|
|
40
|
+
return @"AuVideo";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
44
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
45
|
+
{
|
|
46
|
+
return std::make_shared<facebook::react::NativeAuVideoSpecJSI>(params);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#pragma mark - Lifecycle
|
|
50
|
+
|
|
51
|
+
- (void)nativeInit
|
|
52
|
+
{
|
|
53
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
54
|
+
AuVideoPlayerCore.shared.delegate = self;
|
|
55
|
+
[AuVideoPlayerCore.shared initialize];
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
- (void)releasePlayer
|
|
60
|
+
{
|
|
61
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
62
|
+
[AuVideoPlayerCore.shared releasePlayer];
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#pragma mark - Source
|
|
67
|
+
|
|
68
|
+
- (void)setSource:(NativeVideoSource &)source autoplay:(BOOL)autoplay
|
|
69
|
+
{
|
|
70
|
+
AuVideoSourceSpec *spec = AuVideoParseSource(source);
|
|
71
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
72
|
+
[AuVideoPlayerCore.shared setSource:spec autoplay:autoplay];
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
- (void)preload:(NativeVideoSource &)source
|
|
77
|
+
{
|
|
78
|
+
AuVideoSourceSpec *spec = AuVideoParseSource(source);
|
|
79
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
80
|
+
[AuVideoPlayerCore.shared preload:spec];
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#pragma mark - Commands
|
|
85
|
+
|
|
86
|
+
- (void)play
|
|
87
|
+
{
|
|
88
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [AuVideoPlayerCore.shared play]; });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
- (void)pause
|
|
92
|
+
{
|
|
93
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [AuVideoPlayerCore.shared pause]; });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
- (void)stop
|
|
97
|
+
{
|
|
98
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [AuVideoPlayerCore.shared stop]; });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
- (void)seekTo:(double)position
|
|
102
|
+
{
|
|
103
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [AuVideoPlayerCore.shared seekTo:position]; });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
- (void)setRate:(double)rate
|
|
107
|
+
{
|
|
108
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [AuVideoPlayerCore.shared setRate:rate]; });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
- (void)setVolume:(double)volume
|
|
112
|
+
{
|
|
113
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [AuVideoPlayerCore.shared setVolume:volume]; });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
- (void)setMuted:(BOOL)muted
|
|
117
|
+
{
|
|
118
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [AuVideoPlayerCore.shared setMuted:muted]; });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
- (void)setRepeat:(BOOL)repeat
|
|
122
|
+
{
|
|
123
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [AuVideoPlayerCore.shared setRepeat:repeat]; });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
- (void)setResizeMode:(NSString *)mode
|
|
127
|
+
{
|
|
128
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [AuVideoPlayerCore.shared setResizeMode:mode]; });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
- (void)getPosition:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
|
|
132
|
+
{
|
|
133
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
134
|
+
resolve(@([AuVideoPlayerCore.shared positionSeconds]));
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#pragma mark - Surfaces
|
|
139
|
+
|
|
140
|
+
- (void)attach:(NSString *)surfaceId
|
|
141
|
+
{
|
|
142
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [AuVideoPlayerCore.shared attach:surfaceId]; });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
- (void)detach
|
|
146
|
+
{
|
|
147
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [AuVideoPlayerCore.shared detach]; });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#pragma mark - Fullscreen
|
|
151
|
+
|
|
152
|
+
- (void)enterFullscreen
|
|
153
|
+
{
|
|
154
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [AuVideoOrientation setFullscreen:YES]; });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
- (void)exitFullscreen
|
|
158
|
+
{
|
|
159
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [AuVideoOrientation setFullscreen:NO]; });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#pragma mark - PiP
|
|
163
|
+
|
|
164
|
+
- (void)enterPip:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
|
|
165
|
+
{
|
|
166
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
167
|
+
resolve(@([AuVideoPlayerCore.shared enterPip]));
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
- (void)exitPip
|
|
172
|
+
{
|
|
173
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [AuVideoPlayerCore.shared exitPip]; });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
#pragma mark - AuVideoCoreDelegate
|
|
177
|
+
|
|
178
|
+
- (void)onStatusChange:(NSString *)status
|
|
179
|
+
{
|
|
180
|
+
[self emitOnStatusChange:@{@"status": status}];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
- (void)onLoad:(NSString *)videoId duration:(double)duration width:(double)width height:(double)height
|
|
184
|
+
{
|
|
185
|
+
[self emitOnLoad:@{
|
|
186
|
+
@"videoId": videoId,
|
|
187
|
+
@"duration": @(duration),
|
|
188
|
+
@"width": @(width),
|
|
189
|
+
@"height": @(height),
|
|
190
|
+
}];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
- (void)onProgress:(double)position duration:(double)duration buffered:(double)buffered
|
|
194
|
+
{
|
|
195
|
+
[self emitOnProgress:@{
|
|
196
|
+
@"position": @(position),
|
|
197
|
+
@"duration": @(duration),
|
|
198
|
+
@"buffered": @(buffered),
|
|
199
|
+
}];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
- (void)onSeek:(double)position
|
|
203
|
+
{
|
|
204
|
+
[self emitOnSeek:@{@"position": @(position)}];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
- (void)onEnd
|
|
208
|
+
{
|
|
209
|
+
[self emitOnEnd];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
- (void)onError:(NSString *)code message:(NSString *)message
|
|
213
|
+
{
|
|
214
|
+
[self emitOnError:@{@"code": code, @"message": message}];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
- (void)onAttach:(NSString *)surfaceId
|
|
218
|
+
{
|
|
219
|
+
[self emitOnAttach:@{@"surfaceId": surfaceId}];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
- (void)onDetach:(NSString *)surfaceId
|
|
223
|
+
{
|
|
224
|
+
[self emitOnDetach:@{@"surfaceId": surfaceId}];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
- (void)onPipChange:(BOOL)active
|
|
228
|
+
{
|
|
229
|
+
[self emitOnPipChange:@{@"active": @(active)}];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/// Fullscreen orientation coordination.
|
|
5
|
+
///
|
|
6
|
+
/// iOS decides allowed orientations by asking the AppDelegate. For rotation
|
|
7
|
+
/// to unlock only while the player is fullscreen, the host app forwards that
|
|
8
|
+
/// question here (see README):
|
|
9
|
+
///
|
|
10
|
+
/// ```swift
|
|
11
|
+
/// func application(_ application: UIApplication,
|
|
12
|
+
/// supportedInterfaceOrientationsFor window: UIWindow?)
|
|
13
|
+
/// -> UIInterfaceOrientationMask {
|
|
14
|
+
/// return AuVideoOrientation.mask(withDefault: .portrait)
|
|
15
|
+
/// }
|
|
16
|
+
/// ```
|
|
17
|
+
@objc(AuVideoOrientation)
|
|
18
|
+
public final class AuVideoOrientation: NSObject {
|
|
19
|
+
|
|
20
|
+
@objc public private(set) static var isFullscreenActive = false
|
|
21
|
+
|
|
22
|
+
/// Orientations allowed while fullscreen. Default: everything but upside-down.
|
|
23
|
+
@objc public static var fullscreenMask: UIInterfaceOrientationMask = .allButUpsideDown
|
|
24
|
+
|
|
25
|
+
/// Call from AppDelegate's supportedInterfaceOrientationsFor.
|
|
26
|
+
@objc(maskWithDefault:)
|
|
27
|
+
public static func mask(
|
|
28
|
+
withDefault defaultMask: UIInterfaceOrientationMask
|
|
29
|
+
) -> UIInterfaceOrientationMask {
|
|
30
|
+
return isFullscreenActive ? fullscreenMask : defaultMask
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@objc public static func setFullscreen(_ active: Bool) {
|
|
34
|
+
guard isFullscreenActive != active else { return }
|
|
35
|
+
isFullscreenActive = active
|
|
36
|
+
refresh()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Makes the system re-query supported orientations; when exiting
|
|
40
|
+
/// fullscreen in a portrait-locked app this also rotates back.
|
|
41
|
+
private static func refresh() {
|
|
42
|
+
DispatchQueue.main.async {
|
|
43
|
+
if #available(iOS 16.0, *) {
|
|
44
|
+
for case let scene as UIWindowScene in UIApplication.shared.connectedScenes {
|
|
45
|
+
scene.keyWindow?.rootViewController?
|
|
46
|
+
.setNeedsUpdateOfSupportedInterfaceOrientations()
|
|
47
|
+
if !isFullscreenActive {
|
|
48
|
+
scene.requestGeometryUpdate(.iOS(interfaceOrientations: .portrait), errorHandler: { _ in })
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
if !isFullscreenActive {
|
|
53
|
+
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
|
|
54
|
+
}
|
|
55
|
+
UIViewController.attemptRotationToDeviceOrientation()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|