@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,147 @@
1
+ import Foundation
2
+ import ExpoModulesCore
3
+
4
+ class MediaInfo: Codable {
5
+ var expectedContentLength: Int64
6
+ var supportsByteRangeAccess: Bool
7
+ var mimeType: String?
8
+ var headerFields: [String: String]?
9
+ var savePath: String
10
+
11
+ // Tuples can't be encoded/decoded, so we workaround that with an array
12
+ private var loadedDataRangesArr: [[Int]] = []
13
+ private(set) var loadedDataRanges: [(Int, Int)] {
14
+ get {
15
+ return loadedDataRangesArrayToTuple()
16
+ }
17
+ set {
18
+ loadedDataRangesArr = loadedDataRangesTupleToArray(newValue)
19
+ }
20
+ }
21
+
22
+ private enum CodingKeys: String, CodingKey {
23
+ case expectedContentLength, supportsByteRangeAccess, mimeType, loadedDataRangesArr, headerFields, savePath
24
+ }
25
+
26
+ init(expectedContentLength: Int64, mimeType: String?, supportsByteRangeAccess: Bool, headerFields: [String: String]?, savePath: String) {
27
+ self.mimeType = mimeType
28
+ self.supportsByteRangeAccess = supportsByteRangeAccess
29
+ self.expectedContentLength = expectedContentLength
30
+ self.headerFields = headerFields
31
+ self.savePath = savePath
32
+ self.loadedDataRanges = loadedDataRanges
33
+
34
+ if let url = URL(string: savePath) {
35
+ VideoCacheManager.shared.registerOpenFile(at: url)
36
+ }
37
+ }
38
+
39
+ deinit {
40
+ if let url = URL(string: savePath) {
41
+ VideoCacheManager.shared.unregisterOpenFile(at: url)
42
+ }
43
+ }
44
+
45
+ convenience init?(data: Data, dataPath: String) {
46
+ do {
47
+ let mediaInfo = try JSONDecoder().decode(MediaInfo.self, from: data)
48
+ self.init(
49
+ expectedContentLength: mediaInfo.expectedContentLength,
50
+ mimeType: mediaInfo.mimeType,
51
+ supportsByteRangeAccess: mediaInfo.supportsByteRangeAccess,
52
+ headerFields: mediaInfo.headerFields,
53
+ savePath: mediaInfo.savePath)
54
+ self.loadedDataRanges = mediaInfo.loadedDataRanges
55
+ } catch {
56
+ return nil
57
+ }
58
+ }
59
+
60
+ convenience init?(at path: String) {
61
+ guard FileManager.default.fileExists(atPath: path), let mediaInfoData = FileManager.default.contents(atPath: path) else {
62
+ return nil
63
+ }
64
+ self.init(data: mediaInfoData, dataPath: path)
65
+ }
66
+
67
+ convenience init?(forResourceUrl url: URL) {
68
+ guard let filePath = VideoAsset.pathForUrl(url: url, fileExtension: url.pathExtension) else {
69
+ return nil
70
+ }
71
+ let mediaInfoPath = filePath + VideoCacheManager.mediaInfoSuffix
72
+ self.init(at: mediaInfoPath)
73
+ }
74
+
75
+ func addDataRange(newDataRange: (Int, Int)) {
76
+ var i = 0
77
+ var merged = [(Int, Int)]()
78
+
79
+ // Add all intervals before newInterval starts
80
+ while i < loadedDataRanges.count && loadedDataRanges[i].1 < newDataRange.0 {
81
+ merged.append(loadedDataRanges[i])
82
+ i += 1
83
+ }
84
+
85
+ // Merge all overlapping intervals to one new Interval
86
+ var newStart = newDataRange.0
87
+ var newEnd = newDataRange.1
88
+ while i < loadedDataRanges.count && loadedDataRanges[i].0 <= newDataRange.1 {
89
+ newStart = min(newStart, loadedDataRanges[i].0)
90
+ newEnd = max(newEnd, loadedDataRanges[i].1)
91
+ i += 1
92
+ }
93
+ merged.append((newStart, newEnd))
94
+
95
+ // Add remaining intervals
96
+ while i < loadedDataRanges.count {
97
+ merged.append(loadedDataRanges[i])
98
+ i += 1
99
+ }
100
+ loadedDataRanges = merged
101
+ }
102
+
103
+ func encodeToData() -> Data? {
104
+ do {
105
+ return try JSONEncoder().encode(self)
106
+ } catch {
107
+ log.warn("Error encoding MediaInfo object: \(error)")
108
+ return nil
109
+ }
110
+ }
111
+
112
+ // Saves the mime type of a video fetched from the server into a file. This allows playing videos without an extension in the
113
+ // url.
114
+ func saveToFile() {
115
+ do {
116
+ if FileManager.default.fileExists(atPath: savePath) {
117
+ try FileManager.default.removeItem(atPath: savePath)
118
+ }
119
+
120
+ FileManager.default.createFile(atPath: savePath, contents: self.encodeToData())
121
+ } catch {
122
+ log.warn("Failed to save media info at: \(savePath)")
123
+ }
124
+ }
125
+
126
+ // Public setter for loadedDataRanges
127
+ public func setLoadedDataRanges(_ ranges: [(Int, Int)]) {
128
+ self.loadedDataRanges = ranges
129
+ }
130
+
131
+ private func loadedDataRangesArrayToTuple() -> [(Int, Int)] {
132
+ // The filter shouldn't be necessary, but we can't be too careful
133
+ let filteredDataRanges = loadedDataRangesArr.filter { rangeArray in
134
+ rangeArray.count == 2
135
+ }
136
+
137
+ return filteredDataRanges.map { rangeArray in
138
+ (rangeArray[0], rangeArray[1])
139
+ }
140
+ }
141
+
142
+ private func loadedDataRangesTupleToArray(_ loadedDataRanges: [(Int, Int)]) -> [[Int]] {
143
+ return loadedDataRanges.map { from, to in
144
+ return [from, to]
145
+ }
146
+ }
147
+ }
@@ -0,0 +1,274 @@
1
+ import Foundation
2
+ import AVFoundation
3
+ import UIKit
4
+ import CoreServices
5
+ import ExpoModulesCore
6
+
7
+ /**
8
+ * Class responsible for fulfilling data requests created by the AVAsset. There are two types of requests:
9
+ * - Initial request - The response contains most of the information about the data source such as support for content ranges, total size etc.
10
+ * this information is cached for offline playback support.
11
+ * - Data request - For each range request from the player the delegate will request and receive multiple chunks of data. We have to return a correct subrange
12
+ * of data and cache it. If a chunk of data is already available we will return it from cache.
13
+ */
14
+ final class ResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate, URLSessionDataDelegate, URLSessionTaskDelegate {
15
+ private let url: URL
16
+ private let saveFilePath: String
17
+ private let fileExtension: String
18
+ private let cachedResource: CachedResource
19
+ private let urlRequestHeaders: [String: String]?
20
+ internal var onError: ((Error) -> Void)?
21
+
22
+ private var cachableRequests: SynchronizedHashTable<CachableRequest> = SynchronizedHashTable()
23
+ private var session: URLSession?
24
+
25
+ /**
26
+ * The default requestTimeoutInterval is 60, which is too long (UI should respond relatively quickly to network errors)
27
+ */
28
+ private static let requestTimeoutInterval: Double = 5
29
+
30
+ // When playing from an url without an extension appends an extension to the path based on the response from the server
31
+ private var pathWithExtension: String {
32
+ let ext = mimeTypeToExtension(mimeType: cachedResource.mediaInfo?.mimeType)
33
+ if let ext, self.fileExtension.isEmpty {
34
+ return self.saveFilePath + ".\(ext)"
35
+ }
36
+ return self.saveFilePath
37
+ }
38
+
39
+ init(url: URL, saveFilePath: String, fileExtension: String, urlRequestHeaders: [String: String]?) {
40
+ self.url = url
41
+ self.saveFilePath = saveFilePath
42
+ self.fileExtension = fileExtension
43
+ self.urlRequestHeaders = urlRequestHeaders
44
+ cachedResource = CachedResource(dataFileUrl: saveFilePath, resourceUrl: url, dataPath: saveFilePath)
45
+ super.init()
46
+ self.session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
47
+ }
48
+
49
+ deinit {
50
+ session?.invalidateAndCancel()
51
+ session = nil
52
+ }
53
+
54
+ // MARK: - AVAssetResourceLoaderDelegate
55
+
56
+ func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
57
+ processLoadingRequest(loadingRequest: loadingRequest)
58
+ return true
59
+ }
60
+
61
+ func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) {
62
+ cachableRequest(by: loadingRequest)?.dataTask.cancel()
63
+ }
64
+
65
+ // MARK: - URLSessionDelegate
66
+
67
+ func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
68
+ guard let currentRequest = dataTask.currentRequest,
69
+ let response = dataTask.response as? HTTPURLResponse,
70
+ let cachableRequest = cachableRequest(by: dataTask) else {
71
+ return
72
+ }
73
+
74
+ let dataRequest = cachableRequest.dataRequest
75
+ let requestedOffset = dataRequest.requestedOffset
76
+ let currentOffset = dataRequest.currentOffset
77
+ let length = dataRequest.requestedLength
78
+
79
+ // If finding correct subdata failed, fallback to pure received data
80
+ let subdata = data.subdata(request: currentRequest, response: response) ?? data
81
+
82
+ // Append modified or original data
83
+ cachableRequest.onReceivedData(data: subdata)
84
+
85
+ if dataRequest.requestsAllDataToEndOfResource {
86
+ let currentDataResponseOffset = Int(currentOffset - requestedOffset)
87
+ let currentDataResponseLength = cachableRequest.receivedData.count - currentDataResponseOffset
88
+ let subdata = cachableRequest.receivedData.subdata(in: currentDataResponseOffset..<currentDataResponseOffset + currentDataResponseLength)
89
+ dataRequest.respond(with: subdata)
90
+ } else if currentOffset - requestedOffset <= cachableRequest.receivedData.count {
91
+ let rangeStart = Int(currentOffset - requestedOffset)
92
+ let rangeLength = min(cachableRequest.receivedData.count - rangeStart, length)
93
+ let subdata = cachableRequest.receivedData.subdata(in: rangeStart..<rangeStart + rangeLength)
94
+ dataRequest.respond(with: subdata)
95
+ }
96
+ }
97
+
98
+ func urlSession(
99
+ _ session: URLSession,
100
+ dataTask: URLSessionDataTask,
101
+ didReceive response: URLResponse,
102
+ completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
103
+ ) {
104
+ if let cachedDataRequest = cachableRequest(by: dataTask) {
105
+ cachedDataRequest.response = response
106
+ if cachedDataRequest.loadingRequest.contentInformationRequest != nil {
107
+ fillInContentInformationRequest(forDataRequest: cachedDataRequest)
108
+ cachedDataRequest.loadingRequest.response = response
109
+ cachedDataRequest.loadingRequest.finishLoading()
110
+ cachedDataRequest.dataTask.cancel()
111
+ cachableRequests.remove(cachedDataRequest)
112
+ }
113
+ }
114
+ completionHandler(.allow)
115
+ }
116
+
117
+ func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
118
+ guard let cachedDataRequest = cachableRequest(by: task) else {
119
+ return
120
+ }
121
+
122
+ // The data shouldn't be corrupted and can be cached
123
+ if let error = error as? URLError, error.code == URLError.cancelled || error.code == URLError.networkConnectionLost {
124
+ cachedDataRequest.saveData(to: cachedResource)
125
+ } else if error == nil {
126
+ cachedDataRequest.saveData(to: cachedResource)
127
+ } else {
128
+ cachedDataRequest.loadingRequest.finishLoading(with: error)
129
+ }
130
+ cachedDataRequest.loadingRequest.finishLoading(with: error)
131
+ cachableRequests.remove(cachedDataRequest)
132
+ }
133
+
134
+ private func processLoadingRequest(loadingRequest: AVAssetResourceLoadingRequest) {
135
+ let (remainingRequest, dataReceived) = attemptToRespondFromCache(forRequest: loadingRequest)
136
+
137
+ // Cache fulfilled the entire request
138
+ if dataReceived != nil && remainingRequest == nil {
139
+ return
140
+ }
141
+
142
+ var request = remainingRequest ?? createUrlRequest()
143
+
144
+ // remainingRequest will have correct range header fields
145
+ if remainingRequest == nil {
146
+ addRangeHeaderFields(loadingRequest: loadingRequest, urlRequest: &request)
147
+ }
148
+
149
+ guard let session else {
150
+ return
151
+ }
152
+
153
+ let dataTask = session.dataTask(with: request)
154
+
155
+ // we can't do `if let loadingRequest = loadingRequest.dataRequest` as this would create new variable by copying
156
+ if loadingRequest.dataRequest != nil {
157
+ let cachableRequest = CachableRequest(loadingRequest: loadingRequest, dataTask: dataTask, dataRequest: loadingRequest.dataRequest!)
158
+ // We need to add the data that was received from cache in order to keep byte offsets consistent
159
+ if let dataReceived {
160
+ cachableRequest.onReceivedData(data: dataReceived)
161
+ }
162
+ cachableRequests.add(cachableRequest)
163
+ } else {
164
+ log.warn("ResourceLoaderDelegate has received a loading request without a data request")
165
+ }
166
+ dataTask.resume()
167
+ }
168
+
169
+ private func fillInContentInformationRequest(forDataRequest request: CachableRequest?) {
170
+ guard let response = request?.response as? HTTPURLResponse else {
171
+ return
172
+ }
173
+
174
+ request?.loadingRequest.contentInformationRequest?.contentLength = response.expectedContentLength
175
+ request?.loadingRequest.contentInformationRequest?.isByteRangeAccessSupported = true
176
+
177
+ if let mimeType = response.mimeType, isSupported(mimeType: mimeType) {
178
+ let rawUti = UTType(mimeType: mimeType)?.identifier
179
+ request?.loadingRequest.contentInformationRequest?.contentType = rawUti ?? response.mimeType
180
+ cachedResource.onResponseReceived(response: response)
181
+ } else {
182
+ // We can't control the AVPlayer.error property that will be set after the player fails to load the resource
183
+ // We have an additional field that can be used to return a more specific error
184
+ onError?(VideoCacheUnsupportedFormatException(response.mimeType ?? ""))
185
+ }
186
+ }
187
+
188
+ /// Attempts to load the request from cache, if just the beginning of the requested data is available, returns a URL request to fetch the rest of the data
189
+ private func attemptToRespondFromCache(forRequest loadingRequest: AVAssetResourceLoadingRequest) -> (request: URLRequest?, dataReceived: Data?) {
190
+ guard let dataRequest = loadingRequest.dataRequest else {
191
+ return (nil, nil)
192
+ }
193
+
194
+ let from = dataRequest.requestedOffset
195
+ let to = from + Int64(dataRequest.requestedLength) - 1
196
+
197
+ // Try to return the whole data from the cache
198
+ if let cachedData = cachedResource.requestData(from: from, to: to) {
199
+ if loadingRequest.contentInformationRequest != nil {
200
+ cachedResource.fill(forLoadingRequest: loadingRequest)
201
+ }
202
+ loadingRequest.dataRequest?.respond(with: cachedData)
203
+ loadingRequest.finishLoading()
204
+ return (nil, cachedData)
205
+ }
206
+
207
+ // Try to return the beginning of the data, and create a request for the remainder
208
+ if let partialData = cachedResource.requestBeginningOfData(from: from, to: to) {
209
+ if loadingRequest.contentInformationRequest != nil {
210
+ cachedResource.fill(forLoadingRequest: loadingRequest)
211
+ }
212
+ loadingRequest.dataRequest?.respond(with: partialData)
213
+
214
+ var request = createUrlRequest()
215
+ if loadingRequest.contentInformationRequest == nil {
216
+ if loadingRequest.dataRequest?.requestsAllDataToEndOfResource == true {
217
+ let requestedOffset = dataRequest.requestedOffset
218
+ request.setValue("bytes=\(Int(requestedOffset) + partialData.count)-", forHTTPHeaderField: "Range")
219
+ } else if let dataRequest = loadingRequest.dataRequest {
220
+ let requestedOffset = dataRequest.requestedOffset
221
+ let requestedLength = dataRequest.requestedLength
222
+ let from = Int(requestedOffset) + partialData.count
223
+ let to = from + requestedLength - partialData.count - 1
224
+ request.setValue("bytes=\(from)-\(to)", forHTTPHeaderField: "Range")
225
+ }
226
+ }
227
+ return (request, partialData)
228
+ }
229
+
230
+ return (nil, nil)
231
+ }
232
+
233
+ // The loading resource might want only a part of the video
234
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range
235
+ private func addRangeHeaderFields(loadingRequest: AVAssetResourceLoadingRequest, urlRequest: inout URLRequest) {
236
+ guard let dataRequest = loadingRequest.dataRequest, loadingRequest.contentInformationRequest == nil else {
237
+ return
238
+ }
239
+
240
+ if dataRequest.requestsAllDataToEndOfResource {
241
+ let requestedOffset = dataRequest.requestedOffset
242
+ urlRequest.setValue("bytes=\(requestedOffset)-", forHTTPHeaderField: "Range")
243
+ return
244
+ }
245
+
246
+ let requestedOffset = dataRequest.requestedOffset
247
+ let requestedLength = Int64(dataRequest.requestedLength)
248
+ urlRequest.setValue("bytes=\(requestedOffset)-\(requestedOffset + requestedLength - 1)", forHTTPHeaderField: "Range")
249
+ }
250
+
251
+ private func isSupported(mimeType: String?) -> Bool {
252
+ return mimeType?.starts(with: "video/") ?? false
253
+ }
254
+
255
+ private func createUrlRequest() -> URLRequest {
256
+ var request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy)
257
+ request.timeoutInterval = Self.requestTimeoutInterval
258
+
259
+ self.urlRequestHeaders?.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
260
+ return request
261
+ }
262
+
263
+ private func cachableRequest(by loadingRequest: AVAssetResourceLoadingRequest) -> CachableRequest? {
264
+ return cachableRequests.allObjects.first(where: {
265
+ $0.loadingRequest == loadingRequest
266
+ })
267
+ }
268
+
269
+ private func cachableRequest(by task: URLSessionTask) -> CachableRequest? {
270
+ return cachableRequests.allObjects.first(where: {
271
+ $0.dataTask == task
272
+ })
273
+ }
274
+ }
@@ -0,0 +1,23 @@
1
+ // Copyright 2025-present 650 Industries. All rights reserved.
2
+
3
+ internal class SynchronizedHashTable<T: AnyObject> {
4
+ private let lock = NSLock()
5
+ private var hashTable: NSHashTable<T> = NSHashTable()
6
+ var allObjects: [T] {
7
+ lock.lock()
8
+ defer { lock.unlock() }
9
+ return Array(hashTable.allObjects)
10
+ }
11
+
12
+ func add(_ object: T) {
13
+ lock.lock()
14
+ defer { lock.unlock() }
15
+ hashTable.add(object)
16
+ }
17
+
18
+ func remove(_ object: T) {
19
+ lock.lock()
20
+ defer { lock.unlock() }
21
+ hashTable.remove(object)
22
+ }
23
+ }