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.
Files changed (141) hide show
  1. package/AuVideo.podspec +28 -0
  2. package/LICENSE +20 -0
  3. package/README.md +183 -0
  4. package/android/build.gradle +59 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/com/auvideo/AuVideoModule.kt +259 -0
  7. package/android/src/main/java/com/auvideo/AuVideoPackage.kt +35 -0
  8. package/android/src/main/java/com/auvideo/AuVideoSurfaceView.kt +40 -0
  9. package/android/src/main/java/com/auvideo/AuVideoSurfaceViewManager.kt +38 -0
  10. package/android/src/main/java/com/auvideo/PlayerCore.kt +366 -0
  11. package/android/src/main/java/com/auvideo/SurfaceRegistry.kt +26 -0
  12. package/android/src/main/res/layout/au_video_player_view.xml +10 -0
  13. package/ios/AuVideo.h +10 -0
  14. package/ios/AuVideo.mm +232 -0
  15. package/ios/AuVideoOrientation.swift +59 -0
  16. package/ios/AuVideoPlayerCore.swift +415 -0
  17. package/ios/AuVideoSurfaceRegistry.swift +30 -0
  18. package/ios/AuVideoSurfaceView.h +15 -0
  19. package/ios/AuVideoSurfaceView.mm +63 -0
  20. package/lib/module/AuVideoSurfaceNativeComponent.ts +8 -0
  21. package/lib/module/NativeAuVideo.js +5 -0
  22. package/lib/module/NativeAuVideo.js.map +1 -0
  23. package/lib/module/components/FloatingPlayer.js +119 -0
  24. package/lib/module/components/FloatingPlayer.js.map +1 -0
  25. package/lib/module/components/FullscreenPlayer.js +59 -0
  26. package/lib/module/components/FullscreenPlayer.js.map +1 -0
  27. package/lib/module/components/GestureOverlay.js +56 -0
  28. package/lib/module/components/GestureOverlay.js.map +1 -0
  29. package/lib/module/components/MiniPlayer.js +89 -0
  30. package/lib/module/components/MiniPlayer.js.map +1 -0
  31. package/lib/module/components/VideoControls.js +191 -0
  32. package/lib/module/components/VideoControls.js.map +1 -0
  33. package/lib/module/components/VideoPlayer.js +65 -0
  34. package/lib/module/components/VideoPlayer.js.map +1 -0
  35. package/lib/module/components/VideoSurface.js +35 -0
  36. package/lib/module/components/VideoSurface.js.map +1 -0
  37. package/lib/module/core/VideoManager.js +436 -0
  38. package/lib/module/core/VideoManager.js.map +1 -0
  39. package/lib/module/hooks/useFullscreen.js +19 -0
  40. package/lib/module/hooks/useFullscreen.js.map +1 -0
  41. package/lib/module/hooks/usePiP.js +17 -0
  42. package/lib/module/hooks/usePiP.js.map +1 -0
  43. package/lib/module/hooks/usePlayback.js +21 -0
  44. package/lib/module/hooks/usePlayback.js.map +1 -0
  45. package/lib/module/hooks/useVideo.js +17 -0
  46. package/lib/module/hooks/useVideo.js.map +1 -0
  47. package/lib/module/hooks/useVideoEvents.js +26 -0
  48. package/lib/module/hooks/useVideoEvents.js.map +1 -0
  49. package/lib/module/index.js +19 -0
  50. package/lib/module/index.js.map +1 -0
  51. package/lib/module/package.json +1 -0
  52. package/lib/module/provider/VideoContext.js +14 -0
  53. package/lib/module/provider/VideoContext.js.map +1 -0
  54. package/lib/module/provider/VideoProvider.js +36 -0
  55. package/lib/module/provider/VideoProvider.js.map +1 -0
  56. package/lib/module/state/createVideoStore.js +33 -0
  57. package/lib/module/state/createVideoStore.js.map +1 -0
  58. package/lib/module/types/events.js +4 -0
  59. package/lib/module/types/events.js.map +1 -0
  60. package/lib/module/types/index.js +2 -0
  61. package/lib/module/types/index.js.map +1 -0
  62. package/lib/module/types/video.js +2 -0
  63. package/lib/module/types/video.js.map +1 -0
  64. package/lib/module/utils/Emitter.js +39 -0
  65. package/lib/module/utils/Emitter.js.map +1 -0
  66. package/lib/module/utils/formatTime.js +15 -0
  67. package/lib/module/utils/formatTime.js.map +1 -0
  68. package/lib/typescript/package.json +1 -0
  69. package/lib/typescript/src/AuVideoSurfaceNativeComponent.d.ts +8 -0
  70. package/lib/typescript/src/AuVideoSurfaceNativeComponent.d.ts.map +1 -0
  71. package/lib/typescript/src/NativeAuVideo.d.ts +85 -0
  72. package/lib/typescript/src/NativeAuVideo.d.ts.map +1 -0
  73. package/lib/typescript/src/components/FloatingPlayer.d.ts +12 -0
  74. package/lib/typescript/src/components/FloatingPlayer.d.ts.map +1 -0
  75. package/lib/typescript/src/components/FullscreenPlayer.d.ts +12 -0
  76. package/lib/typescript/src/components/FullscreenPlayer.d.ts.map +1 -0
  77. package/lib/typescript/src/components/GestureOverlay.d.ts +14 -0
  78. package/lib/typescript/src/components/GestureOverlay.d.ts.map +1 -0
  79. package/lib/typescript/src/components/MiniPlayer.d.ts +16 -0
  80. package/lib/typescript/src/components/MiniPlayer.d.ts.map +1 -0
  81. package/lib/typescript/src/components/VideoControls.d.ts +17 -0
  82. package/lib/typescript/src/components/VideoControls.d.ts.map +1 -0
  83. package/lib/typescript/src/components/VideoPlayer.d.ts +27 -0
  84. package/lib/typescript/src/components/VideoPlayer.d.ts.map +1 -0
  85. package/lib/typescript/src/components/VideoSurface.d.ts +18 -0
  86. package/lib/typescript/src/components/VideoSurface.d.ts.map +1 -0
  87. package/lib/typescript/src/core/VideoManager.d.ts +88 -0
  88. package/lib/typescript/src/core/VideoManager.d.ts.map +1 -0
  89. package/lib/typescript/src/hooks/useFullscreen.d.ts +7 -0
  90. package/lib/typescript/src/hooks/useFullscreen.d.ts.map +1 -0
  91. package/lib/typescript/src/hooks/usePiP.d.ts +6 -0
  92. package/lib/typescript/src/hooks/usePiP.d.ts.map +1 -0
  93. package/lib/typescript/src/hooks/usePlayback.d.ts +13 -0
  94. package/lib/typescript/src/hooks/usePlayback.d.ts.map +1 -0
  95. package/lib/typescript/src/hooks/useVideo.d.ts +13 -0
  96. package/lib/typescript/src/hooks/useVideo.d.ts.map +1 -0
  97. package/lib/typescript/src/hooks/useVideoEvents.d.ts +14 -0
  98. package/lib/typescript/src/hooks/useVideoEvents.d.ts.map +1 -0
  99. package/lib/typescript/src/index.d.ts +18 -0
  100. package/lib/typescript/src/index.d.ts.map +1 -0
  101. package/lib/typescript/src/provider/VideoContext.d.ts +8 -0
  102. package/lib/typescript/src/provider/VideoContext.d.ts.map +1 -0
  103. package/lib/typescript/src/provider/VideoProvider.d.ts +18 -0
  104. package/lib/typescript/src/provider/VideoProvider.d.ts.map +1 -0
  105. package/lib/typescript/src/state/createVideoStore.d.ts +6 -0
  106. package/lib/typescript/src/state/createVideoStore.d.ts.map +1 -0
  107. package/lib/typescript/src/types/events.d.ts +46 -0
  108. package/lib/typescript/src/types/events.d.ts.map +1 -0
  109. package/lib/typescript/src/types/index.d.ts +3 -0
  110. package/lib/typescript/src/types/index.d.ts.map +1 -0
  111. package/lib/typescript/src/types/video.d.ts +70 -0
  112. package/lib/typescript/src/types/video.d.ts.map +1 -0
  113. package/lib/typescript/src/utils/Emitter.d.ts +15 -0
  114. package/lib/typescript/src/utils/Emitter.d.ts.map +1 -0
  115. package/lib/typescript/src/utils/formatTime.d.ts +2 -0
  116. package/lib/typescript/src/utils/formatTime.d.ts.map +1 -0
  117. package/package.json +154 -0
  118. package/src/AuVideoSurfaceNativeComponent.ts +8 -0
  119. package/src/NativeAuVideo.ts +106 -0
  120. package/src/components/FloatingPlayer.tsx +130 -0
  121. package/src/components/FullscreenPlayer.tsx +64 -0
  122. package/src/components/GestureOverlay.tsx +76 -0
  123. package/src/components/MiniPlayer.tsx +93 -0
  124. package/src/components/VideoControls.tsx +213 -0
  125. package/src/components/VideoPlayer.tsx +77 -0
  126. package/src/components/VideoSurface.tsx +39 -0
  127. package/src/core/VideoManager.ts +441 -0
  128. package/src/hooks/useFullscreen.ts +14 -0
  129. package/src/hooks/usePiP.ts +13 -0
  130. package/src/hooks/usePlayback.ts +26 -0
  131. package/src/hooks/useVideo.ts +16 -0
  132. package/src/hooks/useVideoEvents.ts +33 -0
  133. package/src/index.ts +54 -0
  134. package/src/provider/VideoContext.ts +12 -0
  135. package/src/provider/VideoProvider.tsx +41 -0
  136. package/src/state/createVideoStore.ts +33 -0
  137. package/src/types/events.ts +22 -0
  138. package/src/types/index.ts +11 -0
  139. package/src/types/video.ts +85 -0
  140. package/src/utils/Emitter.ts +51 -0
  141. 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
+ }