@stepincto/expo-video 1.0.3 → 1.0.5
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/build/VideoModule.d.ts.map +1 -1
- package/build/VideoModule.js +24 -1
- package/build/VideoModule.js.map +1 -1
- package/ios/Cache/CachableRequest.swift +2 -37
- package/ios/Cache/CachedResource.swift +1 -1
- package/ios/Cache/MediaFileHandle.swift +10 -8
- package/ios/Cache/MediaInfo.swift +41 -6
- package/ios/Cache/VideoCacheManager.swift +53 -6
- package/ios/VideoAsset.swift +4 -1
- package/package.json +1 -1
- package/src/VideoModule.ts +28 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoModule.d.ts","sourceRoot":"","sources":["../src/VideoModule.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"VideoModule.d.ts","sourceRoot":"","sources":["../src/VideoModule.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AACH,wBAAgB,2BAA2B,IAAI,OAAO,CAErD;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAEpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAOD,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAsBtE;AAED,wBAAsB,yBAAyB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAEjG;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAEhE"}
|
package/build/VideoModule.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import NativeVideoModule from './NativeVideoModule';
|
|
2
|
+
const globalCacheOperations = new Map(); // stores the promises that are currently caching files
|
|
2
3
|
/**
|
|
3
4
|
* Returns whether the current device supports Picture in Picture (PiP) mode.
|
|
4
5
|
*
|
|
@@ -41,8 +42,30 @@ export function setVideoCacheSizeAsync(sizeBytes) {
|
|
|
41
42
|
export function getCurrentVideoCacheSize() {
|
|
42
43
|
return NativeVideoModule.getCurrentVideoCacheSize();
|
|
43
44
|
}
|
|
45
|
+
// export async function preCacheVideoAsync(url: string): Promise<boolean> {
|
|
46
|
+
// return NativeVideoModule.preCacheVideoAsync(url);
|
|
47
|
+
// }
|
|
48
|
+
// Modify the existing function
|
|
44
49
|
export async function preCacheVideoAsync(url) {
|
|
45
|
-
return
|
|
50
|
+
// If already caching, return the existing promise
|
|
51
|
+
if (globalCacheOperations.has(url)) {
|
|
52
|
+
return await globalCacheOperations.get(url);
|
|
53
|
+
}
|
|
54
|
+
// Start new caching operation with proper error handling
|
|
55
|
+
const cachingPromise = NativeVideoModule.preCacheVideoAsync(url).catch((error) => {
|
|
56
|
+
// Ensure cleanup happens even on native errors
|
|
57
|
+
globalCacheOperations.delete(url);
|
|
58
|
+
throw error;
|
|
59
|
+
});
|
|
60
|
+
globalCacheOperations.set(url, cachingPromise);
|
|
61
|
+
try {
|
|
62
|
+
const result = await cachingPromise;
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
// Always cleanup, even if the promise was already cleaned up in catch
|
|
67
|
+
globalCacheOperations.delete(url);
|
|
68
|
+
}
|
|
46
69
|
}
|
|
47
70
|
export async function preCacheVideoPartialAsync(url, chunkSize) {
|
|
48
71
|
return NativeVideoModule.preCacheVideoPartialAsync(url, chunkSize);
|
package/build/VideoModule.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoModule.js","sourceRoot":"","sources":["../src/VideoModule.ts"],"names":[],"mappings":"AAAA,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,UAAU,2BAA2B;IACzC,OAAO,iBAAiB,CAAC,2BAA2B,EAAE,CAAC;AACzD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB;IAClC,OAAO,iBAAiB,CAAC,oBAAoB,EAAE,CAAC;AAClD,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CAAC,SAAiB;IACtD,OAAO,iBAAiB,CAAC,sBAAsB,CAAC,SAAS,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB;IACtC,OAAO,iBAAiB,CAAC,wBAAwB,EAAE,CAAC;AACtD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,GAAW;IAClD,OAAO,iBAAiB,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"VideoModule.js","sourceRoot":"","sources":["../src/VideoModule.ts"],"names":[],"mappings":"AAAA,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AAEpD,MAAM,qBAAqB,GAAG,IAAI,GAAG,EAA4B,CAAC,CAAC,uDAAuD;AAE1H;;;;;;GAMG;AACH,MAAM,UAAU,2BAA2B;IACzC,OAAO,iBAAiB,CAAC,2BAA2B,EAAE,CAAC;AACzD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB;IAClC,OAAO,iBAAiB,CAAC,oBAAoB,EAAE,CAAC;AAClD,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CAAC,SAAiB;IACtD,OAAO,iBAAiB,CAAC,sBAAsB,CAAC,SAAS,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB;IACtC,OAAO,iBAAiB,CAAC,wBAAwB,EAAE,CAAC;AACtD,CAAC;AAED,4EAA4E;AAC5E,sDAAsD;AACtD,IAAI;AAEJ,+BAA+B;AAC/B,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,GAAW;IAClD,kDAAkD;IAClD,IAAI,qBAAqB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QACnC,OAAO,MAAM,qBAAqB,CAAC,GAAG,CAAC,GAAG,CAAE,CAAC;IAC/C,CAAC;IAED,yDAAyD;IACzD,MAAM,cAAc,GAAG,iBAAiB,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;QAC/E,+CAA+C;QAC/C,qBAAqB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAClC,MAAM,KAAK,CAAC;IACd,CAAC,CAAC,CAAC;IAEH,qBAAqB,CAAC,GAAG,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IAE/C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC;QACpC,OAAO,MAAM,CAAC;IAChB,CAAC;YAAS,CAAC;QACT,sEAAsE;QACtE,qBAAqB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACpC,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAAC,GAAW,EAAE,SAAkB;IAC7E,OAAO,iBAAiB,CAAC,yBAAyB,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;AACrE,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,OAAO,iBAAiB,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;AACnD,CAAC","sourcesContent":["import NativeVideoModule from './NativeVideoModule';\n\nconst globalCacheOperations = new Map<string, Promise<boolean>>(); // stores the promises that are currently caching files\n\n/**\n * Returns whether the current device supports Picture in Picture (PiP) mode.\n *\n * @returns A `boolean` which is `true` if the device supports PiP mode, and `false` otherwise.\n * @platform android\n * @platform ios\n */\nexport function isPictureInPictureSupported(): boolean {\n return NativeVideoModule.isPictureInPictureSupported();\n}\n\n/**\n * Clears all video cache.\n * > This function can be called only if there are no existing `VideoPlayer` instances.\n *\n * @returns A promise that fulfills after the cache has been cleaned.\n * @platform android\n * @platform ios\n */\nexport function clearVideoCacheAsync(): Promise<void> {\n return NativeVideoModule.clearVideoCacheAsync();\n}\n\n/**\n * Sets desired video cache size in bytes. The default video cache size is 1GB. Value set by this function is persistent.\n * The cache size is not guaranteed to be exact and the actual cache size may be slightly larger. The cache is evicted on a least-recently-used basis.\n * > This function can be called only if there are no existing `VideoPlayer` instances.\n *\n * @returns A promise that fulfills after the cache size has been set.\n * @platform android\n * @platform ios\n */\nexport function setVideoCacheSizeAsync(sizeBytes: number): Promise<void> {\n return NativeVideoModule.setVideoCacheSizeAsync(sizeBytes);\n}\n\n/**\n * Returns the space currently occupied by the video cache in bytes.\n *\n * @platform android\n * @platform ios\n */\nexport function getCurrentVideoCacheSize(): number {\n return NativeVideoModule.getCurrentVideoCacheSize();\n}\n\n// export async function preCacheVideoAsync(url: string): Promise<boolean> {\n// return NativeVideoModule.preCacheVideoAsync(url);\n// }\n\n// Modify the existing function\nexport async function preCacheVideoAsync(url: string): Promise<boolean> {\n // If already caching, return the existing promise\n if (globalCacheOperations.has(url)) {\n return await globalCacheOperations.get(url)!;\n }\n\n // Start new caching operation with proper error handling\n const cachingPromise = NativeVideoModule.preCacheVideoAsync(url).catch((error) => {\n // Ensure cleanup happens even on native errors\n globalCacheOperations.delete(url);\n throw error;\n });\n \n globalCacheOperations.set(url, cachingPromise);\n\n try {\n const result = await cachingPromise;\n return result;\n } finally {\n // Always cleanup, even if the promise was already cleaned up in catch\n globalCacheOperations.delete(url);\n }\n}\n\nexport async function preCacheVideoPartialAsync(url: string, chunkSize?: number): Promise<boolean> {\n return NativeVideoModule.preCacheVideoPartialAsync(url, chunkSize);\n}\n\nexport function isVideoCachedAsync(url: string): Promise<boolean> {\n return NativeVideoModule.isVideoCachedAsync(url);\n}\n"]}
|
|
@@ -17,8 +17,6 @@ class CachableRequest: Equatable, Hashable {
|
|
|
17
17
|
var response: URLResponse?
|
|
18
18
|
private(set) var receivedData = Data()
|
|
19
19
|
private let dataOffset: Int64
|
|
20
|
-
private static var fileLocks: [String: NSLock] = [:]
|
|
21
|
-
private static let fileLocksQueue = DispatchQueue(label: "expo-video-cache-file-locks")
|
|
22
20
|
|
|
23
21
|
init(loadingRequest: AVAssetResourceLoadingRequest, dataTask: URLSessionDataTask, dataRequest: AVAssetResourceLoadingDataRequest) {
|
|
24
22
|
self.loadingRequest = loadingRequest
|
|
@@ -32,41 +30,8 @@ class CachableRequest: Equatable, Hashable {
|
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
func saveData(to cachedResource: CachedResource) {
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
let expectedLength = Int(mediaInfo.expectedContentLength)
|
|
38
|
-
let ranges = mediaInfo.loadedDataRanges
|
|
39
|
-
if ranges.count == 1, ranges[0].0 == 0, ranges[0].1 == expectedLength - 1 {
|
|
40
|
-
// Already fully cached, do not write
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// 2. Acquire per-file lock
|
|
46
|
-
let filePath = cachedResource.dataPath
|
|
47
|
-
let lock: NSLock = Self.fileLocksQueue.sync {
|
|
48
|
-
if let l = Self.fileLocks[filePath] { return l }
|
|
49
|
-
let l = NSLock()
|
|
50
|
-
Self.fileLocks[filePath] = l
|
|
51
|
-
return l
|
|
52
|
-
}
|
|
53
|
-
if !lock.try() {
|
|
54
|
-
// Another writer is active, skip this write
|
|
55
|
-
return
|
|
56
|
-
}
|
|
57
|
-
defer { lock.unlock() }
|
|
58
|
-
|
|
59
|
-
// 3. Write only missing data
|
|
60
|
-
let offset = dataOffset
|
|
61
|
-
let length = receivedData.count
|
|
62
|
-
let end = offset + Int64(length) - 1
|
|
63
|
-
if cachedResource.canRespondWithData(from: offset, to: end) {
|
|
64
|
-
// This range is already cached, skip
|
|
65
|
-
return
|
|
66
|
-
}
|
|
67
|
-
Task {
|
|
68
|
-
await cachedResource.writeData(data: receivedData, offset: offset)
|
|
69
|
-
}
|
|
33
|
+
// Disabled: Player is now read-only with respect to the cache.
|
|
34
|
+
// Do nothing.
|
|
70
35
|
}
|
|
71
36
|
|
|
72
37
|
static func == (lhs: CachableRequest, rhs: CachableRequest) -> Bool {
|
|
@@ -26,16 +26,17 @@ internal class MediaFileHandle {
|
|
|
26
26
|
return attributes?[.size] as? Int ?? 0
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
private var fileUrl: URL
|
|
30
|
-
URL(
|
|
29
|
+
private var fileUrl: URL {
|
|
30
|
+
URL(fileURLWithPath: filePath)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
init(filePath: String) {
|
|
34
34
|
self.filePath = filePath
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
// Use fileURLWithPath for proper file URL creation
|
|
37
|
+
let url = fileUrl
|
|
38
|
+
VideoCacheManager.shared.registerOpenFile(at: url)
|
|
39
|
+
print("[MediaFileHandle] Registering file URL: \(url.path)")
|
|
39
40
|
|
|
40
41
|
if !FileManager.default.fileExists(atPath: filePath) {
|
|
41
42
|
FileManager.default.createFile(atPath: filePath, contents: nil, attributes: nil)
|
|
@@ -43,9 +44,10 @@ internal class MediaFileHandle {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
deinit {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
// Use fileURLWithPath for proper file URL creation
|
|
48
|
+
let url = fileUrl
|
|
49
|
+
VideoCacheManager.shared.unregisterOpenFile(at: url)
|
|
50
|
+
print("[MediaFileHandle] Unregistering file URL: \(url.path)")
|
|
49
51
|
guard FileManager.default.fileExists(atPath: filePath) else {
|
|
50
52
|
return
|
|
51
53
|
}
|
|
@@ -7,6 +7,7 @@ class MediaInfo: Codable {
|
|
|
7
7
|
var mimeType: String?
|
|
8
8
|
var headerFields: [String: String]?
|
|
9
9
|
var savePath: String
|
|
10
|
+
private var shouldAutoUnregister: Bool = true
|
|
10
11
|
|
|
11
12
|
// Tuples can't be encoded/decoded, so we workaround that with an array
|
|
12
13
|
private var loadedDataRangesArr: [[Int]] = []
|
|
@@ -21,24 +22,57 @@ class MediaInfo: Codable {
|
|
|
21
22
|
|
|
22
23
|
private enum CodingKeys: String, CodingKey {
|
|
23
24
|
case expectedContentLength, supportsByteRangeAccess, mimeType, loadedDataRangesArr, headerFields, savePath
|
|
25
|
+
// Note: shouldAutoUnregister is not encoded/decoded as it's runtime-only
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Custom decoding to handle shouldAutoUnregister
|
|
29
|
+
required init(from decoder: Decoder) throws {
|
|
30
|
+
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
31
|
+
expectedContentLength = try container.decode(Int64.self, forKey: .expectedContentLength)
|
|
32
|
+
supportsByteRangeAccess = try container.decode(Bool.self, forKey: .supportsByteRangeAccess)
|
|
33
|
+
mimeType = try container.decodeIfPresent(String.self, forKey: .mimeType)
|
|
34
|
+
loadedDataRangesArr = try container.decode([[Int]].self, forKey: .loadedDataRangesArr)
|
|
35
|
+
headerFields = try container.decodeIfPresent([String: String].self, forKey: .headerFields)
|
|
36
|
+
savePath = try container.decode(String.self, forKey: .savePath)
|
|
37
|
+
shouldAutoUnregister = true // Default to true for decoded objects
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Custom encoding to exclude shouldAutoUnregister
|
|
41
|
+
func encode(to encoder: Encoder) throws {
|
|
42
|
+
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
43
|
+
try container.encode(expectedContentLength, forKey: .expectedContentLength)
|
|
44
|
+
try container.encode(supportsByteRangeAccess, forKey: .supportsByteRangeAccess)
|
|
45
|
+
try container.encodeIfPresent(mimeType, forKey: .mimeType)
|
|
46
|
+
try container.encode(loadedDataRangesArr, forKey: .loadedDataRangesArr)
|
|
47
|
+
try container.encodeIfPresent(headerFields, forKey: .headerFields)
|
|
48
|
+
try container.encode(savePath, forKey: .savePath)
|
|
24
49
|
}
|
|
25
50
|
|
|
26
|
-
init(expectedContentLength: Int64, mimeType: String?, supportsByteRangeAccess: Bool, headerFields: [String: String]?, savePath: String) {
|
|
51
|
+
init(expectedContentLength: Int64, mimeType: String?, supportsByteRangeAccess: Bool, headerFields: [String: String]?, savePath: String, autoRegister: Bool = true) {
|
|
27
52
|
self.mimeType = mimeType
|
|
28
53
|
self.supportsByteRangeAccess = supportsByteRangeAccess
|
|
29
54
|
self.expectedContentLength = expectedContentLength
|
|
30
55
|
self.headerFields = headerFields
|
|
31
56
|
self.savePath = savePath
|
|
57
|
+
self.shouldAutoUnregister = autoRegister
|
|
32
58
|
self.loadedDataRanges = loadedDataRanges
|
|
33
59
|
|
|
34
|
-
|
|
35
|
-
|
|
60
|
+
// Only auto-register if requested (prevents double registration)
|
|
61
|
+
if autoRegister {
|
|
62
|
+
// Use fileURLWithPath for proper file URL creation instead of URL(string:)
|
|
63
|
+
let fileUrl = URL(fileURLWithPath: savePath)
|
|
64
|
+
VideoCacheManager.shared.registerOpenFile(at: fileUrl)
|
|
65
|
+
print("[MediaInfo] Registering file URL: \(fileUrl.path)")
|
|
36
66
|
}
|
|
37
67
|
}
|
|
38
68
|
|
|
39
69
|
deinit {
|
|
40
|
-
if
|
|
41
|
-
|
|
70
|
+
// Only unregister if we auto-registered (prevents double unregistration)
|
|
71
|
+
if shouldAutoUnregister {
|
|
72
|
+
// Use fileURLWithPath for proper file URL creation instead of URL(string:)
|
|
73
|
+
let fileUrl = URL(fileURLWithPath: savePath)
|
|
74
|
+
VideoCacheManager.shared.unregisterOpenFile(at: fileUrl)
|
|
75
|
+
print("[MediaInfo] Unregistering file URL: \(fileUrl.path)")
|
|
42
76
|
}
|
|
43
77
|
}
|
|
44
78
|
|
|
@@ -50,7 +84,8 @@ class MediaInfo: Codable {
|
|
|
50
84
|
mimeType: mediaInfo.mimeType,
|
|
51
85
|
supportsByteRangeAccess: mediaInfo.supportsByteRangeAccess,
|
|
52
86
|
headerFields: mediaInfo.headerFields,
|
|
53
|
-
savePath: mediaInfo.savePath
|
|
87
|
+
savePath: mediaInfo.savePath,
|
|
88
|
+
autoRegister: true) // Default behavior for loaded MediaInfo
|
|
54
89
|
self.loadedDataRanges = mediaInfo.loadedDataRanges
|
|
55
90
|
} catch {
|
|
56
91
|
return nil
|
|
@@ -13,7 +13,17 @@ class VideoCacheManager {
|
|
|
13
13
|
private let maxCacheSizeKey = "\(VideoCacheManager.expoVideoCacheScheme)/maxCacheSize"
|
|
14
14
|
|
|
15
15
|
// Files currently being used/modified by the player - they will be skipped when clearing the cache
|
|
16
|
-
private
|
|
16
|
+
private let openFilesQueue = DispatchQueue(label: "\(VideoCacheManager.expoVideoCacheScheme)-openFiles-queue", attributes: .concurrent)
|
|
17
|
+
private var _openFiles: Set<URL> = Set()
|
|
18
|
+
|
|
19
|
+
private var openFiles: Set<URL> {
|
|
20
|
+
get {
|
|
21
|
+
return openFilesQueue.sync { _openFiles }
|
|
22
|
+
}
|
|
23
|
+
set {
|
|
24
|
+
openFilesQueue.async(flags: .barrier) { self._openFiles = newValue }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
17
27
|
|
|
18
28
|
// All cache commands such as clean or adding new data should be run on this queue
|
|
19
29
|
let cacheQueue = DispatchQueue(label: "\(VideoCacheManager.expoVideoCacheScheme)-dispatch-queue")
|
|
@@ -23,11 +33,29 @@ class VideoCacheManager {
|
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
func registerOpenFile(at url: URL) {
|
|
26
|
-
|
|
36
|
+
guard url.isFileURL else {
|
|
37
|
+
print("[VideoCacheManager] Warning: Attempting to register non-file URL: \(url)")
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
openFilesQueue.async(flags: .barrier) { [weak self] in
|
|
42
|
+
guard let self = self else { return }
|
|
43
|
+
self._openFiles.insert(url)
|
|
44
|
+
print("[VideoCacheManager] Registered file: \(url.path)")
|
|
45
|
+
}
|
|
27
46
|
}
|
|
28
47
|
|
|
29
48
|
func unregisterOpenFile(at url: URL) {
|
|
30
|
-
|
|
49
|
+
guard url.isFileURL else {
|
|
50
|
+
print("[VideoCacheManager] Warning: Attempting to unregister non-file URL: \(url)")
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
openFilesQueue.async(flags: .barrier) { [weak self] in
|
|
55
|
+
guard let self = self else { return }
|
|
56
|
+
self._openFiles.remove(url)
|
|
57
|
+
print("[VideoCacheManager] Unregistered file: \(url.path)")
|
|
58
|
+
}
|
|
31
59
|
}
|
|
32
60
|
|
|
33
61
|
func setMaxCacheSize(newSize: Int) throws {
|
|
@@ -177,7 +205,9 @@ class VideoCacheManager {
|
|
|
177
205
|
}
|
|
178
206
|
|
|
179
207
|
private func fileIsOpen(url: URL) -> Bool {
|
|
180
|
-
return
|
|
208
|
+
return openFilesQueue.sync {
|
|
209
|
+
return _openFiles.contains(url) || _openFiles.contains { $0.relativePath == url.relativePath }
|
|
210
|
+
}
|
|
181
211
|
}
|
|
182
212
|
}
|
|
183
213
|
|
|
@@ -210,6 +240,14 @@ extension VideoCacheManager {
|
|
|
210
240
|
let fileUrl = URL(fileURLWithPath: saveFilePath)
|
|
211
241
|
let mediaInfoPath = saveFilePath + VideoCacheManager.mediaInfoSuffix
|
|
212
242
|
|
|
243
|
+
// CRITICAL FIX: Register file as open BEFORE any operations to prevent cache cleanup race condition
|
|
244
|
+
VideoCacheManager.shared.registerOpenFile(at: fileUrl)
|
|
245
|
+
|
|
246
|
+
defer {
|
|
247
|
+
// Always unregister when done, even if there's an error
|
|
248
|
+
VideoCacheManager.shared.unregisterOpenFile(at: fileUrl)
|
|
249
|
+
}
|
|
250
|
+
|
|
213
251
|
// Ensure parent directory exists before writing file
|
|
214
252
|
let parentDir = fileUrl.deletingLastPathComponent()
|
|
215
253
|
do {
|
|
@@ -226,14 +264,23 @@ extension VideoCacheManager {
|
|
|
226
264
|
let mimeType = response.mimeType
|
|
227
265
|
let supportsByteRangeAccess = true
|
|
228
266
|
let headerFields: [String: String]? = nil
|
|
229
|
-
|
|
267
|
+
|
|
268
|
+
// Create MediaInfo without auto-registration (we already registered above)
|
|
269
|
+
let mediaInfo = MediaInfo(
|
|
270
|
+
expectedContentLength: expectedLength,
|
|
271
|
+
mimeType: mimeType,
|
|
272
|
+
supportsByteRangeAccess: supportsByteRangeAccess,
|
|
273
|
+
headerFields: headerFields,
|
|
274
|
+
savePath: mediaInfoPath,
|
|
275
|
+
autoRegister: false
|
|
276
|
+
)
|
|
230
277
|
mediaInfo.setLoadedDataRanges([(0, data.count - 1)])
|
|
231
278
|
mediaInfo.saveToFile()
|
|
232
279
|
|
|
233
280
|
// Print current cache size
|
|
234
281
|
let cacheSize = VideoCacheManager.shared.getCacheDirectorySize()
|
|
235
282
|
print("[expo-video] Cache size after caching: \(cacheSize) bytes")
|
|
236
|
-
return
|
|
283
|
+
return true
|
|
237
284
|
}
|
|
238
285
|
|
|
239
286
|
static func preCacheVideoPartialAsync(from urlString: String, chunkSize: Int = 1_048_576) async throws -> Bool {
|
package/ios/VideoAsset.swift
CHANGED
|
@@ -72,8 +72,11 @@ internal class VideoAsset: AVURLAsset, @unchecked Sendable {
|
|
|
72
72
|
guard useCaching else {
|
|
73
73
|
return
|
|
74
74
|
}
|
|
75
|
-
if let saveFilePath
|
|
75
|
+
if let saveFilePath {
|
|
76
|
+
// Use fileURLWithPath for proper file URL creation instead of URL(string:)
|
|
77
|
+
let cachedFileUrl = URL(fileURLWithPath: saveFilePath)
|
|
76
78
|
VideoCacheManager.shared.unregisterOpenFile(at: cachedFileUrl)
|
|
79
|
+
print("[VideoAsset] Unregistering file URL: \(cachedFileUrl.path)")
|
|
77
80
|
}
|
|
78
81
|
VideoCacheManager.shared.ensureCacheSize()
|
|
79
82
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stepincto/expo-video",
|
|
3
3
|
"title": "Expo Video",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.5",
|
|
5
5
|
"originalUpstreamVersion": "2.2.2",
|
|
6
6
|
"description": "A cross-platform, performant video component for React Native and Expo with Web support",
|
|
7
7
|
"main": "build/index.js",
|
package/src/VideoModule.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import NativeVideoModule from './NativeVideoModule';
|
|
2
2
|
|
|
3
|
+
const globalCacheOperations = new Map<string, Promise<boolean>>(); // stores the promises that are currently caching files
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Returns whether the current device supports Picture in Picture (PiP) mode.
|
|
5
7
|
*
|
|
@@ -46,8 +48,33 @@ export function getCurrentVideoCacheSize(): number {
|
|
|
46
48
|
return NativeVideoModule.getCurrentVideoCacheSize();
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
// export async function preCacheVideoAsync(url: string): Promise<boolean> {
|
|
52
|
+
// return NativeVideoModule.preCacheVideoAsync(url);
|
|
53
|
+
// }
|
|
54
|
+
|
|
55
|
+
// Modify the existing function
|
|
49
56
|
export async function preCacheVideoAsync(url: string): Promise<boolean> {
|
|
50
|
-
return
|
|
57
|
+
// If already caching, return the existing promise
|
|
58
|
+
if (globalCacheOperations.has(url)) {
|
|
59
|
+
return await globalCacheOperations.get(url)!;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Start new caching operation with proper error handling
|
|
63
|
+
const cachingPromise = NativeVideoModule.preCacheVideoAsync(url).catch((error) => {
|
|
64
|
+
// Ensure cleanup happens even on native errors
|
|
65
|
+
globalCacheOperations.delete(url);
|
|
66
|
+
throw error;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
globalCacheOperations.set(url, cachingPromise);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const result = await cachingPromise;
|
|
73
|
+
return result;
|
|
74
|
+
} finally {
|
|
75
|
+
// Always cleanup, even if the promise was already cleaned up in catch
|
|
76
|
+
globalCacheOperations.delete(url);
|
|
77
|
+
}
|
|
51
78
|
}
|
|
52
79
|
|
|
53
80
|
export async function preCacheVideoPartialAsync(url: string, chunkSize?: number): Promise<boolean> {
|