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,117 @@
|
|
|
1
|
+
//
|
|
2
|
+
// VideoFeedCell.swift
|
|
3
|
+
// App
|
|
4
|
+
//
|
|
5
|
+
// Created by Venkatesh Mandapati on 15/05/2025.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import UIKit
|
|
9
|
+
import SDWebImage
|
|
10
|
+
|
|
11
|
+
class VideoFeedCell: UICollectionViewCell {
|
|
12
|
+
|
|
13
|
+
let feedPlayer = FeedPlayer()
|
|
14
|
+
|
|
15
|
+
private let thumbnailImageView: UIImageView = {
|
|
16
|
+
let iv = UIImageView()
|
|
17
|
+
iv.contentMode = .scaleAspectFit
|
|
18
|
+
iv.clipsToBounds = true
|
|
19
|
+
return iv
|
|
20
|
+
}()
|
|
21
|
+
|
|
22
|
+
override init(frame: CGRect) {
|
|
23
|
+
super.init(frame: frame)
|
|
24
|
+
contentView.backgroundColor = .black
|
|
25
|
+
|
|
26
|
+
contentView.addSubview(thumbnailImageView)
|
|
27
|
+
thumbnailImageView.translatesAutoresizingMaskIntoConstraints = false
|
|
28
|
+
NSLayoutConstraint.activate([
|
|
29
|
+
thumbnailImageView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
|
30
|
+
thumbnailImageView.bottomAnchor.constraint(
|
|
31
|
+
equalTo: contentView.bottomAnchor),
|
|
32
|
+
thumbnailImageView.leadingAnchor.constraint(
|
|
33
|
+
equalTo: contentView.leadingAnchor),
|
|
34
|
+
thumbnailImageView.trailingAnchor.constraint(
|
|
35
|
+
equalTo: contentView.trailingAnchor),
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
contentView.addSubview(feedPlayer)
|
|
39
|
+
feedPlayer.translatesAutoresizingMaskIntoConstraints = false
|
|
40
|
+
NSLayoutConstraint.activate([
|
|
41
|
+
feedPlayer.topAnchor.constraint(equalTo: contentView.topAnchor),
|
|
42
|
+
feedPlayer.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
|
43
|
+
feedPlayer.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
|
44
|
+
feedPlayer.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
|
45
|
+
])
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
required init?(coder: NSCoder) {
|
|
49
|
+
fatalError("init(coder:) has not been implemented")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func configure(with video: VideoData) {
|
|
53
|
+
// Show and start animation while loading
|
|
54
|
+
|
|
55
|
+
thumbnailImageView.sd_setImage(
|
|
56
|
+
with: URL(string: video.thumbnailUrl ?? ""),
|
|
57
|
+
placeholderImage: nil,
|
|
58
|
+
options: [],
|
|
59
|
+
completed: nil
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
feedPlayer.videoUrl = video.videoUrl as NSString
|
|
63
|
+
feedPlayer.id = video.id as NSString
|
|
64
|
+
feedPlayer.isVisible = false
|
|
65
|
+
|
|
66
|
+
// Set callback to hide thumbnail when video actually starts playing
|
|
67
|
+
feedPlayer.onVideoStartedPlaying = { [weak self] in
|
|
68
|
+
self?.showVideoPlaying()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
thumbnailImageView.isHidden = false
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
func showVideoPlaying() {
|
|
75
|
+
// Hide thumbnail when video is visible
|
|
76
|
+
thumbnailImageView.isHidden = true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
override func prepareForReuse() {
|
|
80
|
+
super.prepareForReuse()
|
|
81
|
+
|
|
82
|
+
feedPlayer.reset()
|
|
83
|
+
feedPlayer.isVisible = false
|
|
84
|
+
feedPlayer.onVideoStartedPlaying = nil // Clear callback
|
|
85
|
+
|
|
86
|
+
thumbnailImageView.image = nil
|
|
87
|
+
thumbnailImageView.isHidden = false
|
|
88
|
+
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
// Implement delegate methods to handle taps and communicate with RN side or native controller
|
|
93
|
+
// func didTapOptions() {
|
|
94
|
+
// // Show action sheet for delete/save video here
|
|
95
|
+
// }
|
|
96
|
+
//
|
|
97
|
+
// func didTapMuteToggle() {
|
|
98
|
+
// // Mute/unmute toggle logic here
|
|
99
|
+
// }
|
|
100
|
+
//
|
|
101
|
+
// func didTapFlag() {
|
|
102
|
+
// // Show report bottom sheet logic here
|
|
103
|
+
// }
|
|
104
|
+
//
|
|
105
|
+
// func didTapViewCount() {
|
|
106
|
+
// // Show view count bottom sheet
|
|
107
|
+
// }
|
|
108
|
+
//
|
|
109
|
+
// func didTapTradingVolume() {
|
|
110
|
+
// // Show trading volume bottom sheet
|
|
111
|
+
// }
|
|
112
|
+
//
|
|
113
|
+
// func didTapShare() {
|
|
114
|
+
// // Trigger share logic
|
|
115
|
+
// }
|
|
116
|
+
|
|
117
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//
|
|
2
|
+
// VideoFeedEventEmitter.swift
|
|
3
|
+
// App
|
|
4
|
+
//
|
|
5
|
+
// Created by Venkatesh Mandapati on 15/05/2025.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
import React
|
|
10
|
+
|
|
11
|
+
@objc(VideoFeedEventEmitter)
|
|
12
|
+
class VideoFeedEventEmitter: RCTEventEmitter {
|
|
13
|
+
override func supportedEvents() -> [String]! {
|
|
14
|
+
return ["onEndReached", "onVideoChange", "onVideoTapped", "onPlayStateChanged"]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
override static func requiresMainQueueSetup() -> Bool {
|
|
18
|
+
return true
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@objc func sendEvent(_ event: String, body: Any?) {
|
|
22
|
+
sendEvent(withName: event, body: body)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//
|
|
2
|
+
// VideoFeedManagerBridge.h
|
|
3
|
+
// App
|
|
4
|
+
//
|
|
5
|
+
// Created by Venkatesh Mandapati on 24/07/2025.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
#ifndef VideoFeedManagerBridge_h
|
|
9
|
+
#define VideoFeedManagerBridge_h
|
|
10
|
+
|
|
11
|
+
#import <React/RCTViewManager.h>
|
|
12
|
+
#import <React/RCTBridgeModule.h>
|
|
13
|
+
|
|
14
|
+
@interface VideoFeedViewManager : RCTViewManager
|
|
15
|
+
@end
|
|
16
|
+
|
|
17
|
+
#endif /* VideoFeedManagerBridge_h */
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//
|
|
2
|
+
// VideoFeedManagerBridge.m
|
|
3
|
+
// App
|
|
4
|
+
//
|
|
5
|
+
// Created by Venkatesh Mandapati on 16/05/2025.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
#import <React/RCTViewManager.h>
|
|
9
|
+
#import <React/RCTBridgeModule.h>
|
|
10
|
+
#import <React/RCTUIManager.h>
|
|
11
|
+
#import <React/RCTEventEmitter.h>
|
|
12
|
+
|
|
13
|
+
@interface RCT_EXTERN_MODULE(VideoFeedViewManager, RCTViewManager)
|
|
14
|
+
|
|
15
|
+
RCT_EXTERN_METHOD(setVideos:(nonnull NSNumber *)reactTag videos:(NSArray *)videos)
|
|
16
|
+
RCT_EXTERN_METHOD(appendVideos:(nonnull NSNumber *)reactTag videos:(NSArray *)videos)
|
|
17
|
+
RCT_EXTERN_METHOD(setFeedActive:(nonnull NSNumber *)reactTag isActive:(BOOL)active)
|
|
18
|
+
|
|
19
|
+
RCT_EXTERN_METHOD(pauseVideo:(nonnull NSNumber *)reactTag)
|
|
20
|
+
RCT_EXTERN_METHOD(playVideo:(nonnull NSNumber *)reactTag)
|
|
21
|
+
RCT_EXTERN_METHOD(togglePlayPause:(nonnull NSNumber *)reactTag)
|
|
22
|
+
RCT_EXTERN_METHOD(isVideoPlaying:(nonnull NSNumber *)reactTag)
|
|
23
|
+
|
|
24
|
+
RCT_EXTERN_METHOD(addEventListener:(NSString *)eventName)
|
|
25
|
+
RCT_EXTERN_METHOD(removeEventListener:(NSString *)eventName)
|
|
26
|
+
|
|
27
|
+
@end
|
|
28
|
+
|
|
29
|
+
// Event Emitter for video feed events
|
|
30
|
+
@interface RCT_EXTERN_MODULE(VideoFeedEventEmitter, RCTEventEmitter)
|
|
31
|
+
@end
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
//
|
|
2
|
+
// VideoFeedView.swift
|
|
3
|
+
// App
|
|
4
|
+
//
|
|
5
|
+
// Created by Venkatesh Mandapati on 15/05/2025.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import AVFoundation
|
|
9
|
+
import UIKit
|
|
10
|
+
|
|
11
|
+
class VideoFeedView: UIView {
|
|
12
|
+
private var collectionView: UICollectionView!
|
|
13
|
+
private var videos: [VideoData] = []
|
|
14
|
+
private var currentPlayer: AVPlayer?
|
|
15
|
+
private var preloadTasks: [String: AVAsset] = [:]
|
|
16
|
+
private var feedIsActive = true
|
|
17
|
+
private var shouldPlayFirstVideo = false
|
|
18
|
+
private var isManuallyPaused = false // Track if user manually paused
|
|
19
|
+
weak var eventEmitter: VideoFeedEventEmitter?
|
|
20
|
+
|
|
21
|
+
var onEndReached: (() -> Void)?
|
|
22
|
+
var onVideoChange: ((String) -> Void)?
|
|
23
|
+
|
|
24
|
+
override init(frame: CGRect) {
|
|
25
|
+
super.init(frame: frame)
|
|
26
|
+
setupAppLifecycleObservers()
|
|
27
|
+
setupCollectionView()
|
|
28
|
+
setupTapGesture()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//Not required, only for storyboard or xib included
|
|
32
|
+
required init?(coder: NSCoder) {
|
|
33
|
+
fatalError("init(coder:) has not been implemented")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private func setupAppLifecycleObservers() {
|
|
37
|
+
NotificationCenter.default.addObserver(
|
|
38
|
+
self,
|
|
39
|
+
selector: #selector(appDidEnterBackground),
|
|
40
|
+
name: UIApplication.didEnterBackgroundNotification,
|
|
41
|
+
object: nil
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
NotificationCenter.default.addObserver(
|
|
45
|
+
self,
|
|
46
|
+
selector: #selector(appDidBecomeActive),
|
|
47
|
+
name: UIApplication.didBecomeActiveNotification,
|
|
48
|
+
object: nil
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@objc private func appDidEnterBackground() {
|
|
53
|
+
currentPlayer?.pause()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@objc private func appDidBecomeActive() {
|
|
57
|
+
if feedIsActive && !isManuallyPaused {
|
|
58
|
+
currentPlayer?.play()
|
|
59
|
+
} else if isManuallyPaused {
|
|
60
|
+
print("App became active but video was manually paused — staying paused")
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private func setupCollectionView() {
|
|
65
|
+
let layout = UICollectionViewFlowLayout()
|
|
66
|
+
layout.scrollDirection = .vertical
|
|
67
|
+
layout.minimumLineSpacing = 0
|
|
68
|
+
layout.minimumInteritemSpacing = 0
|
|
69
|
+
// Initial size - will be updated in layoutSubviews with correct bounds
|
|
70
|
+
layout.itemSize = CGSize(
|
|
71
|
+
width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
|
|
72
|
+
|
|
73
|
+
collectionView = UICollectionView(
|
|
74
|
+
frame: .zero, collectionViewLayout: layout)
|
|
75
|
+
collectionView.isPagingEnabled = true
|
|
76
|
+
collectionView.showsVerticalScrollIndicator = false
|
|
77
|
+
collectionView.backgroundColor = .black
|
|
78
|
+
collectionView.contentInsetAdjustmentBehavior = .never // Prevent safe area adjustments
|
|
79
|
+
collectionView.register(
|
|
80
|
+
VideoFeedCell.self, forCellWithReuseIdentifier: "VideoFeedCell")
|
|
81
|
+
collectionView.dataSource = self
|
|
82
|
+
collectionView.delegate = self
|
|
83
|
+
collectionView.prefetchDataSource = self
|
|
84
|
+
|
|
85
|
+
addSubview(collectionView)
|
|
86
|
+
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
|
87
|
+
NSLayoutConstraint.activate([
|
|
88
|
+
collectionView.topAnchor.constraint(equalTo: topAnchor),
|
|
89
|
+
collectionView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
90
|
+
collectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
91
|
+
collectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
override func layoutSubviews() {
|
|
97
|
+
super.layoutSubviews()
|
|
98
|
+
|
|
99
|
+
// Update layout - use full screen bounds, not collectionView bounds which might include safe area
|
|
100
|
+
if let layout = collectionView.collectionViewLayout
|
|
101
|
+
as? UICollectionViewFlowLayout
|
|
102
|
+
{
|
|
103
|
+
// Use bounds.size to ensure full screen, accounting for any parent container constraints
|
|
104
|
+
let fullScreenSize = CGSize(
|
|
105
|
+
width: bounds.width,
|
|
106
|
+
height: bounds.height
|
|
107
|
+
)
|
|
108
|
+
layout.itemSize = fullScreenSize
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
func setVideos(_ videos: [[String: Any]]) {
|
|
113
|
+
self.videos = videos.compactMap { dict -> VideoData? in
|
|
114
|
+
guard let id = dict["id"] as? String,
|
|
115
|
+
let url = dict["videoUrl"] as? String
|
|
116
|
+
else {
|
|
117
|
+
print("Failed to map video data - missing id or url")
|
|
118
|
+
return nil
|
|
119
|
+
}
|
|
120
|
+
let thumbnail = dict["thumbnailUrl"] as? String
|
|
121
|
+
let viewCount: Int? = {
|
|
122
|
+
if let count = dict["viewCount"] as? Int {
|
|
123
|
+
return count
|
|
124
|
+
}
|
|
125
|
+
return nil
|
|
126
|
+
}()
|
|
127
|
+
return VideoData(
|
|
128
|
+
id: id, videoUrl: url, thumbnailUrl: thumbnail, viewCount: viewCount)
|
|
129
|
+
}
|
|
130
|
+
collectionView.reloadData()
|
|
131
|
+
preloadNextVideos()
|
|
132
|
+
|
|
133
|
+
// Set flag to play first video when cell becomes available
|
|
134
|
+
if !videos.isEmpty {
|
|
135
|
+
shouldPlayFirstVideo = true
|
|
136
|
+
print("Set shouldPlayFirstVideo flag to true")
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
func appendVideos(_ videos: [[String: Any]]) {
|
|
141
|
+
let newVideos = videos.compactMap { dict -> VideoData? in
|
|
142
|
+
guard let id = dict["id"] as? String,
|
|
143
|
+
let url = dict["videoUrl"] as? String
|
|
144
|
+
else { return nil }
|
|
145
|
+
let thumbnail = dict["thumbnailUrl"] as? String
|
|
146
|
+
let duration: Int? = {
|
|
147
|
+
if let duration = dict["viewCount"] as? Int {
|
|
148
|
+
return duration
|
|
149
|
+
}
|
|
150
|
+
return nil
|
|
151
|
+
}()
|
|
152
|
+
return VideoData(
|
|
153
|
+
id: id, videoUrl: url, thumbnailUrl: thumbnail, viewCount: duration)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let startIndex = self.videos.count
|
|
157
|
+
self.videos.append(contentsOf: newVideos)
|
|
158
|
+
|
|
159
|
+
// Insert new cells instead of reloading all
|
|
160
|
+
let indexPaths = (startIndex..<self.videos.count).map {
|
|
161
|
+
IndexPath(item: $0, section: 0)
|
|
162
|
+
}
|
|
163
|
+
collectionView.insertItems(at: indexPaths)
|
|
164
|
+
preloadNextVideos()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
func setFeedActive(_ isActive: Bool) {
|
|
168
|
+
self.feedIsActive = isActive
|
|
169
|
+
|
|
170
|
+
if isActive {
|
|
171
|
+
// When becoming active, try to play the current video
|
|
172
|
+
if !isManuallyPaused {
|
|
173
|
+
if let player = currentPlayer {
|
|
174
|
+
player.play()
|
|
175
|
+
eventEmitter?.sendEvent(withName: "onPlayStateChanged", body: ["isPlaying": true])
|
|
176
|
+
} else {
|
|
177
|
+
let currentIndex = getCurrentIndex()
|
|
178
|
+
if currentIndex >= 0 && currentIndex < videos.count {
|
|
179
|
+
playVideo(at: currentIndex)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
print("setFeedActive: Video was manually paused, not resuming")
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
if let player = currentPlayer {
|
|
187
|
+
player.pause()
|
|
188
|
+
} else {
|
|
189
|
+
print("setFeedActive: No current player to pause")
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private func preloadNextVideos() {
|
|
195
|
+
let visibleIndex = getCurrentIndex()
|
|
196
|
+
let start = visibleIndex + 1
|
|
197
|
+
let end = min(visibleIndex + 2, videos.count - 1)
|
|
198
|
+
|
|
199
|
+
guard start <= end else { return } // avoid invalid range
|
|
200
|
+
|
|
201
|
+
let preloadRange = start...end
|
|
202
|
+
for index in preloadRange {
|
|
203
|
+
let video = videos[index]
|
|
204
|
+
guard preloadTasks[video.videoUrl] == nil,
|
|
205
|
+
let url = URL(string: video.videoUrl)
|
|
206
|
+
else { continue }
|
|
207
|
+
|
|
208
|
+
let asset = AVAsset(url: url)
|
|
209
|
+
preloadTasks[video.videoUrl] = asset
|
|
210
|
+
|
|
211
|
+
asset.loadValuesAsynchronously(forKeys: ["playable"]) { [weak self] in
|
|
212
|
+
DispatchQueue.main.async {
|
|
213
|
+
self?.preloadTasks[video.videoUrl] = nil
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private func getCurrentIndex() -> Int {
|
|
220
|
+
let offset = collectionView.contentOffset.y
|
|
221
|
+
let height = collectionView.bounds.height
|
|
222
|
+
|
|
223
|
+
guard height > 0 else {
|
|
224
|
+
print("Warning: CollectionView height is 0")
|
|
225
|
+
return 0 // or some default/fallback index
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return max(0, Int(round(offset / height)))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private func playVideo(at index: Int) {
|
|
232
|
+
print("Attempting to play video at index:", index)
|
|
233
|
+
|
|
234
|
+
guard feedIsActive else {
|
|
235
|
+
print("Feed inactive, skipping playVideo at index:", index)
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
guard index < videos.count else {
|
|
240
|
+
print("Index out of bounds:", index)
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
currentPlayer?.pause()
|
|
244
|
+
if let cell = collectionView.cellForItem(
|
|
245
|
+
at: IndexPath(item: index, section: 0)) as? VideoFeedCell
|
|
246
|
+
{
|
|
247
|
+
print("Found cell for index:", index)
|
|
248
|
+
currentPlayer = cell.feedPlayer.player
|
|
249
|
+
currentPlayer?.isMuted = false
|
|
250
|
+
|
|
251
|
+
// Only play if not manually paused
|
|
252
|
+
if !isManuallyPaused {
|
|
253
|
+
currentPlayer?.play()
|
|
254
|
+
cell.feedPlayer.isVisible = true
|
|
255
|
+
// Don't hide thumbnail here - it will be hidden when video actually starts playing via callback
|
|
256
|
+
print("▶️ Auto-playing new video at index: \(index)")
|
|
257
|
+
} else {
|
|
258
|
+
cell.feedPlayer.isVisible = false
|
|
259
|
+
print("⏸️ New video at index: \(index) but staying paused (manually paused)")
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
onVideoChange?(videos[index].id)
|
|
263
|
+
} else {
|
|
264
|
+
print("No cell found for index:", index)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// MARK: - Tap Gesture Setup
|
|
269
|
+
|
|
270
|
+
private func setupTapGesture() {
|
|
271
|
+
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
|
|
272
|
+
tapGesture.numberOfTapsRequired = 1
|
|
273
|
+
// Allow tap gesture to work alongside collection view gestures
|
|
274
|
+
tapGesture.cancelsTouchesInView = false
|
|
275
|
+
addGestureRecognizer(tapGesture)
|
|
276
|
+
print("🔥 Tap gesture recognizer added to VideoFeedView")
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
@objc private func handleTap(_ gesture: UITapGestureRecognizer) {
|
|
280
|
+
print("🔥 Tap detected in native VideoFeedView")
|
|
281
|
+
|
|
282
|
+
// Toggle play/pause and get the new state
|
|
283
|
+
let isNowPlaying = togglePlayPause()
|
|
284
|
+
|
|
285
|
+
// Emit event to React Native for UI feedback
|
|
286
|
+
eventEmitter?.sendEvent("onVideoTapped", body: ["isPlaying": isNowPlaying])
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// MARK: - Play/Pause Control Methods
|
|
290
|
+
|
|
291
|
+
/// Gets the current visible index based on scroll position
|
|
292
|
+
private func getCurrentVisibleIndex() -> Int? {
|
|
293
|
+
let visibleIndexPaths = collectionView.indexPathsForVisibleItems
|
|
294
|
+
guard let firstVisibleIndexPath = visibleIndexPaths.first else { return nil }
|
|
295
|
+
return firstVisibleIndexPath.item
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/// Gets the current visible cell
|
|
299
|
+
private func getCurrentCell(at index: Int) -> VideoFeedCell? {
|
|
300
|
+
let indexPath = IndexPath(item: index, section: 0)
|
|
301
|
+
return collectionView.cellForItem(at: indexPath) as? VideoFeedCell
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/// Pauses the currently playing video
|
|
305
|
+
func pauseCurrentVideo() {
|
|
306
|
+
guard let currentIndex = getCurrentVisibleIndex(),
|
|
307
|
+
let cell = getCurrentCell(at: currentIndex) else {
|
|
308
|
+
print("Failed to get current video for pause")
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
isManuallyPaused = true
|
|
313
|
+
cell.feedPlayer.isVisible = false
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/// Plays the currently visible video
|
|
317
|
+
func playCurrentVideo() {
|
|
318
|
+
guard let currentIndex = getCurrentVisibleIndex(),
|
|
319
|
+
let cell = getCurrentCell(at: currentIndex) else {
|
|
320
|
+
print("Failed to get current video for play")
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
isManuallyPaused = false
|
|
325
|
+
cell.feedPlayer.isVisible = true
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/// Toggles play/pause state of current video
|
|
329
|
+
/// Returns the new playing state (true = playing, false = paused)
|
|
330
|
+
func togglePlayPause() -> Bool {
|
|
331
|
+
guard let currentIndex = getCurrentVisibleIndex(),
|
|
332
|
+
let cell = getCurrentCell(at: currentIndex) else {
|
|
333
|
+
print("Failed to get current video for toggle")
|
|
334
|
+
return false
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let wasPlaying = cell.feedPlayer.isVisible
|
|
338
|
+
let newPlayingState = !wasPlaying
|
|
339
|
+
isManuallyPaused = !newPlayingState // Update manual pause state
|
|
340
|
+
|
|
341
|
+
cell.feedPlayer.isVisible = newPlayingState
|
|
342
|
+
|
|
343
|
+
return newPlayingState
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/// Gets the current playing state
|
|
347
|
+
func isCurrentVideoPlaying() -> Bool {
|
|
348
|
+
guard let currentIndex = getCurrentVisibleIndex(),
|
|
349
|
+
let cell = getCurrentCell(at: currentIndex) else {
|
|
350
|
+
return false
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return cell.feedPlayer.isVisible
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Handle removing notificationcenter listners for app states
|
|
357
|
+
deinit {
|
|
358
|
+
NotificationCenter.default.removeObserver(self)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
extension VideoFeedView: UICollectionViewDataSource {
|
|
363
|
+
func collectionView(
|
|
364
|
+
_ collectionView: UICollectionView, numberOfItemsInSection section: Int
|
|
365
|
+
) -> Int {
|
|
366
|
+
return videos.count
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
func collectionView(
|
|
370
|
+
_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath
|
|
371
|
+
) -> UICollectionViewCell {
|
|
372
|
+
let cell =
|
|
373
|
+
collectionView.dequeueReusableCell(
|
|
374
|
+
withReuseIdentifier: "VideoFeedCell", for: indexPath) as! VideoFeedCell
|
|
375
|
+
let video = videos[indexPath.item]
|
|
376
|
+
cell.configure(with: video)
|
|
377
|
+
return cell
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
extension VideoFeedView: UICollectionViewDelegate {
|
|
382
|
+
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
383
|
+
let index = getCurrentIndex()
|
|
384
|
+
for cell in collectionView.visibleCells as! [VideoFeedCell] {
|
|
385
|
+
cell.feedPlayer.isVisible = false
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Reset manual pause state for new video
|
|
389
|
+
isManuallyPaused = false
|
|
390
|
+
|
|
391
|
+
playVideo(at: index)
|
|
392
|
+
|
|
393
|
+
// Emit video change event to React Native
|
|
394
|
+
if index < videos.count {
|
|
395
|
+
let videoId = videos[index].id
|
|
396
|
+
eventEmitter?.sendEvent("onVideoChange", body: ["videoId": videoId])
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
func collectionView(
|
|
401
|
+
_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell,
|
|
402
|
+
forItemAt indexPath: IndexPath
|
|
403
|
+
) {
|
|
404
|
+
// Play first video when it becomes available
|
|
405
|
+
if shouldPlayFirstVideo && indexPath.item == 0 {
|
|
406
|
+
shouldPlayFirstVideo = false
|
|
407
|
+
// Use a small delay to ensure the cell is fully configured
|
|
408
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
|
409
|
+
self?.playVideo(at: 0)
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if indexPath.item >= videos.count - 3 {
|
|
414
|
+
onEndReached?()
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
extension VideoFeedView: UICollectionViewDataSourcePrefetching {
|
|
420
|
+
func collectionView(
|
|
421
|
+
_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]
|
|
422
|
+
) {
|
|
423
|
+
preloadNextVideos()
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
struct VideoData {
|
|
428
|
+
let id: String
|
|
429
|
+
let videoUrl: String
|
|
430
|
+
let thumbnailUrl: String?
|
|
431
|
+
let viewCount: Int?
|
|
432
|
+
}
|