@granite-js/video 1.0.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/GraniteVideo.podspec +72 -0
  3. package/android/README.md +232 -0
  4. package/android/build.gradle +117 -0
  5. package/android/gradle.properties +8 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/java/run/granite/video/GraniteVideoModule.kt +70 -0
  8. package/android/src/main/java/run/granite/video/GraniteVideoPackage.kt +43 -0
  9. package/android/src/main/java/run/granite/video/GraniteVideoView.kt +384 -0
  10. package/android/src/main/java/run/granite/video/GraniteVideoViewManager.kt +318 -0
  11. package/android/src/main/java/run/granite/video/event/GraniteVideoEvents.kt +273 -0
  12. package/android/src/main/java/run/granite/video/event/VideoEventDispatcher.kt +66 -0
  13. package/android/src/main/java/run/granite/video/event/VideoEventListenerAdapter.kt +157 -0
  14. package/android/src/main/java/run/granite/video/provider/GraniteVideoProvider.kt +346 -0
  15. package/android/src/media3/AndroidManifest.xml +9 -0
  16. package/android/src/media3/java/run/granite/video/provider/media3/ExoPlayerProvider.kt +386 -0
  17. package/android/src/media3/java/run/granite/video/provider/media3/Media3ContentProvider.kt +29 -0
  18. package/android/src/media3/java/run/granite/video/provider/media3/Media3Initializer.kt +25 -0
  19. package/android/src/media3/java/run/granite/video/provider/media3/factory/ExoPlayerFactory.kt +32 -0
  20. package/android/src/media3/java/run/granite/video/provider/media3/factory/MediaSourceFactory.kt +61 -0
  21. package/android/src/media3/java/run/granite/video/provider/media3/factory/TrackSelectorFactory.kt +26 -0
  22. package/android/src/media3/java/run/granite/video/provider/media3/factory/VideoSurfaceFactory.kt +62 -0
  23. package/android/src/media3/java/run/granite/video/provider/media3/listener/ExoPlayerEventListener.kt +104 -0
  24. package/android/src/media3/java/run/granite/video/provider/media3/scheduler/ProgressScheduler.kt +56 -0
  25. package/android/src/test/java/run/granite/video/GraniteVideoViewRobolectricTest.kt +598 -0
  26. package/android/src/test/java/run/granite/video/event/VideoEventListenerAdapterTest.kt +319 -0
  27. package/android/src/test/java/run/granite/video/helpers/FakeGraniteVideoProvider.kt +161 -0
  28. package/android/src/test/java/run/granite/video/helpers/TestProgressScheduler.kt +42 -0
  29. package/android/src/test/java/run/granite/video/provider/GraniteVideoRegistryTest.kt +232 -0
  30. package/android/src/test/java/run/granite/video/provider/ProviderContractTest.kt +174 -0
  31. package/android/src/test/java/run/granite/video/provider/media3/listener/ExoPlayerEventListenerTest.kt +243 -0
  32. package/android/src/test/resources/kotest.properties +2 -0
  33. package/dist/module/GraniteVideo.js +458 -0
  34. package/dist/module/GraniteVideo.js.map +1 -0
  35. package/dist/module/GraniteVideoNativeComponent.ts +265 -0
  36. package/dist/module/index.js +7 -0
  37. package/dist/module/index.js.map +1 -0
  38. package/dist/module/package.json +1 -0
  39. package/dist/module/types.js +4 -0
  40. package/dist/module/types.js.map +1 -0
  41. package/dist/typescript/GraniteVideo.d.ts +12 -0
  42. package/dist/typescript/GraniteVideoNativeComponent.d.ts +189 -0
  43. package/dist/typescript/index.d.ts +5 -0
  44. package/dist/typescript/types.d.ts +328 -0
  45. package/ios/GraniteVideoComponentsProvider.h +10 -0
  46. package/ios/GraniteVideoProvider.swift +280 -0
  47. package/ios/GraniteVideoView.h +15 -0
  48. package/ios/GraniteVideoView.mm +661 -0
  49. package/ios/Providers/AVPlayerProvider.swift +541 -0
  50. package/package.json +106 -0
  51. package/src/GraniteVideo.tsx +575 -0
  52. package/src/GraniteVideoNativeComponent.ts +265 -0
  53. package/src/index.ts +8 -0
  54. package/src/types.ts +464 -0
