@stepincto/expo-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 (180) hide show
  1. package/README.md +45 -0
  2. package/android/build.gradle +32 -0
  3. package/android/src/main/AndroidManifest.xml +20 -0
  4. package/android/src/main/java/expo/modules/video/AudioFocusManager.kt +241 -0
  5. package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +145 -0
  6. package/android/src/main/java/expo/modules/video/IntervalUpdateClock.kt +54 -0
  7. package/android/src/main/java/expo/modules/video/MediaMetadataRetriever.kt +89 -0
  8. package/android/src/main/java/expo/modules/video/PictureInPictureHelperFragment.kt +26 -0
  9. package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +36 -0
  10. package/android/src/main/java/expo/modules/video/VideoCache.kt +104 -0
  11. package/android/src/main/java/expo/modules/video/VideoExceptions.kt +34 -0
  12. package/android/src/main/java/expo/modules/video/VideoManager.kt +133 -0
  13. package/android/src/main/java/expo/modules/video/VideoModule.kt +414 -0
  14. package/android/src/main/java/expo/modules/video/VideoThumbnail.kt +20 -0
  15. package/android/src/main/java/expo/modules/video/VideoView.kt +367 -0
  16. package/android/src/main/java/expo/modules/video/delegates/IgnoreSameSet.kt +24 -0
  17. package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +217 -0
  18. package/android/src/main/java/expo/modules/video/enums/AudioMixingMode.kt +20 -0
  19. package/android/src/main/java/expo/modules/video/enums/ContentFit.kt +19 -0
  20. package/android/src/main/java/expo/modules/video/enums/ContentType.kt +22 -0
  21. package/android/src/main/java/expo/modules/video/enums/DRMType.kt +26 -0
  22. package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
  23. package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +184 -0
  24. package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +39 -0
  25. package/android/src/main/java/expo/modules/video/playbackService/VideoMediaSessionCallback.kt +47 -0
  26. package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +93 -0
  27. package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +164 -0
  28. package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +460 -0
  29. package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +125 -0
  30. package/android/src/main/java/expo/modules/video/player/VideoPlayerListener.kt +32 -0
  31. package/android/src/main/java/expo/modules/video/player/VideoPlayerLoadControl.kt +525 -0
  32. package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +125 -0
  33. package/android/src/main/java/expo/modules/video/records/BufferOptions.kt +15 -0
  34. package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +25 -0
  35. package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +19 -0
  36. package/android/src/main/java/expo/modules/video/records/Tracks.kt +81 -0
  37. package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +79 -0
  38. package/android/src/main/java/expo/modules/video/records/VideoMetadata.kt +12 -0
  39. package/android/src/main/java/expo/modules/video/records/VideoSize.kt +14 -0
  40. package/android/src/main/java/expo/modules/video/records/VideoSource.kt +104 -0
  41. package/android/src/main/java/expo/modules/video/records/VideoThumbnailOptions.kt +24 -0
  42. package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +75 -0
  43. package/android/src/main/java/expo/modules/video/utils/EventDispatcherUtils.kt +43 -0
  44. package/android/src/main/java/expo/modules/video/utils/MutableWeakReference.kt +15 -0
  45. package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +96 -0
  46. package/android/src/main/java/expo/modules/video/utils/YogaUtils.kt +20 -0
  47. package/android/src/main/res/drawable/seek_backwards_10s.xml +25 -0
  48. package/android/src/main/res/drawable/seek_backwards_15s.xml +25 -0
  49. package/android/src/main/res/drawable/seek_backwards_5s.xml +25 -0
  50. package/android/src/main/res/drawable/seek_forwards_10s.xml +30 -0
  51. package/android/src/main/res/drawable/seek_forwards_15s.xml +31 -0
  52. package/android/src/main/res/drawable/seek_forwards_5s.xml +30 -0
  53. package/android/src/main/res/layout/fullscreen_player_activity.xml +16 -0
  54. package/android/src/main/res/layout/surface_player_view.xml +7 -0
  55. package/android/src/main/res/layout/texture_player_view.xml +7 -0
  56. package/android/src/main/res/values/styles.xml +9 -0
  57. package/app.plugin.js +1 -0
  58. package/build/NativeVideoModule.d.ts +16 -0
  59. package/build/NativeVideoModule.d.ts.map +1 -0
  60. package/build/NativeVideoModule.js +3 -0
  61. package/build/NativeVideoModule.js.map +1 -0
  62. package/build/NativeVideoModule.web.d.ts +3 -0
  63. package/build/NativeVideoModule.web.d.ts.map +1 -0
  64. package/build/NativeVideoModule.web.js +2 -0
  65. package/build/NativeVideoModule.web.js.map +1 -0
  66. package/build/NativeVideoView.d.ts +4 -0
  67. package/build/NativeVideoView.d.ts.map +1 -0
  68. package/build/NativeVideoView.js +6 -0
  69. package/build/NativeVideoView.js.map +1 -0
  70. package/build/VideoModule.d.ts +38 -0
  71. package/build/VideoModule.d.ts.map +1 -0
  72. package/build/VideoModule.js +53 -0
  73. package/build/VideoModule.js.map +1 -0
  74. package/build/VideoPlayer.d.ts +15 -0
  75. package/build/VideoPlayer.d.ts.map +1 -0
  76. package/build/VideoPlayer.js +52 -0
  77. package/build/VideoPlayer.js.map +1 -0
  78. package/build/VideoPlayer.types.d.ts +532 -0
  79. package/build/VideoPlayer.types.d.ts.map +1 -0
  80. package/build/VideoPlayer.types.js +2 -0
  81. package/build/VideoPlayer.types.js.map +1 -0
  82. package/build/VideoPlayer.web.d.ts +75 -0
  83. package/build/VideoPlayer.web.d.ts.map +1 -0
  84. package/build/VideoPlayer.web.js +376 -0
  85. package/build/VideoPlayer.web.js.map +1 -0
  86. package/build/VideoPlayerEvents.types.d.ts +262 -0
  87. package/build/VideoPlayerEvents.types.d.ts.map +1 -0
  88. package/build/VideoPlayerEvents.types.js +2 -0
  89. package/build/VideoPlayerEvents.types.js.map +1 -0
  90. package/build/VideoThumbnail.d.ts +29 -0
  91. package/build/VideoThumbnail.d.ts.map +1 -0
  92. package/build/VideoThumbnail.js +3 -0
  93. package/build/VideoThumbnail.js.map +1 -0
  94. package/build/VideoView.d.ts +44 -0
  95. package/build/VideoView.d.ts.map +1 -0
  96. package/build/VideoView.js +76 -0
  97. package/build/VideoView.js.map +1 -0
  98. package/build/VideoView.types.d.ts +147 -0
  99. package/build/VideoView.types.d.ts.map +1 -0
  100. package/build/VideoView.types.js +2 -0
  101. package/build/VideoView.types.js.map +1 -0
  102. package/build/VideoView.web.d.ts +9 -0
  103. package/build/VideoView.web.d.ts.map +1 -0
  104. package/build/VideoView.web.js +180 -0
  105. package/build/VideoView.web.js.map +1 -0
  106. package/build/index.d.ts +9 -0
  107. package/build/index.d.ts.map +1 -0
  108. package/build/index.js +7 -0
  109. package/build/index.js.map +1 -0
  110. package/build/resolveAssetSource.d.ts +3 -0
  111. package/build/resolveAssetSource.d.ts.map +1 -0
  112. package/build/resolveAssetSource.js +3 -0
  113. package/build/resolveAssetSource.js.map +1 -0
  114. package/build/resolveAssetSource.web.d.ts +4 -0
  115. package/build/resolveAssetSource.web.d.ts.map +1 -0
  116. package/build/resolveAssetSource.web.js +16 -0
  117. package/build/resolveAssetSource.web.js.map +1 -0
  118. package/expo-module.config.json +9 -0
  119. package/ios/Cache/CachableRequest.swift +44 -0
  120. package/ios/Cache/CachedResource.swift +97 -0
  121. package/ios/Cache/CachingHelpers.swift +92 -0
  122. package/ios/Cache/MediaFileHandle.swift +94 -0
  123. package/ios/Cache/MediaInfo.swift +147 -0
  124. package/ios/Cache/ResourceLoaderDelegate.swift +274 -0
  125. package/ios/Cache/SynchronizedHashTable.swift +23 -0
  126. package/ios/Cache/VideoCacheManager.swift +338 -0
  127. package/ios/ContentKeyDelegate.swift +214 -0
  128. package/ios/ContentKeyManager.swift +21 -0
  129. package/ios/Enums/AudioMixingMode.swift +37 -0
  130. package/ios/Enums/ContentType.swift +12 -0
  131. package/ios/Enums/DRMType.swift +20 -0
  132. package/ios/Enums/PlayerStatus.swift +10 -0
  133. package/ios/Enums/VideoContentFit.swift +39 -0
  134. package/ios/ExpoVideo.podspec +29 -0
  135. package/ios/NowPlayingManager.swift +296 -0
  136. package/ios/Records/BufferOptions.swift +12 -0
  137. package/ios/Records/DRMOptions.swift +24 -0
  138. package/ios/Records/PlaybackError.swift +10 -0
  139. package/ios/Records/Tracks.swift +176 -0
  140. package/ios/Records/VideoEventPayloads.swift +76 -0
  141. package/ios/Records/VideoMetadata.swift +16 -0
  142. package/ios/Records/VideoSize.swift +15 -0
  143. package/ios/Records/VideoSource.swift +25 -0
  144. package/ios/Thumbnails/VideoThumbnail.swift +27 -0
  145. package/ios/Thumbnails/VideoThumbnailGenerator.swift +68 -0
  146. package/ios/Thumbnails/VideoThumbnailOptions.swift +15 -0
  147. package/ios/VideoAsset.swift +123 -0
  148. package/ios/VideoExceptions.swift +53 -0
  149. package/ios/VideoItem.swift +11 -0
  150. package/ios/VideoManager.swift +140 -0
  151. package/ios/VideoModule.swift +383 -0
  152. package/ios/VideoPlayer/DangerousPropertiesStore.swift +19 -0
  153. package/ios/VideoPlayer.swift +435 -0
  154. package/ios/VideoPlayerAudioTracks.swift +72 -0
  155. package/ios/VideoPlayerItem.swift +97 -0
  156. package/ios/VideoPlayerObserver.swift +523 -0
  157. package/ios/VideoPlayerSubtitles.swift +71 -0
  158. package/ios/VideoSourceLoader.swift +89 -0
  159. package/ios/VideoSourceLoaderListener.swift +34 -0
  160. package/ios/VideoView.swift +224 -0
  161. package/package.json +59 -0
  162. package/plugin/build/tsconfig.tsbuildinfo +1 -0
  163. package/plugin/build/withExpoVideo.d.ts +7 -0
  164. package/plugin/build/withExpoVideo.js +38 -0
  165. package/src/NativeVideoModule.ts +20 -0
  166. package/src/NativeVideoModule.web.ts +1 -0
  167. package/src/NativeVideoView.ts +8 -0
  168. package/src/VideoModule.ts +59 -0
  169. package/src/VideoPlayer.tsx +67 -0
  170. package/src/VideoPlayer.types.ts +613 -0
  171. package/src/VideoPlayer.web.tsx +451 -0
  172. package/src/VideoPlayerEvents.types.ts +313 -0
  173. package/src/VideoThumbnail.ts +31 -0
  174. package/src/VideoView.tsx +86 -0
  175. package/src/VideoView.types.ts +165 -0
  176. package/src/VideoView.web.tsx +214 -0
  177. package/src/index.ts +46 -0
  178. package/src/resolveAssetSource.ts +2 -0
  179. package/src/resolveAssetSource.web.ts +17 -0
  180. package/src/ts-declarations/react-native-assets.d.ts +1 -0
