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.
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/RNVideoFeed.podspec +25 -0
- package/android/build.gradle +43 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/rnvideofeed/FeedPlayer.kt +202 -0
- package/android/src/main/java/com/rnvideofeed/VideoData.kt +8 -0
- package/android/src/main/java/com/rnvideofeed/VideoFeedCell.kt +185 -0
- package/android/src/main/java/com/rnvideofeed/VideoFeedModule.kt +176 -0
- package/android/src/main/java/com/rnvideofeed/VideoFeedPackage.kt +17 -0
- package/android/src/main/java/com/rnvideofeed/VideoFeedPlayerPool.kt +78 -0
- package/android/src/main/java/com/rnvideofeed/VideoFeedView.kt +706 -0
- package/android/src/main/java/com/rnvideofeed/VideoFeedViewManager.kt +129 -0
- package/android/src/main/res/xml/player_view_texture.xml +5 -0
- package/index.d.ts +30 -0
- package/ios/FeedOptions/ContentCardInfoView.swift +312 -0
- package/ios/FeedPlayer.swift +174 -0
- package/ios/VideoFeedCell.swift +117 -0
- package/ios/VideoFeedEventEmitter.swift +24 -0
- package/ios/VideoFeedManagerBridge.h +17 -0
- package/ios/VideoFeedManagerBridge.m +31 -0
- package/ios/VideoFeedView.swift +432 -0
- package/ios/VideoFeedViewManager.swift +131 -0
- package/package.json +37 -0
- package/react-native.config.js +3 -0
- package/src/VideoFeedManagerBridge.ts +24 -0
- package/src/VideoFeedView.native.tsx +17 -0
- package/src/index.ts +3 -0
|
@@ -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
|
+
}
|