@@ -0,0 +1,541 @@
1
+ import UIKit
2
+ import AVFoundation
3
+ import AVKit
4
+ import Combine
5
+
6
+ // MARK: - Player Container View
7
+
8
+ private class AVPlayerContainerView: UIView {
9
+ var playerLayer: AVPlayerLayer? {
10
+ didSet {
11
+ if let layer = playerLayer {
12
+ self.layer.addSublayer(layer)
13
+ layer.frame = self.bounds
14
+ }
15
+ }
16
+ }
17
+
18
+ override func layoutSubviews() {
19
+ super.layoutSubviews()
20
+ playerLayer?.frame = self.bounds
21
+ }
22
+ }
23
+
24
+ // MARK: - AVPlayerProvider
25
+
26
+ @objc public class AVPlayerProvider: NSObject, GraniteVideoProvidable {
27
+
28
+ // MARK: - Properties
29
+
30
+ @objc public weak var delegate: GraniteVideoDelegate?
31
+
32
+ private var player: AVPlayer
33
+ private var playerLayer: AVPlayerLayer
34
+ private var playerView: AVPlayerContainerView?
35
+ private var playerItem: AVPlayerItem?
36
+ private var timeObserver: Any?
37
+ private var pipController: AVPictureInPictureController?
38
+
39
+ private var shouldRepeat: Bool = false
40
+ private var isMuted: Bool = false
41
+ private var playerVolume: Float = 1.0
42
+ private var playerRate: Float = 1.0
43
+ private var playInBackgroundEnabled: Bool = false
44
+ private var playWhenInactiveEnabled: Bool = false
45
+ private var isFullscreen: Bool = false
46
+ private var pipEnabled: Bool = false
47
+ private var preferredForwardBuffer: Double = 0
48
+ private var automaticallyWaits: Bool = true
49
+ private var allowsExternalPlaybackEnabled: Bool = true
50
+ private var preventsDisplaySleep: Bool = true
51
+ private var maxBitRateValue: Int = 0
52
+
53
+ private var currentUri: String?
54
+ private var hasLoadedData: Bool = false
55
+ private var isSeekingFlag: Bool = false
56
+
57
+ // Combine subscriptions
58
+ private var cancellables = Set<AnyCancellable>()
59
+ private var itemCancellables = Set<AnyCancellable>()
60
+
61
+ // MARK: - Required Protocol Properties
62
+
63
+ @objc public var currentTime: Double {
64
+ guard playerItem != nil else { return 0 }
65
+ return CMTimeGetSeconds(player.currentTime())
66
+ }
67
+
68
+ @objc public var duration: Double {
69
+ guard let item = playerItem else { return 0 }
70
+ let duration = item.duration
71
+ if !duration.isValid || duration.isIndefinite {
72
+ return 0
73
+ }
74
+ return CMTimeGetSeconds(duration)
75
+ }
76
+
77
+ @objc public var isPlaying: Bool {
78
+ return player.rate != 0 && player.error == nil
79
+ }
80
+
81
+ // MARK: - Initialization
82
+
83
+ @objc public override init() {
84
+ player = AVPlayer()
85
+ playerLayer = AVPlayerLayer(player: player)
86
+ playerLayer.videoGravity = .resizeAspect
87
+
88
+ super.init()
89
+
90
+ player.allowsExternalPlayback = true
91
+
92
+ setupPlayerObservers()
93
+ setupNotificationObservers()
94
+ }
95
+
96
+ deinit {
97
+ unload()
98
+ cancellables.removeAll()
99
+ }
100
+
101
+ // MARK: - Combine Observers Setup
102
+
103
+ private func setupPlayerObservers() {
104
+ // Player rate changes (play/pause state)
105
+ player.publisher(for: \.rate)
106
+ .receive(on: DispatchQueue.main)
107
+ .sink { [weak self] rate in
108
+ guard let self = self else { return }
109
+ let isPlaying = rate != 0
110
+ self.delegate?.videoPlaybackStateChanged?(isPlaying: isPlaying, isSeeking: self.isSeekingFlag, isLooping: self.shouldRepeat)
111
+ self.delegate?.videoPlaybackRateChanged?(rate: rate)
112
+ }
113
+ .store(in: &cancellables)
114
+
115
+ // Player time control status (buffering state)
116
+ player.publisher(for: \.timeControlStatus)
117
+ .receive(on: DispatchQueue.main)
118
+ .sink { [weak self] status in
119
+ let isBuffering = (status == .waitingToPlayAtSpecifiedRate)
120
+ self?.delegate?.videoBufferingStateChanged?(isBuffering: isBuffering)
121
+ }
122
+ .store(in: &cancellables)
123
+ }
124
+
125
+ private func setupNotificationObservers() {
126
+ // App lifecycle notifications
127
+ NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)
128
+ .receive(on: DispatchQueue.main)
129
+ .sink { [weak self] _ in
130
+ self?.handleAppDidEnterBackground()
131
+ }
132
+ .store(in: &cancellables)
133
+
134
+ NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
135
+ .receive(on: DispatchQueue.main)
136
+ .sink { [weak self] _ in
137
+ self?.handleAppWillEnterForeground()
138
+ }
139
+ .store(in: &cancellables)
140
+
141
+ NotificationCenter.default.publisher(for: AVAudioSession.interruptionNotification)
142
+ .receive(on: DispatchQueue.main)
143
+ .sink { [weak self] _ in
144
+ self?.handleAudioSessionInterruption()
145
+ }
146
+ .store(in: &cancellables)
147
+
148
+ NotificationCenter.default.publisher(for: AVAudioSession.routeChangeNotification)
149
+ .receive(on: DispatchQueue.main)
150
+ .sink { [weak self] notification in
151
+ self?.handleAudioRouteChange(notification)
152
+ }
153
+ .store(in: &cancellables)
154
+ }
155
+
156
+ private func setupPlayerItemObservers(for item: AVPlayerItem) {
157
+ // Clear previous item observers
158
+ itemCancellables.removeAll()
159
+
160
+ // Player item status
161
+ item.publisher(for: \.status)
162
+ .receive(on: DispatchQueue.main)
163
+ .sink { [weak self] status in
164
+ self?.handlePlayerItemStatusChange(status)
165
+ }
166
+ .store(in: &itemCancellables)
167
+
168
+ // Playback end notification
169
+ NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime, object: item)
170
+ .receive(on: DispatchQueue.main)
171
+ .sink { [weak self] _ in
172
+ self?.handlePlayerItemDidPlayToEndTime()
173
+ }
174
+ .store(in: &itemCancellables)
175
+ }
176
+
177
+ // MARK: - GraniteVideoProvidable Required
178
+
179
+ @objc public func createPlayerView() -> UIView {
180
+ let view = AVPlayerContainerView()
181
+ view.backgroundColor = .black
182
+ view.playerLayer = playerLayer
183
+ view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
184
+ playerView = view
185
+ return view
186
+ }
187
+
188
+ @objc public func loadSource(_ source: GraniteVideoSource) {
189
+ guard let uri = source.uri, !uri.isEmpty else { return }
190
+
191
+ // Cleanup previous item
192
+ unloadPlayerItem()
193
+
194
+ currentUri = uri
195
+ hasLoadedData = false
196
+
197
+ // Notify load start
198
+ let isNetwork = uri.hasPrefix("http://") || uri.hasPrefix("https://")
199
+ let type = source.type ?? ""
200
+ delegate?.videoDidLoadStart?(isNetwork: isNetwork, type: type, uri: uri)
201
+
202
+ // Create URL
203
+ let url: URL?
204
+ if isNetwork {
205
+ url = URL(string: uri)
206
+ } else {
207
+ url = URL(fileURLWithPath: uri)
208
+ }
209
+
210
+ guard let validUrl = url else {
211
+ let errorData = GraniteVideoErrorData()
212
+ errorData.code = -1
213
+ errorData.domain = "GraniteVideo"
214
+ errorData.localizedDescription_ = "Invalid URL"
215
+ delegate?.videoDidFail?(error: errorData)
216
+ return
217
+ }
218
+
219
+ // Create player item
220
+ let asset = AVURLAsset(url: validUrl)
221
+ let item = AVPlayerItem(asset: asset)
222
+ playerItem = item
223
+
224
+ // Configure buffer
225
+ if preferredForwardBuffer > 0 {
226
+ item.preferredForwardBufferDuration = preferredForwardBuffer
227
+ }
228
+
229
+ if maxBitRateValue > 0 {
230
+ item.preferredPeakBitRate = Double(maxBitRateValue)
231
+ }
232
+
233
+ // Setup Combine observers for this item
234
+ setupPlayerItemObservers(for: item)
235
+
236
+ // Replace current item
237
+ player.replaceCurrentItem(with: item)
238
+
239
+ // Set start position if specified
240
+ if source.startPosition > 0 {
241
+ let seekTime = CMTime(seconds: source.startPosition, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
242
+ player.seek(to: seekTime)
243
+ }
244
+
245
+ // Add time observer for progress
246
+ let interval = CMTime(seconds: 0.25, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
247
+ timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] _ in
248
+ self?.handleProgressUpdate()
249
+ }
250
+
251
+ // Apply settings
252
+ player.isMuted = isMuted
253
+ player.volume = playerVolume
254
+ player.automaticallyWaitsToMinimizeStalling = automaticallyWaits
255
+ player.allowsExternalPlayback = allowsExternalPlaybackEnabled
256
+
257
+ // Setup PiP if available and enabled
258
+ if pipEnabled {
259
+ setupPictureInPicture()
260
+ }
261
+ }
262
+
263
+ @objc public func unload() {
264
+ unloadPlayerItem()
265
+ currentUri = nil
266
+ hasLoadedData = false
267
+ }
268
+
269
+ @objc public func play() {
270
+ player.rate = playerRate
271
+ }
272
+
273
+ @objc public func pause() {
274
+ player.pause()
275
+ }
276
+
277
+ @objc public func seek(to time: Double, toleranceBefore: Double, toleranceAfter: Double) {
278
+ guard playerItem != nil else { return }
279
+
280
+ isSeekingFlag = true
281
+
282
+ let seekTime = CMTime(seconds: time, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
283
+ let toleranceBeforeTime = CMTime(seconds: toleranceBefore, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
284
+ let toleranceAfterTime = CMTime(seconds: toleranceAfter, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
285
+
286
+ let currentTimeBeforeSeek = CMTimeGetSeconds(player.currentTime())
287
+
288
+ player.seek(to: seekTime, toleranceBefore: toleranceBeforeTime, toleranceAfter: toleranceAfterTime) { [weak self] finished in
289
+ DispatchQueue.main.async {
290
+ guard let self = self else { return }
291
+ self.isSeekingFlag = false
292
+ if finished {
293
+ self.delegate?.videoDidSeek?(currentTime: currentTimeBeforeSeek, seekTime: time)
294
+ }
295
+ }
296
+ }
297
+ }
298
+
299
+ // MARK: - GraniteVideoProvidable Optional
300
+
301
+ @objc public func setVolume(_ volume: Float) {
302
+ playerVolume = volume
303
+ player.volume = volume
304
+ }
305
+
306
+ @objc public func setMuted(_ muted: Bool) {
307
+ isMuted = muted
308
+ player.isMuted = muted
309
+ }
310
+
311
+ @objc public func setRate(_ rate: Float) {
312
+ playerRate = rate
313
+ if player.rate != 0 {
314
+ player.rate = rate
315
+ }
316
+ }
317
+
318
+ @objc public func setRepeat(_ shouldRepeat: Bool) {
319
+ self.shouldRepeat = shouldRepeat
320
+ }
321
+
322
+ @objc public func setResizeMode(_ mode: GraniteVideoResizeMode) {
323
+ switch mode {
324
+ case .contain:
325
+ playerLayer.videoGravity = .resizeAspect
326
+ case .cover:
327
+ playerLayer.videoGravity = .resizeAspectFill
328
+ case .stretch:
329
+ playerLayer.videoGravity = .resize
330
+ case .none:
331
+ playerLayer.videoGravity = .resizeAspect
332
+ @unknown default:
333
+ playerLayer.videoGravity = .resizeAspect
334
+ }
335
+ }
336
+
337
+ @objc public func setPlayInBackground(_ enabled: Bool) {
338
+ playInBackgroundEnabled = enabled
339
+ }
340
+
341
+ @objc public func setPlayWhenInactive(_ enabled: Bool) {
342
+ playWhenInactiveEnabled = enabled
343
+ }
344
+
345
+ @objc public func setPictureInPictureEnabled(_ enabled: Bool) {
346
+ pipEnabled = enabled
347
+ if enabled && playerItem != nil {
348
+ setupPictureInPicture()
349
+ }
350
+ }
351
+
352
+ @objc public func enterPictureInPicture() {
353
+ if let pipController = pipController, pipController.isPictureInPicturePossible {
354
+ pipController.startPictureInPicture()
355
+ }
356
+ }
357
+
358
+ @objc public func exitPictureInPicture() {
359
+ if let pipController = pipController, pipController.isPictureInPictureActive {
360
+ pipController.stopPictureInPicture()
361
+ }
362
+ }
363
+
364
+ @objc public func setFullscreen(_ fullscreen: Bool, animated: Bool) {
365
+ isFullscreen = fullscreen
366
+ }
367
+
368
+ @objc public func setMaxBitRate(_ bitRate: Int) {
369
+ maxBitRateValue = bitRate
370
+ playerItem?.preferredPeakBitRate = Double(bitRate)
371
+ }
372
+
373
+ @objc public func setPreferredForwardBufferDuration(_ duration: Double) {
374
+ preferredForwardBuffer = duration
375
+ playerItem?.preferredForwardBufferDuration = duration
376
+ }
377
+
378
+ @objc public func setAutomaticallyWaitsToMinimizeStalling(_ waits: Bool) {
379
+ automaticallyWaits = waits
380
+ player.automaticallyWaitsToMinimizeStalling = waits
381
+ }
382
+
383
+ @objc public func setAllowsExternalPlayback(_ allows: Bool) {
384
+ allowsExternalPlaybackEnabled = allows
385
+ player.allowsExternalPlayback = allows
386
+ }
387
+
388
+ @objc public func setPreventsDisplaySleepDuringVideoPlayback(_ prevents: Bool) {
389
+ preventsDisplaySleep = prevents
390
+ player.preventsDisplaySleepDuringVideoPlayback = prevents
391
+ }
392
+
393
+ @objc public func setControlsEnabled(_ enabled: Bool) {
394
+ // Native controls handled by AVPlayerViewController if needed
395
+ }
396
+
397
+ @objc public func onTransferEnd(uri: String, bytesTransferred: Int) {
398
+ delegate?.videoTransferEnd?(uri: uri, bytesTransferred: Double(bytesTransferred))
399
+ }
400
+
401
+ // MARK: - Private Methods
402
+
403
+ private func unloadPlayerItem() {
404
+ if let observer = timeObserver {
405
+ player.removeTimeObserver(observer)
406
+ timeObserver = nil
407
+ }
408
+
409
+ // Cancel all item-specific Combine subscriptions
410
+ itemCancellables.removeAll()
411
+ playerItem = nil
412
+
413
+ player.replaceCurrentItem(with: nil)
414
+ pipController = nil
415
+ }
416
+
417
+ private func setupPictureInPicture() {
418
+ if AVPictureInPictureController.isPictureInPictureSupported() {
419
+ pipController = AVPictureInPictureController(playerLayer: playerLayer)
420
+ pipController?.delegate = self
421
+ }
422
+ }
423
+
424
+ private func handleProgressUpdate() {
425
+ guard playerItem != nil, hasLoadedData else { return }
426
+
427
+ let currentTime = CMTimeGetSeconds(player.currentTime())
428
+ let duration = self.duration
429
+
430
+ var playableDuration: Double = 0
431
+ if let loadedRanges = playerItem?.loadedTimeRanges, let firstRange = loadedRanges.first {
432
+ let range = firstRange.timeRangeValue
433
+ let start = CMTimeGetSeconds(range.start)
434
+ let rangeDuration = CMTimeGetSeconds(range.duration)
435
+ playableDuration = start + rangeDuration
436
+ }
437
+
438
+ let progressData = GraniteVideoProgressData()
439
+ progressData.currentTime = currentTime
440
+ progressData.playableDuration = playableDuration
441
+ progressData.seekableDuration = duration
442
+
443
+ delegate?.videoDidUpdateProgress?(data: progressData)
444
+ }
445
+
446
+ // MARK: - Event Handlers
447
+
448
+ private func handlePlayerItemStatusChange(_ status: AVPlayerItem.Status) {
449
+ guard let item = playerItem else { return }
450
+
451
+ switch status {
452
+ case .readyToPlay:
453
+ if !hasLoadedData {
454
+ hasLoadedData = true
455
+
456
+ var naturalSize = CGSize.zero
457
+ if let tracks = item.asset.tracks(withMediaType: .video).first {
458
+ naturalSize = tracks.naturalSize
459
+ let transform = tracks.preferredTransform
460
+ if transform.a == 0 && transform.d == 0 {
461
+ naturalSize = CGSize(width: naturalSize.height, height: naturalSize.width)
462
+ }
463
+ }
464
+
465
+ let loadData = GraniteVideoLoadData()
466
+ loadData.currentTime = CMTimeGetSeconds(player.currentTime())
467
+ loadData.duration = duration
468
+ loadData.naturalWidth = naturalSize.width
469
+ loadData.naturalHeight = naturalSize.height
470
+ loadData.orientation = naturalSize.width >= naturalSize.height ? "landscape" : "portrait"
471
+
472
+ delegate?.videoDidLoad?(data: loadData)
473
+ delegate?.videoReadyForDisplay?()
474
+ }
475
+
476
+ case .failed:
477
+ let nsError = item.error ?? NSError(domain: "GraniteVideo", code: -1, userInfo: [NSLocalizedDescriptionKey: "Unknown error"])
478
+ let errorData = GraniteVideoErrorData(error: nsError)
479
+ delegate?.videoDidFail?(error: errorData)
480
+
481
+ default:
482
+ break
483
+ }
484
+ }
485
+
486
+ private func handlePlayerItemDidPlayToEndTime() {
487
+ delegate?.videoDidEnd?()
488
+
489
+ if shouldRepeat {
490
+ player.seek(to: .zero)
491
+ player.play()
492
+ }
493
+ }
494
+
495
+ private func handleAppDidEnterBackground() {
496
+ if !playInBackgroundEnabled {
497
+ playerLayer.player = nil
498
+ }
499
+ }
500
+
501
+ private func handleAppWillEnterForeground() {
502
+ if playerLayer.player == nil {
503
+ playerLayer.player = player
504
+ }
505
+ }
506
+
507
+ private func handleAudioSessionInterruption() {
508
+ // Handle audio interruption
509
+ }
510
+
511
+ private func handleAudioRouteChange(_ notification: NotificationCenter.Publisher.Output) {
512
+ guard let userInfo = notification.userInfo,
513
+ let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
514
+ let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
515
+ return
516
+ }
517
+
518
+ if reason == .oldDeviceUnavailable {
519
+ delegate?.videoAudioBecomingNoisy?()
520
+ player.pause()
521
+ }
522
+ }
523
+ }
524
+
525
+ // MARK: - AVPictureInPictureControllerDelegate
526
+
527
+ extension AVPlayerProvider: AVPictureInPictureControllerDelegate {
528
+ public func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
529
+ delegate?.videoPictureInPictureStatusChanged?(isActive: true)
530
+ }
531
+
532
+ public func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
533
+ delegate?.videoPictureInPictureStatusChanged?(isActive: false)
534
+ }
535
+
536
+ public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController,
537
+ restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
538
+ delegate?.videoRestoreUserInterfaceForPictureInPictureStop?()
539
+ completionHandler(true)
540
+ }
541
+ }
package/package.json ADDED
@@ -0,0 +1,106 @@
1
+ {
2
+ "name": "@granite-js/video",
3
+ "version": "1.0.0",
4
+ "description": "Pluggable video component for React Native with provider selection",
5
+ "type": "module",
6
+ "main": "./dist/module/index.js",
7
+ "types": "./dist/typescript/index.d.ts",
8
+ "react-native": "./src/index.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/typescript/index.d.ts",
12
+ "default": "./dist/module/index.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "src",
18
+ "dist",
19
+ "android",
20
+ "ios",
21
+ "*.podspec",
22
+ "react-native.config.js",
23
+ "!example/**",
24
+ "!ios/build",
25
+ "!android/build",
26
+ "!android/gradle",
27
+ "!android/gradlew",
28
+ "!android/gradlew.bat",
29
+ "!android/local.properties",
30
+ "!**/__tests__",
31
+ "!**/__fixtures__",
32
+ "!**/__mocks__",
33
+ "!**/.*"
34
+ ],
35
+ "scripts": {
36
+ "prepack": "yarn build",
37
+ "example": "yarn workspace @granite-js/video-example",
38
+ "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build",
39
+ "typecheck": "tsc --noEmit",
40
+ "lint": "eslint .",
41
+ "build": "bob build && tsc --project tsconfig.build.json"
42
+ },
43
+ "keywords": [
44
+ "react-native",
45
+ "ios",
46
+ "android",
47
+ "video",
48
+ "player",
49
+ "exoplayer",
50
+ "avplayer",
51
+ "vlc",
52
+ "streaming",
53
+ "hls",
54
+ "dash",
55
+ "drm",
56
+ "pluggable",
57
+ "provider"
58
+ ],
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "git+https://github.com/toss/granite.git",
62
+ "directory": "packages/video"
63
+ },
64
+ "author": "Toss <platform@toss.im>",
65
+ "homepage": "https://github.com/toss/granite/tree/main/packages/video#readme",
66
+ "license": "MIT",
67
+ "devDependencies": {
68
+ "@types/react": "19.2.0",
69
+ "del-cli": "^6.0.0",
70
+ "eslint": "^9.7.0",
71
+ "react": "19.2.3",
72
+ "react-native": "0.84.0-rc.5",
73
+ "react-native-builder-bob": "0.40.17",
74
+ "typescript": "5.9.3"
75
+ },
76
+ "peerDependencies": {
77
+ "react": "*",
78
+ "react-native": "*"
79
+ },
80
+ "codegenConfig": {
81
+ "name": "GraniteVideoViewSpec",
82
+ "type": "all",
83
+ "jsSrcsDir": "src",
84
+ "android": {
85
+ "javaPackageName": "run.granite.video"
86
+ },
87
+ "ios": {
88
+ "componentProvider": {
89
+ "GraniteVideoView": "GraniteVideoView"
90
+ }
91
+ }
92
+ },
93
+ "sideEffects": false,
94
+ "react-native-builder-bob": {
95
+ "source": "src",
96
+ "output": "dist",
97
+ "targets": [
98
+ [
99
+ "module",
100
+ {
101
+ "esm": true
102
+ }
103
+ ]
104
+ ]
105
+ }
106
+ }