rn-videofeed 0.1.0

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.
@@ -0,0 +1,176 @@
1
+ package com.rnvideofeed
2
+
3
+ import android.util.Log
4
+ import com.facebook.react.bridge.ReactApplicationContext
5
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
6
+ import com.facebook.react.bridge.ReactMethod
7
+ import com.facebook.react.bridge.ReadableArray
8
+ import com.facebook.react.bridge.WritableNativeMap
9
+ import com.facebook.react.modules.core.DeviceEventManagerModule
10
+ import com.facebook.react.uimanager.UIManagerHelper
11
+ import com.facebook.react.bridge.UiThreadUtil
12
+
13
+ class VideoFeedModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
14
+
15
+ private val TAG = "VideoFeedModule"
16
+
17
+ override fun getName(): String {
18
+ return "VideoFeedViewManager"
19
+ }
20
+
21
+ /**
22
+ * Safely resolves a VideoFeedView by reactTag, handling cases where the view no longer exists
23
+ * @param reactTag The React tag of the view to resolve
24
+ * @return The VideoFeedView if found and valid, null otherwise
25
+ */
26
+ private fun safeResolveVideoFeedView(reactTag: Int): VideoFeedView? {
27
+ return try {
28
+ val uiManager = UIManagerHelper.getUIManagerForReactTag(reactContext, reactTag)
29
+ uiManager?.resolveView(reactTag) as? VideoFeedView
30
+ } catch (e: Exception) {
31
+ Log.e(TAG, "Error resolving view for tag $reactTag: ${e.message}")
32
+ null
33
+ }
34
+ }
35
+
36
+ @ReactMethod
37
+ fun setVideos(reactTag: Double, videos: ReadableArray) {
38
+ Log.d(TAG, "=== setVideos called for tag: $reactTag, videos count: ${videos.size()} ===")
39
+ UiThreadUtil.runOnUiThread {
40
+ val view = safeResolveVideoFeedView(reactTag.toInt())
41
+ if (view != null) {
42
+ Log.d(TAG, "Found VideoFeedView, setting videos")
43
+ val viewManager = VideoFeedViewManager()
44
+ viewManager.setVideos(view, videos)
45
+ } else {
46
+ Log.e(TAG, "Failed to find VideoFeedView for tag: $reactTag")
47
+ }
48
+ }
49
+ }
50
+
51
+ @ReactMethod
52
+ fun appendVideos(reactTag: Double, videos: ReadableArray) {
53
+ Log.d(TAG, "Appending videos for tag: $reactTag")
54
+ UiThreadUtil.runOnUiThread {
55
+ val view = safeResolveVideoFeedView(reactTag.toInt())
56
+ if (view != null) {
57
+ Log.d(TAG, "Found view, appending videos")
58
+ val viewManager = VideoFeedViewManager()
59
+ viewManager.appendVideos(view, videos)
60
+ } else {
61
+ Log.d(TAG, "Failed to find view for tag: $reactTag")
62
+ }
63
+ }
64
+ }
65
+
66
+ @ReactMethod
67
+ fun setFeedActive(reactTag: Double, isActive: Boolean) {
68
+ Log.d(TAG, "setFeedActive for tag: $reactTag, isActive: $isActive")
69
+ UiThreadUtil.runOnUiThread {
70
+ val view = safeResolveVideoFeedView(reactTag.toInt())
71
+ if (view != null) {
72
+ Log.d(TAG, "Found view, setting feed active: $isActive")
73
+ val viewManager = VideoFeedViewManager()
74
+ viewManager.setFeedActive(view, isActive)
75
+ } else {
76
+ Log.d(TAG, "setFeedActive: View not found for tag $reactTag")
77
+ }
78
+ }
79
+ }
80
+
81
+ @ReactMethod
82
+ fun addListener(eventName: String) {
83
+ // Keep: Required for RN built in Event Emitter Calls.
84
+ Log.d(TAG, "addListener: $eventName")
85
+ }
86
+
87
+ @ReactMethod
88
+ fun pauseVideo(reactTag: Double) {
89
+ Log.d(TAG, "=== pauseVideo called for tag: $reactTag ===")
90
+ UiThreadUtil.runOnUiThread {
91
+ val view = safeResolveVideoFeedView(reactTag.toInt())
92
+ if (view != null) {
93
+ Log.d(TAG, "Found VideoFeedView, pausing video")
94
+ view.pauseCurrentVideo()
95
+ } else {
96
+ Log.e(TAG, "Failed to find VideoFeedView for tag: $reactTag")
97
+ }
98
+ }
99
+ }
100
+
101
+ @ReactMethod
102
+ fun playVideo(reactTag: Double) {
103
+ Log.d(TAG, "=== playVideo called for tag: $reactTag ===")
104
+ UiThreadUtil.runOnUiThread {
105
+ val view = safeResolveVideoFeedView(reactTag.toInt())
106
+ if (view != null) {
107
+ Log.d(TAG, "Found VideoFeedView, playing video")
108
+ view.playCurrentVideo()
109
+ } else {
110
+ Log.e(TAG, "Failed to find VideoFeedView for tag: $reactTag")
111
+ }
112
+ }
113
+ }
114
+
115
+ @ReactMethod
116
+ fun togglePlayPause(reactTag: Double) {
117
+ Log.d(TAG, "=== togglePlayPause called for tag: $reactTag ===")
118
+ UiThreadUtil.runOnUiThread {
119
+ val view = safeResolveVideoFeedView(reactTag.toInt())
120
+ if (view != null) {
121
+ Log.d(TAG, "Found VideoFeedView, toggling play/pause")
122
+ val isNowPlaying = view.togglePlayPause()
123
+
124
+ // Emit event back to React Native
125
+ val event = WritableNativeMap().apply {
126
+ putBoolean("isPlaying", isNowPlaying)
127
+ }
128
+ reactContext
129
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
130
+ .emit("onPlayStateChanged", event)
131
+ } else {
132
+ Log.e(TAG, "Failed to find VideoFeedView for tag: $reactTag")
133
+ }
134
+ }
135
+ }
136
+
137
+ @ReactMethod
138
+ fun isVideoPlaying(reactTag: Double) {
139
+ Log.d(TAG, "=== isVideoPlaying called for tag: $reactTag ===")
140
+ UiThreadUtil.runOnUiThread {
141
+ val view = safeResolveVideoFeedView(reactTag.toInt())
142
+ if (view != null) {
143
+ Log.d(TAG, "Found VideoFeedView, checking playing state")
144
+ val isPlaying = view.isCurrentVideoPlaying()
145
+
146
+ // Emit event back to React Native
147
+ val event = WritableNativeMap().apply {
148
+ putBoolean("isPlaying", isPlaying)
149
+ }
150
+ reactContext
151
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
152
+ .emit("onPlayStateChecked", event)
153
+ } else {
154
+ Log.e(TAG, "Failed to find VideoFeedView for tag: $reactTag")
155
+ }
156
+ }
157
+ }
158
+
159
+ @ReactMethod
160
+ fun removeListeners(count: Double) {
161
+ // Keep: Required for RN built in Event Emitter Calls.
162
+ Log.d(TAG, "removeListeners: $count")
163
+ }
164
+
165
+ @ReactMethod
166
+ fun addEventListener(eventName: String) {
167
+ // Legacy support
168
+ Log.d(TAG, "addEventListener: $eventName")
169
+ }
170
+
171
+ @ReactMethod
172
+ fun removeEventListener(eventName: String) {
173
+ // Legacy support
174
+ Log.d(TAG, "removeEventListener: $eventName")
175
+ }
176
+ }
@@ -0,0 +1,17 @@
1
+ package com.rnvideofeed
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ class VideoFeedPackage : ReactPackage {
9
+
10
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
11
+ return listOf(VideoFeedModule(reactContext))
12
+ }
13
+
14
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
15
+ return listOf(VideoFeedViewManager())
16
+ }
17
+ }
@@ -0,0 +1,78 @@
1
+ package com.rnvideofeed
2
+
3
+ import android.content.Context
4
+ import android.net.Uri
5
+ import android.util.Log
6
+ import androidx.media3.common.MediaItem
7
+ import androidx.media3.common.Player
8
+ import androidx.media3.exoplayer.ExoPlayer
9
+
10
+ /**
11
+ * Keeps a small LRU pool of ExoPlayer instances keyed by video id so scrolling
12
+ * back resumes playback where the user left off (Reels-style behaviour).
13
+ */
14
+ class VideoFeedPlayerPool(
15
+ private val context: Context,
16
+ private val maxSize: Int = 8
17
+ ) {
18
+ private val tag = "VideoFeedPlayerPool"
19
+
20
+ private data class PooledEntry(
21
+ val player: ExoPlayer,
22
+ var videoUrl: String,
23
+ var lastAccessTime: Long = System.currentTimeMillis()
24
+ )
25
+
26
+ private val entries = LinkedHashMap<String, PooledEntry>(maxSize, 0.75f, true)
27
+
28
+ fun acquire(videoId: String, videoUrl: String): ExoPlayer {
29
+ entries[videoId]?.let { entry ->
30
+ if (entry.videoUrl == videoUrl) {
31
+ entry.lastAccessTime = System.currentTimeMillis()
32
+ Log.d(tag, "Reusing pooled player for $videoId at ${entry.player.currentPosition}ms")
33
+ return entry.player
34
+ }
35
+ removeEntry(videoId)
36
+ }
37
+
38
+ while (entries.size >= maxSize) {
39
+ val oldestKey = entries.keys.first()
40
+ Log.d(tag, "Evicting oldest player: $oldestKey")
41
+ removeEntry(oldestKey)
42
+ }
43
+
44
+ val player = ExoPlayer.Builder(context).build().apply {
45
+ repeatMode = Player.REPEAT_MODE_ONE
46
+ volume = 1.0f
47
+ }
48
+ player.setMediaItem(MediaItem.fromUri(Uri.parse(videoUrl)))
49
+ player.prepare()
50
+
51
+ entries[videoId] = PooledEntry(player, videoUrl)
52
+ Log.d(tag, "Created new pooled player for $videoId (pool size=${entries.size})")
53
+ return player
54
+ }
55
+
56
+ fun pause(videoId: String) {
57
+ entries[videoId]?.player?.pause()
58
+ }
59
+
60
+ fun hasPlayer(videoId: String, videoUrl: String): Boolean {
61
+ val entry = entries[videoId] ?: return false
62
+ return entry.videoUrl == videoUrl
63
+ }
64
+
65
+ fun getPlaybackPosition(videoId: String): Long {
66
+ return entries[videoId]?.player?.currentPosition ?: 0L
67
+ }
68
+
69
+ fun releaseAll() {
70
+ Log.d(tag, "Releasing all ${entries.size} pooled players")
71
+ entries.values.forEach { it.player.release() }
72
+ entries.clear()
73
+ }
74
+
75
+ private fun removeEntry(videoId: String) {
76
+ entries.remove(videoId)?.player?.release()
77
+ }
78
+ }