@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.
- package/README.md +45 -0
- package/android/build.gradle +32 -0
- package/android/src/main/AndroidManifest.xml +20 -0
- package/android/src/main/java/expo/modules/video/AudioFocusManager.kt +241 -0
- package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +145 -0
- package/android/src/main/java/expo/modules/video/IntervalUpdateClock.kt +54 -0
- package/android/src/main/java/expo/modules/video/MediaMetadataRetriever.kt +89 -0
- package/android/src/main/java/expo/modules/video/PictureInPictureHelperFragment.kt +26 -0
- package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +36 -0
- package/android/src/main/java/expo/modules/video/VideoCache.kt +104 -0
- package/android/src/main/java/expo/modules/video/VideoExceptions.kt +34 -0
- package/android/src/main/java/expo/modules/video/VideoManager.kt +133 -0
- package/android/src/main/java/expo/modules/video/VideoModule.kt +414 -0
- package/android/src/main/java/expo/modules/video/VideoThumbnail.kt +20 -0
- package/android/src/main/java/expo/modules/video/VideoView.kt +367 -0
- package/android/src/main/java/expo/modules/video/delegates/IgnoreSameSet.kt +24 -0
- package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +217 -0
- package/android/src/main/java/expo/modules/video/enums/AudioMixingMode.kt +20 -0
- package/android/src/main/java/expo/modules/video/enums/ContentFit.kt +19 -0
- package/android/src/main/java/expo/modules/video/enums/ContentType.kt +22 -0
- package/android/src/main/java/expo/modules/video/enums/DRMType.kt +26 -0
- package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
- package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +184 -0
- package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +39 -0
- package/android/src/main/java/expo/modules/video/playbackService/VideoMediaSessionCallback.kt +47 -0
- package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +93 -0
- package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +164 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +460 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +125 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerListener.kt +32 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerLoadControl.kt +525 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +125 -0
- package/android/src/main/java/expo/modules/video/records/BufferOptions.kt +15 -0
- package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +25 -0
- package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +19 -0
- package/android/src/main/java/expo/modules/video/records/Tracks.kt +81 -0
- package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +79 -0
- package/android/src/main/java/expo/modules/video/records/VideoMetadata.kt +12 -0
- package/android/src/main/java/expo/modules/video/records/VideoSize.kt +14 -0
- package/android/src/main/java/expo/modules/video/records/VideoSource.kt +104 -0
- package/android/src/main/java/expo/modules/video/records/VideoThumbnailOptions.kt +24 -0
- package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +75 -0
- package/android/src/main/java/expo/modules/video/utils/EventDispatcherUtils.kt +43 -0
- package/android/src/main/java/expo/modules/video/utils/MutableWeakReference.kt +15 -0
- package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +96 -0
- package/android/src/main/java/expo/modules/video/utils/YogaUtils.kt +20 -0
- package/android/src/main/res/drawable/seek_backwards_10s.xml +25 -0
- package/android/src/main/res/drawable/seek_backwards_15s.xml +25 -0
- package/android/src/main/res/drawable/seek_backwards_5s.xml +25 -0
- package/android/src/main/res/drawable/seek_forwards_10s.xml +30 -0
- package/android/src/main/res/drawable/seek_forwards_15s.xml +31 -0
- package/android/src/main/res/drawable/seek_forwards_5s.xml +30 -0
- package/android/src/main/res/layout/fullscreen_player_activity.xml +16 -0
- package/android/src/main/res/layout/surface_player_view.xml +7 -0
- package/android/src/main/res/layout/texture_player_view.xml +7 -0
- package/android/src/main/res/values/styles.xml +9 -0
- package/app.plugin.js +1 -0
- package/build/NativeVideoModule.d.ts +16 -0
- package/build/NativeVideoModule.d.ts.map +1 -0
- package/build/NativeVideoModule.js +3 -0
- package/build/NativeVideoModule.js.map +1 -0
- package/build/NativeVideoModule.web.d.ts +3 -0
- package/build/NativeVideoModule.web.d.ts.map +1 -0
- package/build/NativeVideoModule.web.js +2 -0
- package/build/NativeVideoModule.web.js.map +1 -0
- package/build/NativeVideoView.d.ts +4 -0
- package/build/NativeVideoView.d.ts.map +1 -0
- package/build/NativeVideoView.js +6 -0
- package/build/NativeVideoView.js.map +1 -0
- package/build/VideoModule.d.ts +38 -0
- package/build/VideoModule.d.ts.map +1 -0
- package/build/VideoModule.js +53 -0
- package/build/VideoModule.js.map +1 -0
- package/build/VideoPlayer.d.ts +15 -0
- package/build/VideoPlayer.d.ts.map +1 -0
- package/build/VideoPlayer.js +52 -0
- package/build/VideoPlayer.js.map +1 -0
- package/build/VideoPlayer.types.d.ts +532 -0
- package/build/VideoPlayer.types.d.ts.map +1 -0
- package/build/VideoPlayer.types.js +2 -0
- package/build/VideoPlayer.types.js.map +1 -0
- package/build/VideoPlayer.web.d.ts +75 -0
- package/build/VideoPlayer.web.d.ts.map +1 -0
- package/build/VideoPlayer.web.js +376 -0
- package/build/VideoPlayer.web.js.map +1 -0
- package/build/VideoPlayerEvents.types.d.ts +262 -0
- package/build/VideoPlayerEvents.types.d.ts.map +1 -0
- package/build/VideoPlayerEvents.types.js +2 -0
- package/build/VideoPlayerEvents.types.js.map +1 -0
- package/build/VideoThumbnail.d.ts +29 -0
- package/build/VideoThumbnail.d.ts.map +1 -0
- package/build/VideoThumbnail.js +3 -0
- package/build/VideoThumbnail.js.map +1 -0
- package/build/VideoView.d.ts +44 -0
- package/build/VideoView.d.ts.map +1 -0
- package/build/VideoView.js +76 -0
- package/build/VideoView.js.map +1 -0
- package/build/VideoView.types.d.ts +147 -0
- package/build/VideoView.types.d.ts.map +1 -0
- package/build/VideoView.types.js +2 -0
- package/build/VideoView.types.js.map +1 -0
- package/build/VideoView.web.d.ts +9 -0
- package/build/VideoView.web.d.ts.map +1 -0
- package/build/VideoView.web.js +180 -0
- package/build/VideoView.web.js.map +1 -0
- package/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +7 -0
- package/build/index.js.map +1 -0
- package/build/resolveAssetSource.d.ts +3 -0
- package/build/resolveAssetSource.d.ts.map +1 -0
- package/build/resolveAssetSource.js +3 -0
- package/build/resolveAssetSource.js.map +1 -0
- package/build/resolveAssetSource.web.d.ts +4 -0
- package/build/resolveAssetSource.web.d.ts.map +1 -0
- package/build/resolveAssetSource.web.js +16 -0
- package/build/resolveAssetSource.web.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/Cache/CachableRequest.swift +44 -0
- package/ios/Cache/CachedResource.swift +97 -0
- package/ios/Cache/CachingHelpers.swift +92 -0
- package/ios/Cache/MediaFileHandle.swift +94 -0
- package/ios/Cache/MediaInfo.swift +147 -0
- package/ios/Cache/ResourceLoaderDelegate.swift +274 -0
- package/ios/Cache/SynchronizedHashTable.swift +23 -0
- package/ios/Cache/VideoCacheManager.swift +338 -0
- package/ios/ContentKeyDelegate.swift +214 -0
- package/ios/ContentKeyManager.swift +21 -0
- package/ios/Enums/AudioMixingMode.swift +37 -0
- package/ios/Enums/ContentType.swift +12 -0
- package/ios/Enums/DRMType.swift +20 -0
- package/ios/Enums/PlayerStatus.swift +10 -0
- package/ios/Enums/VideoContentFit.swift +39 -0
- package/ios/ExpoVideo.podspec +29 -0
- package/ios/NowPlayingManager.swift +296 -0
- package/ios/Records/BufferOptions.swift +12 -0
- package/ios/Records/DRMOptions.swift +24 -0
- package/ios/Records/PlaybackError.swift +10 -0
- package/ios/Records/Tracks.swift +176 -0
- package/ios/Records/VideoEventPayloads.swift +76 -0
- package/ios/Records/VideoMetadata.swift +16 -0
- package/ios/Records/VideoSize.swift +15 -0
- package/ios/Records/VideoSource.swift +25 -0
- package/ios/Thumbnails/VideoThumbnail.swift +27 -0
- package/ios/Thumbnails/VideoThumbnailGenerator.swift +68 -0
- package/ios/Thumbnails/VideoThumbnailOptions.swift +15 -0
- package/ios/VideoAsset.swift +123 -0
- package/ios/VideoExceptions.swift +53 -0
- package/ios/VideoItem.swift +11 -0
- package/ios/VideoManager.swift +140 -0
- package/ios/VideoModule.swift +383 -0
- package/ios/VideoPlayer/DangerousPropertiesStore.swift +19 -0
- package/ios/VideoPlayer.swift +435 -0
- package/ios/VideoPlayerAudioTracks.swift +72 -0
- package/ios/VideoPlayerItem.swift +97 -0
- package/ios/VideoPlayerObserver.swift +523 -0
- package/ios/VideoPlayerSubtitles.swift +71 -0
- package/ios/VideoSourceLoader.swift +89 -0
- package/ios/VideoSourceLoaderListener.swift +34 -0
- package/ios/VideoView.swift +224 -0
- package/package.json +59 -0
- package/plugin/build/tsconfig.tsbuildinfo +1 -0
- package/plugin/build/withExpoVideo.d.ts +7 -0
- package/plugin/build/withExpoVideo.js +38 -0
- package/src/NativeVideoModule.ts +20 -0
- package/src/NativeVideoModule.web.ts +1 -0
- package/src/NativeVideoView.ts +8 -0
- package/src/VideoModule.ts +59 -0
- package/src/VideoPlayer.tsx +67 -0
- package/src/VideoPlayer.types.ts +613 -0
- package/src/VideoPlayer.web.tsx +451 -0
- package/src/VideoPlayerEvents.types.ts +313 -0
- package/src/VideoThumbnail.ts +31 -0
- package/src/VideoView.tsx +86 -0
- package/src/VideoView.types.ts +165 -0
- package/src/VideoView.web.tsx +214 -0
- package/src/index.ts +46 -0
- package/src/resolveAssetSource.ts +2 -0
- package/src/resolveAssetSource.web.ts +17 -0
- package/src/ts-declarations/react-native-assets.d.ts +1 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
class VideoCacheManager {
|
|
4
|
+
static let defaultMaxCacheSize = 1_024_000_000 // 1GB
|
|
5
|
+
static let defaultAutoCleanCache = true
|
|
6
|
+
static let expoVideoCacheScheme = "expo-video-cache"
|
|
7
|
+
static let expoVideoCacheDirectory = "expo-video-cache"
|
|
8
|
+
static let mediaInfoSuffix = "&mediaInfo"
|
|
9
|
+
|
|
10
|
+
static let shared = VideoCacheManager()
|
|
11
|
+
private let defaults = UserDefaults.standard
|
|
12
|
+
|
|
13
|
+
private let maxCacheSizeKey = "\(VideoCacheManager.expoVideoCacheScheme)/maxCacheSize"
|
|
14
|
+
|
|
15
|
+
// Files currently being used/modified by the player - they will be skipped when clearing the cache
|
|
16
|
+
private var openFiles: Set<URL> = Set()
|
|
17
|
+
|
|
18
|
+
// All cache commands such as clean or adding new data should be run on this queue
|
|
19
|
+
let cacheQueue = DispatchQueue(label: "\(VideoCacheManager.expoVideoCacheScheme)-dispatch-queue")
|
|
20
|
+
|
|
21
|
+
var maxCacheSize: Int {
|
|
22
|
+
defaults.maybeInteger(forKey: maxCacheSizeKey) ?? Self.defaultMaxCacheSize
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func registerOpenFile(at url: URL) {
|
|
26
|
+
openFiles.insert(url)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func unregisterOpenFile(at url: URL) {
|
|
30
|
+
openFiles.remove(url)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func setMaxCacheSize(newSize: Int) throws {
|
|
34
|
+
if VideoManager.shared.hasRegisteredPlayers {
|
|
35
|
+
throw VideoCacheException("Cannot change the cache size while there are active players")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
defaults.setValue(newSize, forKey: maxCacheSizeKey)
|
|
39
|
+
ensureCacheSize()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func ensureCacheSize() {
|
|
43
|
+
cacheQueue.async { [weak self] in
|
|
44
|
+
guard let self else {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
do {
|
|
49
|
+
try self.limitCacheSize(to: maxCacheSize)
|
|
50
|
+
} catch {
|
|
51
|
+
log.warn("Failed to auto clean expo-video cache")
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func clearAllCache() async throws {
|
|
57
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
58
|
+
cacheQueue.async { [weak self] in
|
|
59
|
+
do {
|
|
60
|
+
try self?.deleteAllFilesInCacheDirectory()
|
|
61
|
+
continuation.resume()
|
|
62
|
+
} catch {
|
|
63
|
+
continuation.resume(throwing: error)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func ensureCacheIntegrity(forSavePath videoFilePath: String) {
|
|
70
|
+
let mediaInfoPath = videoFilePath + Self.mediaInfoSuffix
|
|
71
|
+
let videoFileExists = FileManager.default.fileExists(atPath: videoFilePath)
|
|
72
|
+
let mediaInfoExists = FileManager.default.fileExists(atPath: mediaInfoPath)
|
|
73
|
+
|
|
74
|
+
// If mediaInfo exists and the corresponding data file doesn't we need to remove to avoid false data in
|
|
75
|
+
// the `loadedDataRanges` field
|
|
76
|
+
if mediaInfoExists && !videoFileExists {
|
|
77
|
+
try? FileManager.default.removeItem(atPath: mediaInfoPath)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func getCacheDirectorySize() -> Int64 {
|
|
82
|
+
guard let folderUrl = getCacheDirectory() else {
|
|
83
|
+
return 0
|
|
84
|
+
}
|
|
85
|
+
let fileManager = FileManager.default
|
|
86
|
+
var totalSize: Int64 = 0
|
|
87
|
+
|
|
88
|
+
guard let enumerator = fileManager.enumerator(at: folderUrl, includingPropertiesForKeys: [.fileSizeKey], options: .skipsHiddenFiles) else {
|
|
89
|
+
return 0
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for case let fileURL as URL in enumerator {
|
|
93
|
+
guard let fileAttributes = try? fileURL.resourceValues(forKeys: [.fileSizeKey]) else {
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
if let fileSize = fileAttributes.fileSize {
|
|
97
|
+
totalSize += Int64(fileSize)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return totalSize
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private func limitCacheSize(to maxSize: Int) throws {
|
|
105
|
+
let allFileURLs = getVideoFilesUrls()
|
|
106
|
+
|
|
107
|
+
var totalSize: Int64 = 0
|
|
108
|
+
var fileInfo = [(url: URL, size: Int64, accessDate: Date)]()
|
|
109
|
+
|
|
110
|
+
for url in allFileURLs {
|
|
111
|
+
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
|
|
112
|
+
let fileSize = attributes[.size] as? Int64 ?? 0
|
|
113
|
+
let accessDate = try url.resourceValues(forKeys: [.contentAccessDateKey]).contentAccessDate ?? Date.distantPast
|
|
114
|
+
totalSize += fileSize
|
|
115
|
+
fileInfo.append((url: url, size: fileSize, accessDate: accessDate))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if totalSize <= maxSize {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let deletableFileInfo = fileInfo.filter { !fileIsOpen(url: $0.url) }
|
|
123
|
+
let sortedFileInfo = deletableFileInfo.sorted { $0.accessDate < $1.accessDate }
|
|
124
|
+
|
|
125
|
+
for fileInfo in sortedFileInfo {
|
|
126
|
+
if totalSize <= maxSize {
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
try removeVideoAndMimeTypeFile(at: fileInfo.url)
|
|
130
|
+
totalSize -= fileInfo.size
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private func deleteAllFilesInCacheDirectory() throws {
|
|
135
|
+
if VideoManager.shared.hasRegisteredPlayers {
|
|
136
|
+
throw VideoCacheException("Cannot clear cache while there are active players")
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
guard let cacheDirectory = getCacheDirectory() else {
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let fileUrls = try FileManager.default.contentsOfDirectory(at: cacheDirectory, includingPropertiesForKeys: nil, options: [])
|
|
144
|
+
|
|
145
|
+
for fileUrl in fileUrls {
|
|
146
|
+
try removeVideoAndMimeTypeFile(at: fileUrl)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private func removeVideoAndMimeTypeFile(at fileUrl: URL) throws {
|
|
151
|
+
let mimeTypeFileUrl = URL(string: "\(fileUrl.relativeString)\(Self.mediaInfoSuffix)")
|
|
152
|
+
try FileManager.default.removeItem(at: fileUrl)
|
|
153
|
+
if let mimeTypeFileUrl, FileManager.default.fileExists(atPath: mimeTypeFileUrl.relativePath) {
|
|
154
|
+
try FileManager.default.removeItem(at: mimeTypeFileUrl)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private func getCacheDirectory() -> URL? {
|
|
159
|
+
let cacheDirs = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)
|
|
160
|
+
if let cacheDir = cacheDirs.first {
|
|
161
|
+
return cacheDir.appendingPathComponent(VideoCacheManager.expoVideoCacheDirectory)
|
|
162
|
+
}
|
|
163
|
+
return nil
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private func getVideoFilesUrls() -> [URL] {
|
|
167
|
+
guard let videoCacheDir = getCacheDirectory() else {
|
|
168
|
+
print("Failed to get the video cache directory.")
|
|
169
|
+
return []
|
|
170
|
+
}
|
|
171
|
+
let fileUrls = (try? FileManager.default.contentsOfDirectory(
|
|
172
|
+
at: videoCacheDir,
|
|
173
|
+
includingPropertiesForKeys: [.contentAccessDateKey, .contentModificationDateKey],
|
|
174
|
+
options: .skipsHiddenFiles)
|
|
175
|
+
) ?? []
|
|
176
|
+
return fileUrls.filter { !$0.absoluteString.hasSuffix(Self.mediaInfoSuffix) }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private func fileIsOpen(url: URL) -> Bool {
|
|
180
|
+
return openFiles.contains(url) || openFiles.contains { $0.relativePath == url.relativePath }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// MARK: - Custom Caching Methods
|
|
185
|
+
|
|
186
|
+
extension VideoCacheManager {
|
|
187
|
+
static func preCacheVideoAsync(from urlString: String) async throws -> Bool {
|
|
188
|
+
// Check if already cached (same logic as isVideoCachedAsync)
|
|
189
|
+
if let url = URL(string: urlString) {
|
|
190
|
+
let fileExtension = url.pathExtension
|
|
191
|
+
if let saveFilePath = VideoAsset.pathForUrl(url: url, fileExtension: fileExtension) {
|
|
192
|
+
let mediaInfoPath = saveFilePath + VideoCacheManager.mediaInfoSuffix
|
|
193
|
+
if let mediaInfo = MediaInfo(at: mediaInfoPath) {
|
|
194
|
+
let expectedLength = Int(mediaInfo.expectedContentLength)
|
|
195
|
+
let ranges = mediaInfo.loadedDataRanges
|
|
196
|
+
if ranges.count == 1, ranges[0].0 == 0, ranges[0].1 == expectedLength - 1 {
|
|
197
|
+
print("[expo-video] File already fully cached, skipping download.")
|
|
198
|
+
return true
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
guard let url = URL(string: urlString) else {
|
|
204
|
+
throw NSError(domain: "VideoCacheManager", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL string"])
|
|
205
|
+
}
|
|
206
|
+
let fileExtension = url.pathExtension
|
|
207
|
+
guard let saveFilePath = VideoAsset.pathForUrl(url: url, fileExtension: fileExtension) else {
|
|
208
|
+
throw NSError(domain: "VideoCacheManager", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to create cache file path"])
|
|
209
|
+
}
|
|
210
|
+
let fileUrl = URL(fileURLWithPath: saveFilePath)
|
|
211
|
+
let mediaInfoPath = saveFilePath + VideoCacheManager.mediaInfoSuffix
|
|
212
|
+
|
|
213
|
+
let (data, response) = try await URLSession.shared.data(from: url)
|
|
214
|
+
try data.write(to: fileUrl, options: .atomic)
|
|
215
|
+
|
|
216
|
+
let expectedLength = Int64(data.count)
|
|
217
|
+
let mimeType = response.mimeType
|
|
218
|
+
let supportsByteRangeAccess = true
|
|
219
|
+
let headerFields: [String: String]? = nil
|
|
220
|
+
let mediaInfo = MediaInfo(expectedContentLength: expectedLength, mimeType: mimeType, supportsByteRangeAccess: supportsByteRangeAccess, headerFields: headerFields, savePath: mediaInfoPath)
|
|
221
|
+
mediaInfo.setLoadedDataRanges([(0, data.count - 1)])
|
|
222
|
+
mediaInfo.saveToFile()
|
|
223
|
+
|
|
224
|
+
// Print current cache size
|
|
225
|
+
let cacheSize = VideoCacheManager.shared.getCacheDirectorySize()
|
|
226
|
+
print("[expo-video] Cache size after caching: \(cacheSize) bytes")
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
static func preCacheVideoPartialAsync(from urlString: String, chunkSize: Int = 1_048_576) async throws -> Bool {
|
|
231
|
+
// Check if already cached (same logic as isVideoCachedAsync)
|
|
232
|
+
if let url = URL(string: urlString) {
|
|
233
|
+
let fileExtension = url.pathExtension
|
|
234
|
+
if let saveFilePath = VideoAsset.pathForUrl(url: url, fileExtension: fileExtension) {
|
|
235
|
+
let mediaInfoPath = saveFilePath + VideoCacheManager.mediaInfoSuffix
|
|
236
|
+
if let mediaInfo = MediaInfo(at: mediaInfoPath) {
|
|
237
|
+
let expectedLength = Int(mediaInfo.expectedContentLength)
|
|
238
|
+
let ranges = mediaInfo.loadedDataRanges
|
|
239
|
+
if ranges.count == 1, ranges[0].0 == 0, ranges[0].1 == expectedLength - 1 {
|
|
240
|
+
print("[expo-video] File already fully cached, skipping download.")
|
|
241
|
+
return true
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
guard let url = URL(string: urlString) else {
|
|
247
|
+
throw NSError(domain: "VideoCacheManager", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL string"])
|
|
248
|
+
}
|
|
249
|
+
let fileExtension = url.pathExtension
|
|
250
|
+
guard let saveFilePath = VideoAsset.pathForUrl(url: url, fileExtension: fileExtension) else {
|
|
251
|
+
throw NSError(domain: "VideoCacheManager", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to create cache file path"])
|
|
252
|
+
}
|
|
253
|
+
let fileUrl = URL(fileURLWithPath: saveFilePath)
|
|
254
|
+
let mediaInfoPath = saveFilePath + VideoCacheManager.mediaInfoSuffix
|
|
255
|
+
|
|
256
|
+
// Ensure the cache directory exists (same as VideoAsset)
|
|
257
|
+
if let cacheDir = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) {
|
|
258
|
+
let videoCacheDir = cacheDir.appendingPathComponent(VideoCacheManager.expoVideoCacheScheme, isDirectory: true)
|
|
259
|
+
try? FileManager.default.createDirectory(at: videoCacheDir, withIntermediateDirectories: true)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Ensure the file exists before opening for writing
|
|
263
|
+
if !FileManager.default.fileExists(atPath: fileUrl.path) {
|
|
264
|
+
FileManager.default.createFile(atPath: fileUrl.path, contents: nil, attributes: nil)
|
|
265
|
+
}
|
|
266
|
+
let fileHandle = try FileHandle(forWritingTo: fileUrl)
|
|
267
|
+
defer { try? fileHandle.close() }
|
|
268
|
+
|
|
269
|
+
// Restore these variable declarations:
|
|
270
|
+
var expectedLength: Int64 = 0
|
|
271
|
+
var mimeType: String? = nil
|
|
272
|
+
var supportsByteRangeAccess = true
|
|
273
|
+
var headerFields: [String: String]? = nil
|
|
274
|
+
var loadedRanges: [(Int, Int)] = []
|
|
275
|
+
|
|
276
|
+
// Make a HEAD request to get expectedLength and headers
|
|
277
|
+
var headRequest = URLRequest(url: url)
|
|
278
|
+
headRequest.httpMethod = "HEAD"
|
|
279
|
+
let (_, headResponse) = try await URLSession.shared.data(for: headRequest)
|
|
280
|
+
if let httpResponse = headResponse as? HTTPURLResponse {
|
|
281
|
+
if let contentLength = httpResponse.value(forHTTPHeaderField: "Content-Length"), let length = Int64(contentLength) {
|
|
282
|
+
expectedLength = length
|
|
283
|
+
}
|
|
284
|
+
mimeType = httpResponse.mimeType
|
|
285
|
+
supportsByteRangeAccess = httpResponse.value(forHTTPHeaderField: "Accept-Ranges") == "bytes"
|
|
286
|
+
headerFields = httpResponse.allHeaderFields as? [String: String]
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
var offset = 0
|
|
290
|
+
while Int64(offset) < expectedLength {
|
|
291
|
+
let end = min(offset + chunkSize - 1, Int(expectedLength) - 1)
|
|
292
|
+
var request = URLRequest(url: url)
|
|
293
|
+
request.setValue("bytes=\(offset)-\(end)", forHTTPHeaderField: "Range")
|
|
294
|
+
let (chunkData, _) = try await URLSession.shared.data(for: request)
|
|
295
|
+
try fileHandle.seek(toOffset: UInt64(offset))
|
|
296
|
+
try fileHandle.write(contentsOf: chunkData)
|
|
297
|
+
loadedRanges.append((offset, offset + chunkData.count - 1))
|
|
298
|
+
offset += chunkData.count
|
|
299
|
+
|
|
300
|
+
let mediaInfo = MediaInfo(expectedContentLength: expectedLength, mimeType: mimeType, supportsByteRangeAccess: supportsByteRangeAccess, headerFields: headerFields, savePath: mediaInfoPath)
|
|
301
|
+
mediaInfo.setLoadedDataRanges(loadedRanges)
|
|
302
|
+
mediaInfo.saveToFile()
|
|
303
|
+
let cacheSize = VideoCacheManager.shared.getCacheDirectorySize()
|
|
304
|
+
print("[expo-video] Cache size after chunk: \(cacheSize) bytes")
|
|
305
|
+
}
|
|
306
|
+
// Merge all ranges before final save
|
|
307
|
+
if !loadedRanges.isEmpty {
|
|
308
|
+
loadedRanges.sort { $0.0 < $1.0 }
|
|
309
|
+
var merged: [(Int, Int)] = []
|
|
310
|
+
for range in loadedRanges {
|
|
311
|
+
if let last = merged.last, last.1 + 1 >= range.0 {
|
|
312
|
+
merged[merged.count - 1] = (last.0, max(last.1, range.1))
|
|
313
|
+
} else {
|
|
314
|
+
merged.append(range)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
loadedRanges = merged
|
|
318
|
+
}
|
|
319
|
+
// Final save
|
|
320
|
+
let mediaInfo = MediaInfo(expectedContentLength: expectedLength, mimeType: mimeType, supportsByteRangeAccess: supportsByteRangeAccess, headerFields: headerFields, savePath: mediaInfoPath)
|
|
321
|
+
mediaInfo.setLoadedDataRanges(loadedRanges)
|
|
322
|
+
mediaInfo.saveToFile()
|
|
323
|
+
let cacheSize = VideoCacheManager.shared.getCacheDirectorySize()
|
|
324
|
+
print("[expo-video] Final cache size: \(cacheSize) bytes")
|
|
325
|
+
print("[expo-video] MediaInfo debug: path=\(mediaInfoPath), loadedDataRanges=\(loadedRanges), expectedLength=\(expectedLength)")
|
|
326
|
+
return false
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private extension UserDefaults {
|
|
331
|
+
func exists(forKey key: String) -> Bool {
|
|
332
|
+
return Self.standard.object(forKey: key) != nil
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
func maybeInteger(forKey key: String) -> Int? {
|
|
336
|
+
Self.standard.exists(forKey: key) ? Self.standard.integer(forKey: key) : nil
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Copyright 2024-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import AVFoundation
|
|
4
|
+
import ExpoModulesCore
|
|
5
|
+
|
|
6
|
+
internal class ContentKeyDelegate: NSObject, AVContentKeySessionDelegate {
|
|
7
|
+
// Video source that is currently being loaded. Used for retrieving information like license and certificate urls
|
|
8
|
+
var videoSource: VideoSource?
|
|
9
|
+
|
|
10
|
+
// MARK: - AVContentKeySessionDelegate
|
|
11
|
+
|
|
12
|
+
func contentKeySession(_ session: AVContentKeySession, didProvide keyRequest: AVContentKeyRequest) {
|
|
13
|
+
handleStreamingContentKeyRequest(keyRequest: keyRequest)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
func contentKeySession(_ session: AVContentKeySession, didProvideRenewingContentKeyRequest keyRequest: AVContentKeyRequest) {
|
|
17
|
+
handleStreamingContentKeyRequest(keyRequest: keyRequest)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func contentKeySession(
|
|
21
|
+
_ session: AVContentKeySession,
|
|
22
|
+
shouldRetry keyRequest: AVContentKeyRequest,
|
|
23
|
+
reason retryReason: AVContentKeyRequest.RetryReason
|
|
24
|
+
) -> Bool {
|
|
25
|
+
var shouldRetry = false
|
|
26
|
+
|
|
27
|
+
switch retryReason {
|
|
28
|
+
case AVContentKeyRequest.RetryReason.timedOut:
|
|
29
|
+
shouldRetry = true
|
|
30
|
+
case AVContentKeyRequest.RetryReason.receivedResponseWithExpiredLease:
|
|
31
|
+
shouldRetry = true
|
|
32
|
+
case AVContentKeyRequest.RetryReason.receivedObsoleteContentKey:
|
|
33
|
+
shouldRetry = true
|
|
34
|
+
default:
|
|
35
|
+
break
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return shouldRetry
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func contentKeySession(_ session: AVContentKeySession, contentKeyRequest keyRequest: AVContentKeyRequest, didFailWithError err: Error) {
|
|
42
|
+
log.error(err)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// MARK: - Private
|
|
46
|
+
|
|
47
|
+
func handleStreamingContentKeyRequest(keyRequest: AVContentKeyRequest) {
|
|
48
|
+
do {
|
|
49
|
+
try provideOnlineKey(keyRequest: keyRequest)
|
|
50
|
+
} catch {
|
|
51
|
+
keyRequest.processContentKeyResponseError(error)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private func provideOnlineKey(keyRequest: AVContentKeyRequest) throws {
|
|
56
|
+
guard
|
|
57
|
+
let assetIdString = findAssetIdString(keyRequest: keyRequest, videoSource: videoSource),
|
|
58
|
+
let assetIdData = assetIdString.data(using: .utf8)
|
|
59
|
+
else {
|
|
60
|
+
throw DRMLoadException("Failed to find the asset id for request: \(String(describing: keyRequest.identifier))")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let applicationCertificate = try self.requestApplicationCertificate(keyRequest: keyRequest)
|
|
64
|
+
|
|
65
|
+
let completionHandler = { [weak self] (spcData: Data?, error: Error?) in
|
|
66
|
+
guard let self else {
|
|
67
|
+
keyRequest.processContentKeyResponseError(DRMLoadException("Couldn't find a reference to the key delegate in the online key completion handler."))
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if let error {
|
|
72
|
+
keyRequest.processContentKeyResponseError(error)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
guard let spcData else {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
do {
|
|
81
|
+
let ckcData = try self.requestContentKeyFromKeySecurityModule(spcData: spcData, assetID: assetIdString, keyRequest: keyRequest)
|
|
82
|
+
let keyResponse = AVContentKeyResponse(fairPlayStreamingKeyResponseData: ckcData)
|
|
83
|
+
keyRequest.processContentKeyResponse(keyResponse)
|
|
84
|
+
} catch {
|
|
85
|
+
keyRequest.processContentKeyResponseError(error)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
keyRequest.makeStreamingContentKeyRequestData(
|
|
90
|
+
forApp: applicationCertificate,
|
|
91
|
+
contentIdentifier: assetIdData,
|
|
92
|
+
options: [AVContentKeyRequestProtocolVersionsKey: [1]],
|
|
93
|
+
completionHandler: completionHandler
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private func requestApplicationCertificate(keyRequest: AVContentKeyRequest) throws -> Data {
|
|
98
|
+
if let certificateData = videoSource?.drm?.base64CertificateData {
|
|
99
|
+
return try requestCertificateFrom(base64String: certificateData)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
guard let url = videoSource?.drm?.certificateUrl else {
|
|
103
|
+
throw DRMLoadException("The certificate uri and data are null")
|
|
104
|
+
}
|
|
105
|
+
return try requestCertificateFrom(url: url)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private func requestCertificateFrom(url: URL) throws -> Data {
|
|
109
|
+
let urlRequest = URLRequest(url: url)
|
|
110
|
+
let (data, response, error) = URLSession.shared.synchronousDataTask(with: urlRequest)
|
|
111
|
+
|
|
112
|
+
guard error == nil else {
|
|
113
|
+
let errorDescription = error?.localizedDescription ?? "unknown error"
|
|
114
|
+
throw DRMLoadException("Failed to load the application certificate from \(url.absoluteString): \(errorDescription)")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if let httpResponse = response as? HTTPURLResponse {
|
|
118
|
+
guard httpResponse.statusCode == 200 else {
|
|
119
|
+
throw DRMLoadException("Fetching the application certificate failed with status: \(httpResponse.statusCode)")
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
guard let data else {
|
|
123
|
+
throw DRMLoadException("Application certificate data received from \(url.absoluteString) is empty")
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
guard SecCertificateCreateWithData(nil, data as CFData) != nil else {
|
|
127
|
+
throw DRMLoadException("The application certificate received from the server is invalid")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return data
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private func requestCertificateFrom(base64String: String) throws -> Data {
|
|
134
|
+
guard let certificateData = Data(base64Encoded: base64String, options: .ignoreUnknownCharacters) else {
|
|
135
|
+
throw DRMLoadException("Failed to load the application certificate from the provided base64 string")
|
|
136
|
+
}
|
|
137
|
+
return certificateData
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private func requestContentKeyFromKeySecurityModule(spcData: Data, assetID: String, keyRequest: AVContentKeyRequest) throws -> Data {
|
|
141
|
+
let ckcData: Data? = nil
|
|
142
|
+
|
|
143
|
+
guard let licenseServerUri = videoSource?.drm?.licenseServer else {
|
|
144
|
+
throw DRMLoadException("LicenseServer uri hasn't been provided")
|
|
145
|
+
}
|
|
146
|
+
guard let licenseServerUrl = URL(string: licenseServerUri) else {
|
|
147
|
+
throw DRMLoadException("LicenseServer uri is invalid")
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
var ckcRequest = URLRequest(url: licenseServerUrl)
|
|
151
|
+
ckcRequest.httpMethod = "POST"
|
|
152
|
+
ckcRequest.httpBody = spcData
|
|
153
|
+
|
|
154
|
+
if let headers = videoSource?.drm?.headers {
|
|
155
|
+
for item in headers {
|
|
156
|
+
guard let value = item.value as? String else {
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
ckcRequest.setValue(value, forHTTPHeaderField: item.key)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let (data, response, error) = URLSession.shared.synchronousDataTask(with: ckcRequest)
|
|
164
|
+
|
|
165
|
+
guard error == nil else {
|
|
166
|
+
throw DRMLoadException("Fetching the content key has failed with error: \(String(describing: error?.localizedDescription))")
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if let httpResponse = response as? HTTPURLResponse {
|
|
170
|
+
guard httpResponse.statusCode == 200 else {
|
|
171
|
+
throw DRMLoadException("Fetching the content key has failed with status: \(httpResponse.statusCode)")
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
guard let data else {
|
|
175
|
+
throw DRMLoadException("Fetched content key data is empty")
|
|
176
|
+
}
|
|
177
|
+
return data
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private func findAssetIdString(keyRequest: AVContentKeyRequest) -> String? {
|
|
181
|
+
let url = keyRequest.identifier as? String
|
|
182
|
+
return url?.replacingOccurrences(of: "skd://", with: "")
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private func findAssetIdString(keyRequest: AVContentKeyRequest, videoSource: VideoSource?) -> String? {
|
|
186
|
+
return videoSource?.drm?.contentId ?? findAssetIdString(keyRequest: keyRequest)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// MARK: - Utils
|
|
191
|
+
|
|
192
|
+
// https://stackoverflow.com/questions/26784315/can-i-somehow-do-a-synchronous-http-request-via-nsurlsession-in-swift
|
|
193
|
+
private extension URLSession {
|
|
194
|
+
func synchronousDataTask(with urlRequest: URLRequest) -> (data: Data?, response: URLResponse?, error: Error?) {
|
|
195
|
+
var data: Data?
|
|
196
|
+
var response: URLResponse?
|
|
197
|
+
var error: Error?
|
|
198
|
+
|
|
199
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
200
|
+
|
|
201
|
+
let dataTask = self.dataTask(with: urlRequest) {
|
|
202
|
+
data = $0
|
|
203
|
+
response = $1
|
|
204
|
+
error = $2
|
|
205
|
+
|
|
206
|
+
semaphore.signal()
|
|
207
|
+
}
|
|
208
|
+
dataTask.resume()
|
|
209
|
+
|
|
210
|
+
_ = semaphore.wait(timeout: .distantFuture)
|
|
211
|
+
|
|
212
|
+
return (data, response, error)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Copyright 2024-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import AVFoundation
|
|
4
|
+
|
|
5
|
+
internal class ContentKeyManager {
|
|
6
|
+
static let contentKeyDelegateQueue = DispatchQueue(label: "dev.expo.video.ExpoVideo.ContentKeyDelegateQueue")
|
|
7
|
+
let contentKeySession: AVContentKeySession
|
|
8
|
+
let contentKeyDelegate: ContentKeyDelegate
|
|
9
|
+
|
|
10
|
+
init() {
|
|
11
|
+
contentKeySession = AVContentKeySession(keySystem: .fairPlayStreaming)
|
|
12
|
+
contentKeyDelegate = ContentKeyDelegate()
|
|
13
|
+
|
|
14
|
+
contentKeySession.setDelegate(contentKeyDelegate, queue: ContentKeyManager.contentKeyDelegateQueue)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func addContentKeyRequest(videoSource: VideoSource, asset: AVContentKeyRecipient) {
|
|
18
|
+
contentKeyDelegate.videoSource = videoSource
|
|
19
|
+
contentKeySession.addContentKeyRecipient(asset)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Copyright 2024-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesCore
|
|
4
|
+
import CoreMedia
|
|
5
|
+
|
|
6
|
+
internal enum AudioMixingMode: String, Enumerable {
|
|
7
|
+
case mixWithOthers
|
|
8
|
+
case duckOthers
|
|
9
|
+
case doNotMix
|
|
10
|
+
case auto
|
|
11
|
+
|
|
12
|
+
func priority() -> Int {
|
|
13
|
+
switch self {
|
|
14
|
+
case .doNotMix:
|
|
15
|
+
return 3
|
|
16
|
+
case .auto:
|
|
17
|
+
return 2
|
|
18
|
+
case .duckOthers:
|
|
19
|
+
return 1
|
|
20
|
+
case .mixWithOthers:
|
|
21
|
+
return 0
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func toSessionCategoryOption() -> AVAudioSession.CategoryOptions? {
|
|
26
|
+
switch self {
|
|
27
|
+
case .duckOthers:
|
|
28
|
+
return .duckOthers
|
|
29
|
+
case .mixWithOthers:
|
|
30
|
+
return .mixWithOthers
|
|
31
|
+
case .doNotMix:
|
|
32
|
+
return nil
|
|
33
|
+
case .auto:
|
|
34
|
+
return nil
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Copyright 2024-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesCore
|
|
4
|
+
|
|
5
|
+
internal enum DRMType: String, Enumerable {
|
|
6
|
+
case clearkey
|
|
7
|
+
case fairplay
|
|
8
|
+
case playready
|
|
9
|
+
case widevine
|
|
10
|
+
|
|
11
|
+
func isSupported() -> Bool {
|
|
12
|
+
return self == .fairplay
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
internal func assertIsSupported() throws {
|
|
16
|
+
if !isSupported() {
|
|
17
|
+
throw DRMUnsupportedException(self)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
internal enum VideoContentFit: String, Enumerable {
|
|
4
|
+
/**
|
|
5
|
+
The video is scaled to maintain its aspect ratio while fitting within the container's box.
|
|
6
|
+
The entire video is made to fill the box, while preserving its aspect ratio,
|
|
7
|
+
so the video will be "letterboxed" if its aspect ratio does not match the aspect ratio of the box.
|
|
8
|
+
*/
|
|
9
|
+
case contain
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
The video is sized to maintain its aspect ratio while filling the element's entire content box.
|
|
13
|
+
If the video's aspect ratio does not match the aspect ratio of its box, then the object will be clipped to fit.
|
|
14
|
+
*/
|
|
15
|
+
case cover
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
The video is sized to fill the element's content box. The entire object will completely fill the box.
|
|
19
|
+
If the video's aspect ratio does not match the aspect ratio of its box, then the video will be stretched to fit.
|
|
20
|
+
*/
|
|
21
|
+
case fill
|
|
22
|
+
|
|
23
|
+
// TODO: Add `none` and `scaleDown`
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
`VideoContentFit` cases can be directly translated to the native `AVLayerVideoGravity`
|
|
27
|
+
except `scaleDown` that needs to be handled differently at the later step of rendering.
|
|
28
|
+
*/
|
|
29
|
+
func toVideoGravity() -> AVLayerVideoGravity {
|
|
30
|
+
switch self {
|
|
31
|
+
case .contain:
|
|
32
|
+
return .resizeAspect
|
|
33
|
+
case .cover:
|
|
34
|
+
return .resizeAspectFill
|
|
35
|
+
case .fill:
|
|
36
|
+
return .resize
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|