@stepincto/expo-video 2.2.2-sicto.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 +41 -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 +13 -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 +35 -0
  71. package/build/VideoModule.d.ts.map +1 -0
  72. package/build/VideoModule.js +44 -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 +46 -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 +142 -0
  124. package/ios/Cache/ResourceLoaderDelegate.swift +274 -0
  125. package/ios/Cache/SynchronizedHashTable.swift +23 -0
  126. package/ios/Cache/VideoCacheManager.swift +192 -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 +357 -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 +58 -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 +16 -0
  166. package/src/NativeVideoModule.web.ts +1 -0
  167. package/src/NativeVideoView.ts +8 -0
  168. package/src/VideoModule.ts +47 -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 +43 -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,24 @@
1
+ // Copyright 2024-present 650 Industries. All rights reserved.
2
+
3
+ import Foundation
4
+ import ExpoModulesCore
5
+
6
+ internal struct DRMOptions: Record {
7
+ @Field
8
+ var type: DRMType = .fairplay
9
+
10
+ @Field
11
+ var licenseServer: String?
12
+
13
+ @Field
14
+ var headers: [String: Any]?
15
+
16
+ @Field
17
+ var contentId: String?
18
+
19
+ @Field
20
+ var certificateUrl: URL?
21
+
22
+ @Field
23
+ var base64CertificateData: String?
24
+ }
@@ -0,0 +1,10 @@
1
+ // Copyright 2024-present 650 Industries. All rights reserved.
2
+
3
+ import Foundation
4
+ import ExpoModulesCore
5
+
6
+ internal struct PlaybackError: Record {
7
+ @Field
8
+ // swiftlint:disable:next redundant_optional_initialization - Initialization with nil is necessary
9
+ var message: String? = nil
10
+ }
@@ -0,0 +1,176 @@
1
+ // Copyright 2024-present 650 Industries. All rights reserved.
2
+
3
+ import ExpoModulesCore
4
+ import AVKit
5
+
6
+ // swiftlint:disable redundant_optional_initialization - Initialization with nil is necessary
7
+ internal struct SubtitleTrack: Record {
8
+ @Field
9
+ var language: String? = nil
10
+
11
+ @Field
12
+ var label: String? = nil
13
+
14
+ static func from(mediaSelectionOption option: AVMediaSelectionOption) -> SubtitleTrack? {
15
+ guard let identifier = option.locale?.identifier else {
16
+ return nil
17
+ }
18
+
19
+ return SubtitleTrack(language: identifier, label: option.displayName)
20
+ }
21
+ }
22
+
23
+ internal struct AudioTrack: Record {
24
+ @Field var language: String? = nil
25
+ @Field var label: String? = nil
26
+
27
+ static func from(mediaSelectionOption option: AVMediaSelectionOption) -> AudioTrack? {
28
+ guard let identifier = option.locale?.identifier else {
29
+ return nil
30
+ }
31
+
32
+ return AudioTrack(language: identifier, label: option.displayName)
33
+ }
34
+ }
35
+
36
+ internal struct VideoTrack: Record, Equatable {
37
+ @Field var id: String? = nil
38
+ @Field var size: VideoSize? = nil
39
+ @Field var mimeType: String? = nil
40
+ @Field var bitrate: Int? = nil
41
+ @Field var isSupported: Bool = true
42
+ @Field var frameRate: Float? = nil
43
+
44
+ static func == (lhs: VideoTrack, rhs: VideoTrack) -> Bool {
45
+ guard lhs.id != nil, rhs.id != nil else {
46
+ return false
47
+ }
48
+ return lhs.id == rhs.id
49
+ }
50
+
51
+ static func from(assetTrack: AVAssetTrack) async -> VideoTrack {
52
+ var bitrate: Int?
53
+ var size: VideoSize?
54
+ let supported = (try? await assetTrack.load(.isPlayable)) ?? true
55
+ let mediaFormat = try? await assetTrack.mediaFormat
56
+ let frameRate = try? await assetTrack.load(.nominalFrameRate)
57
+
58
+ if let bitrateFloat = try? await assetTrack.load(.estimatedDataRate) {
59
+ bitrate = Int(bitrateFloat)
60
+ }
61
+ if let cgSize = try? await assetTrack.load(.naturalSize) {
62
+ size = VideoSize.from(cgSize)
63
+ }
64
+
65
+ return VideoTrack(id: "\(assetTrack.trackID)", size: size, mimeType: mediaFormat, bitrate: bitrate, isSupported: supported, frameRate: frameRate)
66
+ }
67
+
68
+ static func from(hlsHeaderLine: String, idLine: String) -> VideoTrack? {
69
+ // The minimum information we require from a video track is it's resolution
70
+ guard hlsHeaderLine.starts(with: "#EXT-X-STREAM-INF"), hlsHeaderLine.contains("RESOLUTION") else {
71
+ return nil
72
+ }
73
+ // The information about the track is separated with ,
74
+ let details = hlsHeaderLine.split(separator: ",")
75
+ .reduce(into: [String: String]()) { dict, detail in
76
+ let pair = detail.split(separator: "=", maxSplits: 1).map {
77
+ String($0).trimmingCharacters(in: .whitespacesAndNewlines)
78
+ }
79
+ if pair.count == 2 {
80
+ let (key, value) = (pair[0], pair[1])
81
+ // Remove possible double quotes
82
+ dict[key] = value.trimmingCharacters(in: CharacterSet(charactersIn: "\""))
83
+ }
84
+ }
85
+ guard let resolution = details["RESOLUTION"] else {
86
+ return nil
87
+ }
88
+
89
+ let dimensions = resolution.split(separator: "x").map { Int($0) }
90
+ guard dimensions.count == 2, let width = dimensions[0], let height = dimensions[1] else {
91
+ return nil
92
+ }
93
+
94
+ let size = VideoSize(width: width, height: height)
95
+ let mimeType = codecsToMimeType(codecs: details["CODECS"])
96
+ var bitrate: Int? = nil
97
+ var frameRate: Float? = nil
98
+
99
+ // Use the default Andorid behavior for reporting the bitrate
100
+ if let bitrateString = details["BANDWIDTH"] ?? details["AVERAGE-BANDWIDTH"] {
101
+ bitrate = Int(bitrateString)
102
+ }
103
+ if let frameRateString = details["FRAME-RATE"] {
104
+ frameRate = Float(frameRateString)
105
+ }
106
+
107
+ return VideoTrack(id: idLine, size: size, mimeType: mimeType, bitrate: bitrate, frameRate: frameRate)
108
+ }
109
+
110
+ // I'm not aware of any built in conversion functions. For HLS sources we only need to worry about a few formats though.
111
+ // https://developer.apple.com/documentation/http-live-streaming/hls-authoring-specification-for-apple-devices#:~:text=1.1.%20All%20video%20MUST%20be%20encoded%20using%20H.264/AVC%2C%20HEVC/H.265%2C%20Dolby%20Vision%2C%20or%20AV1.
112
+ private static func codecsToMimeType(codecs: String?) -> String? {
113
+ guard let codecs else {
114
+ return nil
115
+ }
116
+ if codecs.starts(with: "avc1") {
117
+ return "video/avc"
118
+ }
119
+ if codecs.starts(with: "hvc1") {
120
+ return "video/hevc"
121
+ }
122
+ if codecs.starts(with: "dvh1") {
123
+ return "video/dolby-vision"
124
+ }
125
+ if codecs.starts(with: "av11") {
126
+ return "video/av1"
127
+ }
128
+ return nil // Unknown codec
129
+ }
130
+ }
131
+ // swiftlint:enable redundant_optional_initialization
132
+
133
+ // https://developer.apple.com/documentation/avfoundation/avpartialasyncproperty/formatdescriptions
134
+ private extension AVAssetTrack {
135
+ var mediaFormat: String {
136
+ get async throws {
137
+ var format = ""
138
+ let descriptions = try await load(.formatDescriptions)
139
+ for (index, formatDesc) in descriptions.enumerated() {
140
+ let subType = CMFormatDescriptionGetMediaSubType(formatDesc).toString()
141
+
142
+ // The reported subType is different for iOS and Android, ideally they should be the same
143
+ let correctedSubType: String
144
+ switch subType {
145
+ case "avc1": // H264 videos
146
+ correctedSubType = "avc"
147
+ case "hev1": // H265 videos
148
+ correctedSubType = "hevc"
149
+ default:
150
+ correctedSubType = subType
151
+ }
152
+ format += "video/\(correctedSubType)"
153
+ if index < descriptions.count - 1 {
154
+ format += ","
155
+ }
156
+ }
157
+ return format
158
+ }
159
+ }
160
+ }
161
+
162
+ private extension FourCharCode {
163
+ // Create a string representation of a FourCC.
164
+ func toString() -> String {
165
+ let bytes: [CChar] = [
166
+ CChar((self >> 24) & 0xff),
167
+ CChar((self >> 16) & 0xff),
168
+ CChar((self >> 8) & 0xff),
169
+ CChar(self & 0xff),
170
+ 0
171
+ ]
172
+ let result = String(cString: bytes)
173
+ let characterSet = CharacterSet.whitespaces
174
+ return result.trimmingCharacters(in: characterSet)
175
+ }
176
+ }
@@ -0,0 +1,76 @@
1
+ import Foundation
2
+ import ExpoModulesCore
3
+
4
+ // swiftlint:disable redundant_optional_initialization - Initialization with nil is necessary
5
+ internal struct StatusChangedEventPayload: Record {
6
+ @Field var status: PlayerStatus = .idle
7
+ @Field var oldStatus: PlayerStatus? = nil
8
+ @Field var error: PlaybackError? = nil
9
+ }
10
+
11
+ internal struct IsPlayingEventPayload: Record {
12
+ @Field var isPlaying: Bool = false
13
+ @Field var oldIsPlaying: Bool? = nil
14
+ }
15
+
16
+ internal struct VolumeChangedEventPayload: Record {
17
+ @Field var volume: Float = 1
18
+ @Field var oldVolume: Float? = nil
19
+ }
20
+
21
+ internal struct MutedChangedEventPayload: Record {
22
+ @Field var muted: Bool = false
23
+ @Field var oldMuted: Bool? = nil
24
+ }
25
+
26
+ internal struct SourceChangedEventPayload: Record {
27
+ @Field var source: VideoSource? = nil
28
+ @Field var oldSource: VideoSource? = nil
29
+ }
30
+
31
+ internal struct PlaybackRateChangedEventPayload: Record {
32
+ @Field var playbackRate: Float = 1
33
+ @Field var oldPlaybackRate: Float? = nil
34
+ }
35
+
36
+ internal struct SubtitleTracksChangedEventPayload: Record {
37
+ @Field var availableSubtitleTracks: [SubtitleTrack] = []
38
+ @Field var oldAvailableSubtitleTracks: [SubtitleTrack] = []
39
+ }
40
+
41
+ internal struct SubtitleTrackChangedEventPayload: Record {
42
+ @Field var subtitleTrack: SubtitleTrack? = nil
43
+ @Field var oldSubtitleTrack: SubtitleTrack? = nil
44
+ }
45
+
46
+ internal struct AudioTracksChangedEventPayload: Record {
47
+ @Field var availableAudioTracks: [AudioTrack] = []
48
+ @Field var oldAvailableAudioTracks: [AudioTrack] = []
49
+ }
50
+
51
+ internal struct AudioTrackChangedEventPayload: Record {
52
+ @Field var audioTrack: AudioTrack? = nil
53
+ @Field var oldAudioTrack: AudioTrack? = nil
54
+ }
55
+
56
+ internal struct TimeUpdate: Record {
57
+ @Field var currentTime: Double = 0
58
+ @Field var currentLiveTimestamp: Double? = nil
59
+ @Field var currentOffsetFromLive: Double? = nil
60
+ @Field var bufferedPosition: Double = -1
61
+ }
62
+
63
+ internal struct VideoTrackChangedEventPayload: Record {
64
+ @Field var videoTrack: VideoTrack? = nil
65
+ @Field var oldVideoTrack: VideoTrack? = nil
66
+ }
67
+
68
+ internal struct VideoSourceLoadedEventPayload: Record {
69
+ @Field var videoSource: VideoSource? = nil
70
+ @Field var duration: Double? = nil
71
+ @Field var availableVideoTracks: [VideoTrack]? = nil
72
+ @Field var availableSubtitleTracks: [SubtitleTrack]? = nil
73
+ @Field var availableAudioTracks: [AudioTrack]? = nil
74
+ }
75
+
76
+ // swiftlint:enable redundant_optional_initialization
@@ -0,0 +1,16 @@
1
+ // Copyright 2024-present 650 Industries. All rights reserved.
2
+
3
+ import ExpoModulesCore
4
+
5
+ // swiftlint:disable redundant_optional_initialization - Initialization with nil is necessary
6
+ internal struct VideoMetadata: Record {
7
+ @Field
8
+ var title: String? = nil
9
+
10
+ @Field
11
+ var artist: String? = nil
12
+
13
+ @Field
14
+ var artwork: URL? = nil
15
+ }
16
+ // swiftlint:enable redundant_optional_initialization
@@ -0,0 +1,15 @@
1
+ import ExpoModulesCore
2
+
3
+ // swiftlint:disable redundant_optional_initialization - Initialization with nil is necessary
4
+ internal struct VideoSize: Record {
5
+ @Field
6
+ var width: Int? = nil
7
+
8
+ @Field
9
+ var height: Int? = nil
10
+
11
+ static func from(_ size: CGSize) -> Self {
12
+ return VideoSize(width: Int(size.width), height: Int(size.height))
13
+ }
14
+ }
15
+ // swiftlint:enable redundant_optional_initialization
@@ -0,0 +1,25 @@
1
+ // Copyright 2024-present 650 Industries. All rights reserved.
2
+
3
+ import ExpoModulesCore
4
+
5
+ // swiftlint:disable redundant_optional_initialization - Initialization with nil is necessary
6
+ internal struct VideoSource: Record {
7
+ @Field
8
+ var uri: URL? = nil
9
+
10
+ @Field
11
+ var drm: DRMOptions? = nil
12
+
13
+ @Field
14
+ var metadata: VideoMetadata? = nil
15
+
16
+ @Field
17
+ var headers: [String: String]? = nil
18
+
19
+ @Field
20
+ var useCaching: Bool = false
21
+
22
+ @Field
23
+ var contentType: ContentType = .auto
24
+ }
25
+ // swiftlint:enable redundant_optional_initialization
@@ -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
+ }