@@ -0,0 +1,27 @@
1
+ // Copyright 2024-present 650 Industries. All rights reserved.
2
+
3
+ import CoreImage
4
+ import CoreGraphics
5
+ import ExpoModulesCore
6
+
7
+ internal final class VideoThumbnail: SharedRef<UIImage> {
8
+ internal override var nativeRefType: String {
9
+ "image"
10
+ }
11
+
12
+ var requestedTime: CMTime
13
+ var actualTime: CMTime
14
+
15
+ internal init(_ image: CGImage, requestedTime: CMTime, actualTime: CMTime) {
16
+ self.requestedTime = requestedTime
17
+ self.actualTime = actualTime
18
+ super.init(UIImage(cgImage: image))
19
+ }
20
+
21
+ override func getAdditionalMemoryPressure() -> Int {
22
+ guard let cgImage = ref.cgImage else {
23
+ return 0
24
+ }
25
+ return cgImage.bytesPerRow * cgImage.height
26
+ }
27
+ }
@@ -0,0 +1,68 @@
1
+ // Copyright 2024-present 650 Industries. All rights reserved.
2
+
3
+ import AVFoundation
4
+ import CoreMedia
5
+
6
+ /**
7
+ Generates an array of thumbnails from the given assets and options.
8
+ */
9
+ internal func generateThumbnails(asset: AVAsset, times: [CMTime], options: VideoThumbnailOptions?) async throws -> [VideoThumbnail] {
10
+ let generator = AVAssetImageGenerator(asset: asset)
11
+
12
+ generator.appliesPreferredTrackTransform = true
13
+ generator.requestedTimeToleranceAfter = .zero
14
+ generator.maximumSize = options?.getMaxSize() ?? .zero
15
+
16
+ // `requestedTimeToleranceBefore` can only be set if times are less
17
+ // than the video duration, otherwise it will fail to generate an image.
18
+ if times.allSatisfy({ $0 < asset.duration }) {
19
+ generator.requestedTimeToleranceBefore = .zero
20
+ }
21
+ return try await generateThumbnails(generator: generator, times: times)
22
+ }
23
+
24
+ /**
25
+ Generates an array of thumbnails using the given image generator. It uses two different ways to generate the images, based on the platform version.
26
+ */
27
+ private func generateThumbnails(generator: AVAssetImageGenerator, times: [CMTime]) async throws -> [VideoThumbnail] {
28
+ if #available(iOS 16, tvOS 16, *) {
29
+ return try await generator
30
+ .images(for: times)
31
+ .reduce(into: [VideoThumbnail]()) { thumbnails, result in
32
+ let thumbnail = try VideoThumbnail(result.image, requestedTime: result.requestedTime, actualTime: result.actualTime)
33
+ thumbnails.append(thumbnail)
34
+ }
35
+ }
36
+ return try await VideoThumbnailLegacyGenerator(generator: generator, times: times)
37
+ .reduce(into: [VideoThumbnail]()) { thumbnails, thumbnail in
38
+ thumbnails.append(thumbnail)
39
+ }
40
+ }
41
+
42
+ /**
43
+ A replacement for the `AVAssetImageGenerator.images(for:)` async iterator that is available only as of iOS 16.
44
+ */
45
+ internal struct VideoThumbnailLegacyGenerator: AsyncSequence, AsyncIteratorProtocol {
46
+ typealias Element = VideoThumbnail
47
+
48
+ let generator: AVAssetImageGenerator
49
+ let times: [CMTime]
50
+ var currentIndex: Int = 0
51
+
52
+ mutating func next() async throws -> Element? {
53
+ guard currentIndex < times.count, !Task.isCancelled else {
54
+ return nil
55
+ }
56
+ let requestedTime = times[currentIndex]
57
+ var actualTime = CMTime.zero
58
+ let image = try generator.copyCGImage(at: requestedTime, actualTime: &actualTime)
59
+
60
+ currentIndex += 1
61
+
62
+ return VideoThumbnail(image, requestedTime: requestedTime, actualTime: actualTime)
63
+ }
64
+
65
+ func makeAsyncIterator() -> Self {
66
+ return self
67
+ }
68
+ }
@@ -0,0 +1,15 @@
1
+ import ExpoModulesCore
2
+
3
+ /**
4
+ Record representing thumbnail generation options.
5
+ */
6
+ internal struct VideoThumbnailOptions: Record {
7
+ static let `default` = VideoThumbnailOptions()
8
+
9
+ @Field var maxWidth: Int = 0
10
+ @Field var maxHeight: Int = 0
11
+
12
+ func getMaxSize() -> CGSize {
13
+ return CGSize(width: maxWidth, height: maxHeight)
14
+ }
15
+ }
@@ -0,0 +1,123 @@
1
+ import Foundation
2
+ import AVFoundation
3
+ import CryptoKit
4
+ import MobileCoreServices
5
+ import ExpoModulesCore
6
+
7
+ internal class VideoAsset: AVURLAsset, @unchecked Sendable {
8
+ internal let videoSource: VideoSource
9
+ private var resourceLoaderDelegate: ResourceLoaderDelegate?
10
+ private let initialScheme: String?
11
+ private let saveFilePath: String?
12
+ private var customFileExtension: String?
13
+ private let useCaching: Bool
14
+
15
+ var cachingError: Error?
16
+
17
+ internal var urlRequestHeaders: [String: String]?
18
+
19
+ init(url: URL, videoSource: VideoSource) {
20
+ self.videoSource = videoSource
21
+ let cachedMimeType = MediaInfo(forResourceUrl: url)?.mimeType
22
+ let cachedExtension = mimeTypeToExtension(mimeType: cachedMimeType) ?? ""
23
+ let fileExtension = url.pathExtension.isEmpty ? cachedExtension : url.pathExtension
24
+ self.saveFilePath = Self.pathForUrl(url: url, fileExtension: fileExtension)
25
+ self.urlRequestHeaders = videoSource.headers
26
+ self.initialScheme = URLComponents(url: url, resolvingAgainstBaseURL: false)?.scheme
27
+
28
+ // Creates an URL that will delegate it's requests to ResourceLoaderDelegate
29
+ let urlWithCustomScheme = url.withScheme(VideoCacheManager.expoVideoCacheScheme)
30
+
31
+ let assetOptions: [String: Any]? = if let headers = videoSource.headers {
32
+ ["AVURLAssetHTTPHeaderFieldsKey": headers]
33
+ } else {
34
+ nil
35
+ }
36
+
37
+ let canCache = Self.canCache(videoSource: videoSource)
38
+
39
+ if saveFilePath == nil && videoSource.useCaching {
40
+ log.warn("Failed to create a cache file path for the provided source with uri: \(videoSource.uri?.absoluteString ?? "null")")
41
+ }
42
+
43
+ if !canCache && videoSource.useCaching {
44
+ log.warn("Provided source with uri: \(videoSource.uri?.absoluteString ?? "null") cannot be cached. Caching will be disabled")
45
+ }
46
+
47
+ if urlWithCustomScheme == nil && videoSource.useCaching {
48
+ log.warn("CachingPlayerItem error: Urls without a scheme are not supported, the resource won't be cached")
49
+ }
50
+
51
+ guard let saveFilePath, let urlWithCustomScheme, videoSource.useCaching else {
52
+ // Initialize with no caching
53
+ useCaching = false
54
+ super.init(url: url, options: assetOptions)
55
+ return
56
+ }
57
+
58
+ // Enable caching
59
+ useCaching = true
60
+ resourceLoaderDelegate = ResourceLoaderDelegate(url: url, saveFilePath: saveFilePath, fileExtension: fileExtension, urlRequestHeaders: urlRequestHeaders)
61
+ super.init(url: urlWithCustomScheme, options: assetOptions)
62
+
63
+ resourceLoaderDelegate?.onError = { [weak self] error in
64
+ self?.cachingError = error
65
+ }
66
+ self.resourceLoader.setDelegate(resourceLoaderDelegate, queue: VideoCacheManager.shared.cacheQueue)
67
+ self.createCacheDirectoryIfNeeded()
68
+ VideoCacheManager.shared.ensureCacheIntegrity(forSavePath: saveFilePath)
69
+ }
70
+
71
+ deinit {
72
+ guard useCaching else {
73
+ return
74
+ }
75
+ if let saveFilePath, let cachedFileUrl = URL(string: saveFilePath) {
76
+ VideoCacheManager.shared.unregisterOpenFile(at: cachedFileUrl)
77
+ }
78
+ VideoCacheManager.shared.ensureCacheSize()
79
+ }
80
+
81
+ static func pathForUrl(url: URL, fileExtension: String) -> String? {
82
+ let hashedData = SHA256.hash(data: Data(url.absoluteString.utf8))
83
+ let hashString = hashedData.compactMap { String(format: "%02x", $0) }.joined()
84
+ let parsedExtension = fileExtension.starts(with: ".") || fileExtension.isEmpty ? fileExtension : ("." + fileExtension)
85
+ let hashFilename = hashString + parsedExtension
86
+
87
+ guard var cachesDirectory = try? FileManager.default.url(
88
+ for: .cachesDirectory,
89
+ in: .userDomainMask,
90
+ appropriateFor: nil,
91
+ create: true)
92
+ else {
93
+ log.warn("CachingPlayerItem error: Can't access default cache directory")
94
+ return nil
95
+ }
96
+
97
+ cachesDirectory.appendPathComponent(VideoCacheManager.expoVideoCacheScheme, isDirectory: true)
98
+ cachesDirectory.appendPathComponent(hashFilename)
99
+
100
+ return cachesDirectory.path
101
+ }
102
+
103
+ static func canCache(videoSource: VideoSource) -> Bool {
104
+ guard videoSource.uri?.scheme?.starts(with: "http") == true else {
105
+ return false
106
+ }
107
+ return videoSource.drm == nil
108
+ }
109
+
110
+ private func createCacheDirectoryIfNeeded() {
111
+ guard var cachesDirectory = try? FileManager.default.url(
112
+ for: .cachesDirectory,
113
+ in: .userDomainMask,
114
+ appropriateFor: nil,
115
+ create: true)
116
+ else {
117
+ return
118
+ }
119
+
120
+ cachesDirectory.appendPathComponent(VideoCacheManager.expoVideoCacheScheme, isDirectory: true)
121
+ try? FileManager.default.createDirectory(at: cachesDirectory, withIntermediateDirectories: true)
122
+ }
123
+ }
@@ -0,0 +1,53 @@
1
+ // Copyright 2023-present 650 Industries. All rights reserved.
2
+
3
+ import ExpoModulesCore
4
+
5
+ private let defaultCause = "unknown cause"
6
+
7
+ internal final class PictureInPictureUnsupportedException: Exception {
8
+ override var reason: String {
9
+ "Picture in picture is not supported on this device"
10
+ }
11
+ }
12
+
13
+ internal final class DRMUnsupportedException: GenericException<DRMType> {
14
+ override var reason: String {
15
+ "DRMType: `\(param)` is unsupported on iOS"
16
+ }
17
+ }
18
+
19
+ internal final class DRMLoadException: GenericException<String?> {
20
+ override var reason: String {
21
+ "Failed to decrypt the video stream: \(param ?? defaultCause)"
22
+ }
23
+ }
24
+
25
+ internal final class PlayerException: GenericException<String?> {
26
+ override var reason: String {
27
+ "Failed to initialise the player: \(param ?? defaultCause)"
28
+ }
29
+ }
30
+
31
+ internal final class PlayerItemLoadException: GenericException<String?> {
32
+ override var reason: String {
33
+ "Failed to load the player item: \(param ?? defaultCause)"
34
+ }
35
+ }
36
+
37
+ internal final class CachingAssetInitializationException: GenericException<URL?> {
38
+ override var reason: String {
39
+ "Failed to initialize a caching asset. The provided url: \(param?.absoluteString ?? "nil") doesn't have a valid scheme for caching"
40
+ }
41
+ }
42
+
43
+ internal final class VideoCacheException: GenericException<String?> {
44
+ override var reason: String {
45
+ param ?? "Unexpected expo-video cache error"
46
+ }
47
+ }
48
+
49
+ internal final class VideoCacheUnsupportedFormatException: GenericException<String> {
50
+ override var reason: String {
51
+ "The server responded with a resource with mimeType: \(param) which cannot be played with caching enabled"
52
+ }
53
+ }
@@ -0,0 +1,11 @@
1
+ // Copyright 2023-present 650 Industries. All rights reserved.
2
+
3
+ import ExpoModulesCore
4
+
5
+ internal final class VideoItem: SharedObject {
6
+ let playerItem: AVPlayerItem
7
+
8
+ init(url: URL) {
9
+ playerItem = AVPlayerItem(url: url)
10
+ }
11
+ }
@@ -0,0 +1,140 @@
1
+ // Copyright 2024-present 650 Industries. All rights reserved.
2
+
3
+ import AVKit
4
+ import Foundation
5
+ import ExpoModulesCore
6
+
7
+ /**
8
+ * Helper class used to keep track of all existing VideoViews and VideoPlayers and manage their lifecycle
9
+ */
10
+ class VideoManager {
11
+ static var shared = VideoManager()
12
+
13
+ private static var managerQueue = DispatchQueue(label: "com.expo.video.manager.managerQueue")
14
+ private var videoViews = NSHashTable<VideoView>.weakObjects()
15
+ private var videoPlayers = NSHashTable<VideoPlayer>.weakObjects()
16
+
17
+ var hasRegisteredPlayers: Bool {
18
+ return !videoPlayers.allObjects.isEmpty
19
+ }
20
+
21
+ func register(videoPlayer: VideoPlayer) {
22
+ Self.managerQueue.async { [weak self, weak videoPlayer] in
23
+ guard let self = self, let videoPlayer = videoPlayer else {
24
+ return
25
+ }
26
+ self.videoPlayers.add(videoPlayer)
27
+ }
28
+ }
29
+
30
+ func unregister(videoPlayer: VideoPlayer) {
31
+ Self.managerQueue.async { [weak self, weak videoPlayer] in
32
+ guard let self = self, let videoPlayer = videoPlayer else {
33
+ return
34
+ }
35
+ self.videoPlayers.remove(videoPlayer)
36
+ }
37
+ }
38
+
39
+ func register(videoView: VideoView) {
40
+ videoViews.add(videoView)
41
+ }
42
+
43
+ func unregister(videoView: VideoView) {
44
+ videoViews.remove(videoView)
45
+ }
46
+
47
+ func onAppForegrounded() {
48
+ for videoPlayer in videoPlayers.allObjects {
49
+ videoPlayer.setTracksEnabled(true)
50
+ }
51
+ }
52
+
53
+ func onAppBackgrounded() {
54
+ for videoView in videoViews.allObjects {
55
+ guard let player = videoView.player else {
56
+ continue
57
+ }
58
+ if player.staysActiveInBackground == true {
59
+ player.setTracksEnabled(videoView.isInPictureInPicture)
60
+ } else if !videoView.isInPictureInPicture {
61
+ player.ref.pause()
62
+ }
63
+ }
64
+ }
65
+
66
+ // MARK: - Audio Session Management
67
+
68
+ // This function usually takes less than 5ms to execute, but in some cases (initial setup) it takes up to 70ms
69
+ // Because of this we dispatch it on another queue to minimize the load on main queue.
70
+ internal func setAppropriateAudioSessionOrWarn() {
71
+ Self.managerQueue.async { [weak self] in
72
+ self?.setAudioSession()
73
+ }
74
+ }
75
+
76
+ private func setAudioSession() {
77
+ let audioSession = AVAudioSession.sharedInstance()
78
+ let audioMixingMode = findAudioMixingMode()
79
+ var audioSessionCategoryOptions: AVAudioSession.CategoryOptions = audioSession.categoryOptions
80
+
81
+ let isOutputtingAudio = videoPlayers.allObjects.contains { player in
82
+ player.isPlaying && !player.isMuted
83
+ }
84
+ let anyPlayerShowsNotification = videoPlayers.allObjects.contains { player in
85
+ player.showNowPlayingNotification
86
+ }
87
+
88
+ let shouldMixOverride = audioMixingMode == .mixWithOthers
89
+ let doNotMixOverride = audioMixingMode == .doNotMix
90
+ let shouldDuckOthers = audioMixingMode == .duckOthers && isOutputtingAudio
91
+
92
+ // The now playing notification won't be shown if we allow the audio to mix with others
93
+ let autoShouldMix = !isOutputtingAudio && !anyPlayerShowsNotification
94
+ let shouldMixWithOthers = shouldMixOverride || autoShouldMix
95
+
96
+ if shouldMixWithOthers && !shouldDuckOthers && !doNotMixOverride {
97
+ audioSessionCategoryOptions.insert(.mixWithOthers)
98
+ } else {
99
+ audioSessionCategoryOptions.remove(.mixWithOthers)
100
+ }
101
+
102
+ if shouldDuckOthers && !doNotMixOverride {
103
+ audioSessionCategoryOptions.insert(.duckOthers)
104
+ } else {
105
+ audioSessionCategoryOptions.remove(.duckOthers)
106
+ }
107
+
108
+ if audioSession.categoryOptions != audioSessionCategoryOptions || audioSession.category != .playback || audioSession.mode != .moviePlayback {
109
+ do {
110
+ try audioSession.setCategory(.playback, mode: .moviePlayback, options: audioSessionCategoryOptions)
111
+ } catch {
112
+ log.warn("Failed to set audio session category. This might cause issues with audio playback and Picture in Picture. \(error.localizedDescription)")
113
+ }
114
+ }
115
+
116
+ // Make sure audio session is active if any video is playing
117
+ if isOutputtingAudio || doNotMixOverride {
118
+ do {
119
+ try audioSession.setActive(true)
120
+ } catch {
121
+ log.warn("Failed to activate the audio session. This might cause issues with audio playback. \(error.localizedDescription)")
122
+ }
123
+ }
124
+ }
125
+
126
+ private func findAudioMixingMode() -> AudioMixingMode? {
127
+ let playingPlayers = videoPlayers.allObjects.filter({ player in
128
+ player.isPlaying
129
+ })
130
+ var audioMixingMode: AudioMixingMode = .mixWithOthers
131
+
132
+ if playingPlayers.isEmpty {
133
+ return nil
134
+ }
135
+ for videoPlayer in playingPlayers where (audioMixingMode.priority()) < videoPlayer.audioMixingMode.priority() {
136
+ audioMixingMode = videoPlayer.audioMixingMode
137
+ }
138
+ return audioMixingMode
139
+ }
140
+ }