react-native-unified-player 0.2.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 +20 -0
- package/README.md +95 -0
- package/UnifiedPlayer.podspec +32 -0
- package/android/build.gradle +81 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +7 -0
- package/android/src/main/AndroidManifestNew.xml +4 -0
- package/android/src/main/java/com/unifiedplayer/UnifiedPlayerEventEmitter.kt +62 -0
- package/android/src/main/java/com/unifiedplayer/UnifiedPlayerModule.kt +93 -0
- package/android/src/main/java/com/unifiedplayer/UnifiedPlayerPackage.kt +20 -0
- package/android/src/main/java/com/unifiedplayer/UnifiedPlayerView.kt +265 -0
- package/android/src/main/java/com/unifiedplayer/UnifiedPlayerViewManager.kt +33 -0
- package/ios/UnifiedPlayerModule.h +5 -0
- package/ios/UnifiedPlayerViewManager.m +670 -0
- package/lib/module/index.js +97 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/index.d.ts +46 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +154 -0
- package/src/index.tsx +135 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
#import <React/RCTViewManager.h>
|
|
2
|
+
#import <React/RCTLog.h>
|
|
3
|
+
#import <React/RCTBridgeModule.h>
|
|
4
|
+
#import <React/RCTEventEmitter.h>
|
|
5
|
+
#import <React/RCTUIManager.h>
|
|
6
|
+
#import <React/RCTBridge.h>
|
|
7
|
+
#import <React/RCTUIManagerUtils.h>
|
|
8
|
+
#import <MobileVLCKit/MobileVLCKit.h>
|
|
9
|
+
|
|
10
|
+
// Forward declarations
|
|
11
|
+
@interface UnifiedPlayerUIView : UIView <VLCMediaPlayerDelegate>
|
|
12
|
+
@property (nonatomic, strong) VLCMediaPlayer *player;
|
|
13
|
+
@property (nonatomic, copy) NSString *videoUrlString;
|
|
14
|
+
@property (nonatomic, copy) NSString *authToken;
|
|
15
|
+
@property (nonatomic, assign) BOOL autoplay;
|
|
16
|
+
@property (nonatomic, assign) BOOL loop;
|
|
17
|
+
@property (nonatomic, strong) NSArray *mediaOptions;
|
|
18
|
+
@property (nonatomic, weak) RCTBridge *bridge;
|
|
19
|
+
@property (nonatomic, assign) VLCMediaPlayerState previousState;
|
|
20
|
+
@property (nonatomic, assign) BOOL hasRenderedVideo;
|
|
21
|
+
|
|
22
|
+
- (void)setupWithVideoUrlString:(NSString *)videoUrlString;
|
|
23
|
+
- (void)play;
|
|
24
|
+
- (void)pause;
|
|
25
|
+
- (void)seekToTime:(float)time;
|
|
26
|
+
- (float)getCurrentTime;
|
|
27
|
+
- (float)getDuration;
|
|
28
|
+
@end
|
|
29
|
+
|
|
30
|
+
// UnifiedPlayerModule - Module for handling control methods
|
|
31
|
+
@interface UnifiedPlayerModule : RCTEventEmitter <RCTBridgeModule>
|
|
32
|
+
@end
|
|
33
|
+
|
|
34
|
+
@implementation UnifiedPlayerModule
|
|
35
|
+
|
|
36
|
+
RCT_EXPORT_MODULE();
|
|
37
|
+
|
|
38
|
+
- (NSArray<NSString *> *)supportedEvents {
|
|
39
|
+
return @[
|
|
40
|
+
@"onReadyToPlay",
|
|
41
|
+
@"onError",
|
|
42
|
+
@"onProgress",
|
|
43
|
+
@"onPlaybackComplete",
|
|
44
|
+
@"onPlaybackStalled",
|
|
45
|
+
@"onPlaybackResumed",
|
|
46
|
+
@"onBuffering",
|
|
47
|
+
@"onPlaying",
|
|
48
|
+
@"onPaused",
|
|
49
|
+
@"onStopped"
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
- (dispatch_queue_t)methodQueue {
|
|
54
|
+
return dispatch_get_main_queue();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Play video
|
|
58
|
+
RCT_EXPORT_METHOD(play:(nonnull NSNumber *)reactTag) {
|
|
59
|
+
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
|
|
60
|
+
UnifiedPlayerUIView *view = (UnifiedPlayerUIView *)viewRegistry[reactTag];
|
|
61
|
+
if (![view isKindOfClass:[UnifiedPlayerUIView class]]) {
|
|
62
|
+
RCTLogError(@"Invalid view for tag %@", reactTag);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
[view play];
|
|
66
|
+
}];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Pause video
|
|
70
|
+
RCT_EXPORT_METHOD(pause:(nonnull NSNumber *)reactTag) {
|
|
71
|
+
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
|
|
72
|
+
UnifiedPlayerUIView *view = (UnifiedPlayerUIView *)viewRegistry[reactTag];
|
|
73
|
+
if (![view isKindOfClass:[UnifiedPlayerUIView class]]) {
|
|
74
|
+
RCTLogError(@"Invalid view for tag %@", reactTag);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
[view pause];
|
|
78
|
+
}];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Seek to specific time
|
|
82
|
+
RCT_EXPORT_METHOD(seekTo:(nonnull NSNumber *)reactTag time:(nonnull NSNumber *)time) {
|
|
83
|
+
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
|
|
84
|
+
UnifiedPlayerUIView *view = (UnifiedPlayerUIView *)viewRegistry[reactTag];
|
|
85
|
+
if (![view isKindOfClass:[UnifiedPlayerUIView class]]) {
|
|
86
|
+
RCTLogError(@"Invalid view for tag %@", reactTag);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
[view seekToTime:[time floatValue]];
|
|
90
|
+
}];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Get current playback time
|
|
94
|
+
RCT_EXPORT_METHOD(getCurrentTime:(nonnull NSNumber *)reactTag
|
|
95
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
96
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
97
|
+
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
|
|
98
|
+
UnifiedPlayerUIView *view = (UnifiedPlayerUIView *)viewRegistry[reactTag];
|
|
99
|
+
if (![view isKindOfClass:[UnifiedPlayerUIView class]]) {
|
|
100
|
+
reject(@"error", @"Invalid view for tag", nil);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
resolve(@([view getCurrentTime]));
|
|
104
|
+
}];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Get video duration
|
|
108
|
+
RCT_EXPORT_METHOD(getDuration:(nonnull NSNumber *)reactTag
|
|
109
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
110
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
111
|
+
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
|
|
112
|
+
UnifiedPlayerUIView *view = (UnifiedPlayerUIView *)viewRegistry[reactTag];
|
|
113
|
+
if (![view isKindOfClass:[UnifiedPlayerUIView class]]) {
|
|
114
|
+
reject(@"error", @"Invalid view for tag", nil);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
resolve(@([view getDuration]));
|
|
118
|
+
}];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@end
|
|
122
|
+
|
|
123
|
+
// Global event emitter instance
|
|
124
|
+
static UnifiedPlayerModule *eventEmitter = nil;
|
|
125
|
+
|
|
126
|
+
// Main player view implementation
|
|
127
|
+
@implementation UnifiedPlayerUIView
|
|
128
|
+
|
|
129
|
+
- (instancetype)init {
|
|
130
|
+
if ((self = [super init])) {
|
|
131
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] Initializing player view");
|
|
132
|
+
|
|
133
|
+
// Initialize properties
|
|
134
|
+
_hasRenderedVideo = NO;
|
|
135
|
+
|
|
136
|
+
// Create the player
|
|
137
|
+
_player = [[VLCMediaPlayer alloc] init];
|
|
138
|
+
_player.delegate = self;
|
|
139
|
+
|
|
140
|
+
// Make sure we're visible and properly laid out
|
|
141
|
+
self.backgroundColor = [UIColor blackColor];
|
|
142
|
+
self.opaque = YES;
|
|
143
|
+
self.userInteractionEnabled = YES;
|
|
144
|
+
|
|
145
|
+
// Important: Enable content mode to scale properly
|
|
146
|
+
self.contentMode = UIViewContentModeScaleAspectFit;
|
|
147
|
+
self.clipsToBounds = YES;
|
|
148
|
+
|
|
149
|
+
// After the view is fully initialized, set it as the drawable
|
|
150
|
+
_player.drawable = self;
|
|
151
|
+
|
|
152
|
+
_autoplay = YES;
|
|
153
|
+
_loop = NO;
|
|
154
|
+
|
|
155
|
+
// Add notification for app entering background
|
|
156
|
+
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
157
|
+
selector:@selector(appDidEnterBackground:)
|
|
158
|
+
name:UIApplicationDidEnterBackgroundNotification
|
|
159
|
+
object:nil];
|
|
160
|
+
|
|
161
|
+
// Add notification for app entering foreground
|
|
162
|
+
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
163
|
+
selector:@selector(appDidBecomeActive:)
|
|
164
|
+
name:UIApplicationDidBecomeActiveNotification
|
|
165
|
+
object:nil];
|
|
166
|
+
}
|
|
167
|
+
return self;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Override drawRect to ensure our rendering context is set up without recursion
|
|
171
|
+
- (void)drawRect:(CGRect)rect {
|
|
172
|
+
[super drawRect:rect];
|
|
173
|
+
|
|
174
|
+
// This can sometimes help with VLC rendering issues
|
|
175
|
+
// But check if we need to do this to avoid unnecessary work
|
|
176
|
+
if (_player && _player.drawable != self) {
|
|
177
|
+
_player.drawable = self;
|
|
178
|
+
// Do not call any methods that would trigger layoutSubviews or drawRect again
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Override layoutSubviews with safer implementation to avoid recursion
|
|
183
|
+
- (void)layoutSubviews {
|
|
184
|
+
[super layoutSubviews];
|
|
185
|
+
|
|
186
|
+
// Check bounds without using NSStringFromCGRect (which can cause recursion)
|
|
187
|
+
CGRect bounds = self.bounds;
|
|
188
|
+
|
|
189
|
+
// Only update if we have valid bounds and a player
|
|
190
|
+
if (bounds.size.width > 0 && bounds.size.height > 0 && _player) {
|
|
191
|
+
// Ensure drawable is set but don't call any methods that could cause recursion
|
|
192
|
+
if (_player.drawable != self) {
|
|
193
|
+
_player.drawable = self;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Let VLC know the size has changed but don't force any redraws here
|
|
197
|
+
// This may be VLC-specific and not required for all implementations
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
- (void)didMoveToSuperview {
|
|
202
|
+
[super didMoveToSuperview];
|
|
203
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] View moved to superview: %@", self.superview);
|
|
204
|
+
|
|
205
|
+
// Sometimes VLC needs a reset of the drawable when moving to a superview
|
|
206
|
+
if (_player) {
|
|
207
|
+
_player.drawable = nil;
|
|
208
|
+
_player.drawable = self;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
- (void)didMoveToWindow {
|
|
213
|
+
[super didMoveToWindow];
|
|
214
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] View moved to window: %@", self.window);
|
|
215
|
+
|
|
216
|
+
// Ensure proper rendering when window changes
|
|
217
|
+
if (_player) {
|
|
218
|
+
_player.drawable = nil;
|
|
219
|
+
_player.drawable = self;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
- (void)appDidEnterBackground:(NSNotification *)notification {
|
|
224
|
+
if (_player.isPlaying) {
|
|
225
|
+
[_player pause];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
- (void)appDidBecomeActive:(NSNotification *)notification {
|
|
230
|
+
// Optionally resume playback when app becomes active again
|
|
231
|
+
// if (wasPlayingBeforeBackground) {
|
|
232
|
+
// [_player play];
|
|
233
|
+
// }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
- (void)sendProgressEvent:(float)currentTime duration:(float)duration {
|
|
237
|
+
NSDictionary *event = @{
|
|
238
|
+
@"currentTime": @(currentTime),
|
|
239
|
+
@"duration": @(duration)
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
if (eventEmitter != nil) {
|
|
243
|
+
[eventEmitter sendEventWithName:@"onProgress" body:event];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
- (void)sendEvent:(NSString *)eventName body:(NSDictionary *)body {
|
|
248
|
+
if (eventEmitter != nil) {
|
|
249
|
+
[eventEmitter sendEventWithName:eventName body:body];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
- (void)setupWithVideoUrlString:(NSString *)videoUrlString {
|
|
254
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] setupWithVideoUrlString: %@", videoUrlString);
|
|
255
|
+
_videoUrlString = [videoUrlString copy];
|
|
256
|
+
|
|
257
|
+
if (videoUrlString) {
|
|
258
|
+
[self loadVideo];
|
|
259
|
+
} else {
|
|
260
|
+
[_player stop];
|
|
261
|
+
_player.media = nil;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
- (void)loadVideo {
|
|
266
|
+
// Log view state first
|
|
267
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] View state before loading - Frame: %@, Bounds: %@, Visible: %@, Superview: %@",
|
|
268
|
+
NSStringFromCGRect(self.frame),
|
|
269
|
+
NSStringFromCGRect(self.bounds),
|
|
270
|
+
self.window ? @"YES" : @"NO",
|
|
271
|
+
self.superview ? @"YES" : @"NO");
|
|
272
|
+
|
|
273
|
+
// Reset the rendered flag
|
|
274
|
+
_hasRenderedVideo = NO;
|
|
275
|
+
|
|
276
|
+
// Make sure we're in the main thread
|
|
277
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
278
|
+
// Check if URL is valid
|
|
279
|
+
NSURL *videoURL = [NSURL URLWithString:self->_videoUrlString];
|
|
280
|
+
if (!videoURL) {
|
|
281
|
+
// Try with encoding if the original URL doesn't work
|
|
282
|
+
NSString *escapedString = [self->_videoUrlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
|
|
283
|
+
videoURL = [NSURL URLWithString:escapedString];
|
|
284
|
+
|
|
285
|
+
if (!videoURL) {
|
|
286
|
+
NSDictionary *errorInfo = @{
|
|
287
|
+
@"code": @"INVALID_URL",
|
|
288
|
+
@"message": @"Invalid URL format",
|
|
289
|
+
@"details": self->_videoUrlString ?: @""
|
|
290
|
+
};
|
|
291
|
+
[self sendEvent:@"onError" body:errorInfo];
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] URL needed encoding: %@", videoURL.absoluteString);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] Using URL: %@", videoURL.absoluteString);
|
|
299
|
+
|
|
300
|
+
// Create VLC media options array
|
|
301
|
+
NSMutableArray *mediaOptions = [NSMutableArray array];
|
|
302
|
+
|
|
303
|
+
// Add auth token if available
|
|
304
|
+
if (self->_authToken.length > 0) {
|
|
305
|
+
[mediaOptions addObject:[NSString stringWithFormat:@"http-header-fields=Authorization: Bearer %@", self->_authToken]];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Add default network caching options for streaming
|
|
309
|
+
BOOL isStreaming = [videoURL.scheme hasPrefix:@"http"] ||
|
|
310
|
+
[videoURL.scheme hasPrefix:@"rtsp"] ||
|
|
311
|
+
[videoURL.scheme hasPrefix:@"rtmp"] ||
|
|
312
|
+
[videoURL.scheme hasPrefix:@"mms"];
|
|
313
|
+
|
|
314
|
+
if (isStreaming) {
|
|
315
|
+
if (![self->_mediaOptions containsObject:@"network-caching"]) {
|
|
316
|
+
[mediaOptions addObject:@"--network-caching=1500"];
|
|
317
|
+
[mediaOptions addObject:@"--live-caching=1500"];
|
|
318
|
+
[mediaOptions addObject:@"--file-caching=1500"];
|
|
319
|
+
[mediaOptions addObject:@"--disc-caching=300"];
|
|
320
|
+
[mediaOptions addObject:@"--clock-jitter=0"];
|
|
321
|
+
[mediaOptions addObject:@"--clock-synchro=0"];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Improve TCP/UDP performance for streams
|
|
325
|
+
[mediaOptions addObject:@"--rtsp-tcp"];
|
|
326
|
+
[mediaOptions addObject:@"--ipv4-timeout=1000"];
|
|
327
|
+
[mediaOptions addObject:@"--http-reconnect"];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Add custom media options if provided
|
|
331
|
+
if (self->_mediaOptions && self->_mediaOptions.count > 0) {
|
|
332
|
+
for (NSString *option in self->_mediaOptions) {
|
|
333
|
+
if ([option isKindOfClass:[NSString class]]) {
|
|
334
|
+
// Make sure option starts with --
|
|
335
|
+
if ([option hasPrefix:@"--"]) {
|
|
336
|
+
[mediaOptions addObject:option];
|
|
337
|
+
} else {
|
|
338
|
+
[mediaOptions addObject:[NSString stringWithFormat:@"--%@", option]];
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Stop any existing playback first
|
|
345
|
+
[self->_player stop];
|
|
346
|
+
|
|
347
|
+
// Create VLC media with options
|
|
348
|
+
VLCMedia *media = [VLCMedia mediaWithURL:videoURL];
|
|
349
|
+
|
|
350
|
+
// Apply media options
|
|
351
|
+
for (NSString *option in mediaOptions) {
|
|
352
|
+
[media addOption:option];
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Set new media to player
|
|
356
|
+
self->_player.media = media;
|
|
357
|
+
|
|
358
|
+
// Ensure drawable is set correctly
|
|
359
|
+
self->_player.drawable = self;
|
|
360
|
+
|
|
361
|
+
// Configure additional player settings
|
|
362
|
+
self->_player.videoAspectRatio = NULL; // Use default aspect ratio - must be NULL not nil for VLCKit
|
|
363
|
+
|
|
364
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] Media configured with options: %@", mediaOptions);
|
|
365
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] Player drawable: %@, bounds: %@",
|
|
366
|
+
self->_player.drawable, NSStringFromCGRect(self.bounds));
|
|
367
|
+
|
|
368
|
+
// Instead of force redraw, use these methods directly:
|
|
369
|
+
[self setNeedsLayout];
|
|
370
|
+
[self layoutIfNeeded];
|
|
371
|
+
[self setNeedsDisplay];
|
|
372
|
+
|
|
373
|
+
// Start playback if autoplay is enabled
|
|
374
|
+
if (self->_autoplay) {
|
|
375
|
+
[self->_player play];
|
|
376
|
+
|
|
377
|
+
// Don't call forceRedraw again, just update the view
|
|
378
|
+
[self setNeedsDisplay];
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
- (void)play {
|
|
384
|
+
if (_player.media) {
|
|
385
|
+
// Make sure drawable is properly set before playing
|
|
386
|
+
if (_player.drawable != self) {
|
|
387
|
+
_player.drawable = self;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Ensure video track is enabled if available
|
|
391
|
+
if (_player.numberOfVideoTracks > 0 && _player.currentVideoTrackIndex == -1) {
|
|
392
|
+
// Attempt to enable the first video track
|
|
393
|
+
if (_player.videoTrackIndexes.count > 0) {
|
|
394
|
+
NSNumber *videoTrackIndex = [_player.videoTrackIndexes firstObject];
|
|
395
|
+
_player.currentVideoTrackIndex = [videoTrackIndex intValue];
|
|
396
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] Enabling video track index: %@", videoTrackIndex);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Apply aspect ratio settings
|
|
401
|
+
_player.videoAspectRatio = NULL; // Use default aspect ratio
|
|
402
|
+
|
|
403
|
+
// Set scale mode for the VLC video output
|
|
404
|
+
// Note: VLC has its own scaling which is separate from UIView contentMode
|
|
405
|
+
_player.scaleFactor = 0.0; // Auto scale
|
|
406
|
+
|
|
407
|
+
[_player play];
|
|
408
|
+
|
|
409
|
+
// Use these methods instead of forceRedraw
|
|
410
|
+
[self setNeedsLayout];
|
|
411
|
+
[self layoutIfNeeded];
|
|
412
|
+
[self setNeedsDisplay];
|
|
413
|
+
|
|
414
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] play called, drawable: %@, frame: %@",
|
|
415
|
+
_player.drawable, NSStringFromCGRect(self.frame));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
- (void)pause {
|
|
420
|
+
[_player pause];
|
|
421
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] pause called");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
- (void)seekToTime:(float)time {
|
|
425
|
+
// VLC uses a 0-1 position value for seeking
|
|
426
|
+
float position = time / [self getDuration];
|
|
427
|
+
position = MAX(0, MIN(1, position)); // Ensure position is between 0 and 1
|
|
428
|
+
|
|
429
|
+
[_player setPosition:position];
|
|
430
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] Seek to %f (position: %f)", time, position);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
- (float)getCurrentTime {
|
|
434
|
+
return _player.time.intValue / 1000.0f; // Convert from milliseconds to seconds
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
- (float)getDuration {
|
|
438
|
+
return _player.media.length.intValue / 1000.0f; // Convert from milliseconds to seconds
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
- (void)setAutoplay:(BOOL)autoplay {
|
|
442
|
+
_autoplay = autoplay;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
- (void)setLoop:(BOOL)loop {
|
|
446
|
+
_loop = loop;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
- (void)setAuthToken:(NSString *)authToken {
|
|
450
|
+
if (_authToken != authToken) {
|
|
451
|
+
_authToken = [authToken copy];
|
|
452
|
+
// If we already have a URL, reload with the new auth token
|
|
453
|
+
if (_videoUrlString) {
|
|
454
|
+
[self loadVideo];
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
#pragma mark - VLCMediaPlayerDelegate
|
|
460
|
+
|
|
461
|
+
- (void)mediaPlayerTimeChanged:(NSNotification *)notification {
|
|
462
|
+
float currentTime = [self getCurrentTime];
|
|
463
|
+
float duration = [self getDuration];
|
|
464
|
+
|
|
465
|
+
// Avoid sending progress events for invalid durations
|
|
466
|
+
if (duration > 0 && !isnan(duration)) {
|
|
467
|
+
[self sendProgressEvent:currentTime duration:duration];
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
- (void)mediaPlayerStateChanged:(NSNotification *)notification {
|
|
472
|
+
VLCMediaPlayerState state = _player.state;
|
|
473
|
+
|
|
474
|
+
// Debug information for video output
|
|
475
|
+
if (state == VLCMediaPlayerStatePlaying) {
|
|
476
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] Video size: %@",
|
|
477
|
+
NSStringFromCGSize(_player.videoSize));
|
|
478
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] Has video out: %@",
|
|
479
|
+
_player.hasVideoOut ? @"YES" : @"NO");
|
|
480
|
+
|
|
481
|
+
// Check video tracks
|
|
482
|
+
NSInteger videoTrackCount = _player.numberOfVideoTracks;
|
|
483
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] Video track count: %ld", (long)videoTrackCount);
|
|
484
|
+
|
|
485
|
+
if (videoTrackCount > 0) {
|
|
486
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] Current video track index: %d", _player.currentVideoTrackIndex);
|
|
487
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] Video track names: %@", _player.videoTrackNames);
|
|
488
|
+
|
|
489
|
+
// Trigger a delayed drawable update to help with rendering
|
|
490
|
+
if (!_hasRenderedVideo && _player.videoSize.width > 0) {
|
|
491
|
+
_hasRenderedVideo = YES;
|
|
492
|
+
[self updatePlayerDrawableWithDelay];
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
495
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] No video tracks found!");
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Check for buffer state transitions
|
|
500
|
+
if ((state == VLCMediaPlayerStateBuffering && _previousState != VLCMediaPlayerStateBuffering) ||
|
|
501
|
+
(state == VLCMediaPlayerStatePlaying && _previousState == VLCMediaPlayerStateBuffering)) {
|
|
502
|
+
[self handleBufferingStatusChange];
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Save the current state for next time
|
|
506
|
+
_previousState = state;
|
|
507
|
+
|
|
508
|
+
switch (state) {
|
|
509
|
+
case VLCMediaPlayerStateOpening:
|
|
510
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] VLCMediaPlayerStateOpening");
|
|
511
|
+
break;
|
|
512
|
+
|
|
513
|
+
case VLCMediaPlayerStateBuffering:
|
|
514
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] VLCMediaPlayerStateBuffering");
|
|
515
|
+
[self sendEvent:@"onBuffering" body:@{
|
|
516
|
+
@"isPlaying": @(_player.isPlaying),
|
|
517
|
+
@"duration": @([self getDuration])
|
|
518
|
+
}];
|
|
519
|
+
break;
|
|
520
|
+
|
|
521
|
+
case VLCMediaPlayerStatePlaying:
|
|
522
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] VLCMediaPlayerStatePlaying");
|
|
523
|
+
[self sendEvent:@"onPlaying" body:@{
|
|
524
|
+
@"target": @(1),
|
|
525
|
+
@"duration": @([self getDuration] * 1000), // Convert to ms for consistency
|
|
526
|
+
@"seekable": @(YES)
|
|
527
|
+
}];
|
|
528
|
+
[self sendEvent:@"onReadyToPlay" body:@{}];
|
|
529
|
+
break;
|
|
530
|
+
|
|
531
|
+
case VLCMediaPlayerStatePaused:
|
|
532
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] VLCMediaPlayerStatePaused");
|
|
533
|
+
[self sendEvent:@"onPaused" body:@{}];
|
|
534
|
+
break;
|
|
535
|
+
|
|
536
|
+
case VLCMediaPlayerStateStopped:
|
|
537
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] VLCMediaPlayerStateStopped");
|
|
538
|
+
[self sendEvent:@"onStopped" body:@{}];
|
|
539
|
+
break;
|
|
540
|
+
|
|
541
|
+
case VLCMediaPlayerStateEnded:
|
|
542
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] VLCMediaPlayerStateEnded");
|
|
543
|
+
[self sendEvent:@"onPlaybackComplete" body:@{}];
|
|
544
|
+
|
|
545
|
+
// Handle looping
|
|
546
|
+
if (_loop) {
|
|
547
|
+
[_player stop];
|
|
548
|
+
[_player play];
|
|
549
|
+
}
|
|
550
|
+
break;
|
|
551
|
+
|
|
552
|
+
case VLCMediaPlayerStateError:
|
|
553
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] VLCMediaPlayerStateError");
|
|
554
|
+
[self sendEvent:@"onError" body:@{
|
|
555
|
+
@"code": @"PLAYBACK_ERROR",
|
|
556
|
+
@"message": @"VLC player encountered an error",
|
|
557
|
+
@"details": @{@"url": _videoUrlString ?: @""}
|
|
558
|
+
}];
|
|
559
|
+
break;
|
|
560
|
+
|
|
561
|
+
default:
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
- (void)mediaPlayerSnapshot:(NSNotification *)notification {
|
|
567
|
+
// Handle snapshot completion if needed
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
- (void)mediaPlayerTitleChanged:(NSNotification *)notification {
|
|
571
|
+
// Handle title changes if needed
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Define a method that maps VLC buffer state changes to appropriate RN events
|
|
575
|
+
- (void)handleBufferingStatusChange {
|
|
576
|
+
if (_player.state == VLCMediaPlayerStateBuffering) {
|
|
577
|
+
// When buffering, send stalled event
|
|
578
|
+
[self sendEvent:@"onPlaybackStalled" body:@{}];
|
|
579
|
+
} else if (_player.state == VLCMediaPlayerStatePlaying && _player.isPlaying) {
|
|
580
|
+
// When buffer is full and playing again, send resumed event
|
|
581
|
+
[self sendEvent:@"onPlaybackResumed" body:@{}];
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
- (void)dealloc {
|
|
586
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] Deallocating player view");
|
|
587
|
+
|
|
588
|
+
// Remove all observers
|
|
589
|
+
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
590
|
+
|
|
591
|
+
// Stop playback and release player
|
|
592
|
+
[_player stop];
|
|
593
|
+
_player.delegate = nil;
|
|
594
|
+
_player = nil;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Update updatePlayerDrawableWithDelay to use safer redraw approach
|
|
598
|
+
- (void)updatePlayerDrawableWithDelay {
|
|
599
|
+
// Sometimes a slight delay can help with rendering issues
|
|
600
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
601
|
+
if (self->_player) {
|
|
602
|
+
// Reset drawable
|
|
603
|
+
self->_player.drawable = nil;
|
|
604
|
+
self->_player.drawable = self;
|
|
605
|
+
|
|
606
|
+
// Call UIKit methods directly instead of forceRedraw
|
|
607
|
+
[self setNeedsLayout];
|
|
608
|
+
[self layoutIfNeeded];
|
|
609
|
+
[self setNeedsDisplay];
|
|
610
|
+
|
|
611
|
+
RCTLogInfo(@"[UnifiedPlayerViewManager] Drawable updated with delay");
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
@end
|
|
617
|
+
|
|
618
|
+
@interface UnifiedPlayerViewManager : RCTViewManager
|
|
619
|
+
@end
|
|
620
|
+
|
|
621
|
+
@implementation UnifiedPlayerViewManager
|
|
622
|
+
|
|
623
|
+
RCT_EXPORT_MODULE(UnifiedPlayerView)
|
|
624
|
+
|
|
625
|
+
@synthesize bridge = _bridge;
|
|
626
|
+
|
|
627
|
+
- (UIView *)view
|
|
628
|
+
{
|
|
629
|
+
UnifiedPlayerUIView *playerView = [[UnifiedPlayerUIView alloc] init];
|
|
630
|
+
playerView.bridge = self.bridge;
|
|
631
|
+
|
|
632
|
+
// Store the event emitter for sending events
|
|
633
|
+
if (eventEmitter == nil) {
|
|
634
|
+
eventEmitter = [self.bridge moduleForClass:[UnifiedPlayerModule class]];
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return playerView;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Video URL property
|
|
641
|
+
RCT_CUSTOM_VIEW_PROPERTY(videoUrl, NSString, UnifiedPlayerUIView)
|
|
642
|
+
{
|
|
643
|
+
[view setupWithVideoUrlString:json];
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Autoplay property
|
|
647
|
+
RCT_CUSTOM_VIEW_PROPERTY(autoplay, BOOL, UnifiedPlayerUIView)
|
|
648
|
+
{
|
|
649
|
+
view.autoplay = [RCTConvert BOOL:json];
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Loop property
|
|
653
|
+
RCT_CUSTOM_VIEW_PROPERTY(loop, BOOL, UnifiedPlayerUIView)
|
|
654
|
+
{
|
|
655
|
+
view.loop = [RCTConvert BOOL:json];
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Auth token property
|
|
659
|
+
RCT_CUSTOM_VIEW_PROPERTY(authToken, NSString, UnifiedPlayerUIView)
|
|
660
|
+
{
|
|
661
|
+
view.authToken = [RCTConvert NSString:json];
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Media options property
|
|
665
|
+
RCT_CUSTOM_VIEW_PROPERTY(mediaOptions, NSArray, UnifiedPlayerUIView)
|
|
666
|
+
{
|
|
667
|
+
view.mediaOptions = [RCTConvert NSArray:json];
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
@end
|