snapback4-expo 0.2.22 → 0.2.24
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/ios/MediaFiles.swift +89 -0
- package/ios/Snapback4Device.xcframework/Info.plist +4 -4
- package/ios/Snapback4Device.xcframework/ios-arm64/libsnapback4_device.a +0 -0
- package/ios/Snapback4Device.xcframework/ios-arm64-simulator/libsnapback4_device.a +0 -0
- package/ios/Snapback4Module.swift +13 -32
- package/package.json +1 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
// @ref LLP 3000.007#8 — ordinary media stays in its device store's private cache
|
|
4
|
+
// The module serializes access with its existing device lock.
|
|
5
|
+
final class MediaFiles {
|
|
6
|
+
enum Failure: String, Error, LocalizedError {
|
|
7
|
+
case invalidIdentity = "invalid media id or closed device"
|
|
8
|
+
case invalidBytes = "invalid media bytes"
|
|
9
|
+
var errorDescription: String? { rawValue }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
private var directory: URL?
|
|
13
|
+
init(directory: URL) { self.directory = directory }
|
|
14
|
+
func close() { directory = nil }
|
|
15
|
+
|
|
16
|
+
private func base(_ id: String) throws -> URL {
|
|
17
|
+
guard let directory, !id.isEmpty,
|
|
18
|
+
id.utf8.allSatisfy({ ($0 >= 48 && $0 <= 57) || ($0 >= 65 && $0 <= 90) || ($0 >= 97 && $0 <= 122) })
|
|
19
|
+
else { throw Failure.invalidIdentity }
|
|
20
|
+
return directory.appendingPathComponent(id)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Match the server's supported brands in the initial ftyp box. This is a
|
|
24
|
+
// filename hint for already-admitted bytes, not another media admission rule.
|
|
25
|
+
private func isMP4(_ bytes: Data) -> Bool {
|
|
26
|
+
guard bytes.count >= 12, bytes[4..<8].elementsEqual("ftyp".utf8) else { return false }
|
|
27
|
+
let size32 = bytes.prefix(4).reduce(UInt64(0)) { ($0 << 8) | UInt64($1) }
|
|
28
|
+
let header = size32 == 1 ? 16 : 8
|
|
29
|
+
guard bytes.count >= header else { return false }
|
|
30
|
+
let size = size32 == 0 ? UInt64(bytes.count) : size32 == 1
|
|
31
|
+
? bytes[8..<16].reduce(UInt64(0)) { ($0 << 8) | UInt64($1) } : size32
|
|
32
|
+
guard size >= UInt64(header), size <= UInt64(bytes.count) else { return false }
|
|
33
|
+
let brands = ["isom", "iso2", "iso5", "iso6", "mp41", "mp42", "avc1", "M4V ", "qt "]
|
|
34
|
+
for offset in stride(from: header, to: Int(size) - 3, by: 4) where offset != header + 4 {
|
|
35
|
+
if let brand = String(data: bytes[offset..<offset + 4], encoding: .ascii), brands.contains(brand) { return true }
|
|
36
|
+
}
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private func resolve(_ id: String) throws -> URL {
|
|
41
|
+
let file = try base(id), video = file.appendingPathExtension("mp4")
|
|
42
|
+
let manager = FileManager.default
|
|
43
|
+
if manager.fileExists(atPath: file.path) {
|
|
44
|
+
// A bare file is either a legacy entry or the latest atomic write. It
|
|
45
|
+
// wins if interruption left both variants; stale bytes cannot reappear.
|
|
46
|
+
if isMP4(try Data(contentsOf: file, options: .mappedIfSafe)) {
|
|
47
|
+
if manager.fileExists(atPath: video.path) {
|
|
48
|
+
_ = try manager.replaceItemAt(video, withItemAt: file, options: .usingNewMetadataOnly)
|
|
49
|
+
} else { try manager.moveItem(at: file, to: video) }
|
|
50
|
+
return video
|
|
51
|
+
}
|
|
52
|
+
if manager.fileExists(atPath: video.path) { try manager.removeItem(at: video) }
|
|
53
|
+
return file
|
|
54
|
+
}
|
|
55
|
+
return manager.fileExists(atPath: video.path) ? video : file
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func read(_ id: String) throws -> String? {
|
|
59
|
+
let file = try resolve(id)
|
|
60
|
+
guard FileManager.default.fileExists(atPath: file.path) else { return nil }
|
|
61
|
+
return try Data(contentsOf: file).map { String(format: "%02x", $0) }.joined()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func write(_ id: String, hex: String) throws {
|
|
65
|
+
let file = try base(id), chars = Array(hex.utf8)
|
|
66
|
+
guard chars.count % 2 == 0 else { throw Failure.invalidBytes }
|
|
67
|
+
var bytes = Data(capacity: chars.count / 2)
|
|
68
|
+
for i in stride(from: 0, to: chars.count, by: 2) {
|
|
69
|
+
guard let byte = UInt8(String(bytes: chars[i...i + 1], encoding: .utf8) ?? "", radix: 16) else { throw Failure.invalidBytes }
|
|
70
|
+
bytes.append(byte)
|
|
71
|
+
}
|
|
72
|
+
try FileManager.default.createDirectory(at: file.deletingLastPathComponent(), withIntermediateDirectories: true)
|
|
73
|
+
// One atomic commit point regardless of format. Resolution renames this
|
|
74
|
+
// same file within the cache; it never creates a separate display copy.
|
|
75
|
+
try bytes.write(to: file, options: .atomic)
|
|
76
|
+
_ = try resolve(id)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func remove(_ id: String) throws {
|
|
80
|
+
let file = try base(id)
|
|
81
|
+
// Remove the possible shadow first. If it cannot be removed, leave the
|
|
82
|
+
// authoritative bare file in place and report the error for retry.
|
|
83
|
+
for variant in [file.appendingPathExtension("mp4"), file] {
|
|
84
|
+
if FileManager.default.fileExists(atPath: variant.path) { try FileManager.default.removeItem(at: variant) }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func uri(_ id: String) throws -> String { try resolve(id).absoluteString }
|
|
89
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<key>HeadersPath</key>
|
|
11
11
|
<string>Headers</string>
|
|
12
12
|
<key>LibraryIdentifier</key>
|
|
13
|
-
<string>ios-arm64
|
|
13
|
+
<string>ios-arm64</string>
|
|
14
14
|
<key>LibraryPath</key>
|
|
15
15
|
<string>libsnapback4_device.a</string>
|
|
16
16
|
<key>SupportedArchitectures</key>
|
|
@@ -19,8 +19,6 @@
|
|
|
19
19
|
</array>
|
|
20
20
|
<key>SupportedPlatform</key>
|
|
21
21
|
<string>ios</string>
|
|
22
|
-
<key>SupportedPlatformVariant</key>
|
|
23
|
-
<string>simulator</string>
|
|
24
22
|
</dict>
|
|
25
23
|
<dict>
|
|
26
24
|
<key>BinaryPath</key>
|
|
@@ -28,7 +26,7 @@
|
|
|
28
26
|
<key>HeadersPath</key>
|
|
29
27
|
<string>Headers</string>
|
|
30
28
|
<key>LibraryIdentifier</key>
|
|
31
|
-
<string>ios-arm64</string>
|
|
29
|
+
<string>ios-arm64-simulator</string>
|
|
32
30
|
<key>LibraryPath</key>
|
|
33
31
|
<string>libsnapback4_device.a</string>
|
|
34
32
|
<key>SupportedArchitectures</key>
|
|
@@ -37,6 +35,8 @@
|
|
|
37
35
|
</array>
|
|
38
36
|
<key>SupportedPlatform</key>
|
|
39
37
|
<string>ios</string>
|
|
38
|
+
<key>SupportedPlatformVariant</key>
|
|
39
|
+
<string>simulator</string>
|
|
40
40
|
</dict>
|
|
41
41
|
</array>
|
|
42
42
|
<key>CFBundlePackageType</key>
|
|
Binary file
|
|
Binary file
|
|
@@ -6,7 +6,7 @@ import Snapback4Device
|
|
|
6
6
|
// link and the transport. Handles are small integers the module hands out.
|
|
7
7
|
public final class Snapback4Module: Module {
|
|
8
8
|
private var devices: [Int: OpaquePointer] = [:]
|
|
9
|
-
private var
|
|
9
|
+
private var mediaFiles: [Int: MediaFiles] = [:]
|
|
10
10
|
private var nextHandle = 1
|
|
11
11
|
private let lock = NSLock()
|
|
12
12
|
|
|
@@ -37,7 +37,7 @@ public final class Snapback4Module: Module {
|
|
|
37
37
|
let handle = self.nextHandle
|
|
38
38
|
self.nextHandle += 1
|
|
39
39
|
self.devices[handle] = device
|
|
40
|
-
self.
|
|
40
|
+
self.mediaFiles[handle] = MediaFiles(directory: URL(fileURLWithPath: path + ".media", isDirectory: true))
|
|
41
41
|
return handle
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -57,52 +57,33 @@ public final class Snapback4Module: Module {
|
|
|
57
57
|
|
|
58
58
|
// @ref LLP 3000.007#8 — the opener owns ordinary media in Documents
|
|
59
59
|
AsyncFunction("readAsset") { (handle: Int, id: String) -> String? in
|
|
60
|
-
self.
|
|
61
|
-
defer { self.lock.unlock() }
|
|
62
|
-
let file = try self.assetFile(handle, id)
|
|
63
|
-
guard FileManager.default.fileExists(atPath: file.path) else { return nil }
|
|
64
|
-
return try Data(contentsOf: file).map { String(format: "%02x", $0) }.joined()
|
|
60
|
+
try self.withMedia(handle) { try $0.read(id) }
|
|
65
61
|
}
|
|
66
62
|
AsyncFunction("writeAsset") { (handle: Int, id: String, hex: String) in
|
|
67
|
-
self.
|
|
68
|
-
defer { self.lock.unlock() }
|
|
69
|
-
let file = try self.assetFile(handle, id)
|
|
70
|
-
let chars = Array(hex.utf8)
|
|
71
|
-
guard chars.count % 2 == 0 else { throw Snapback4OpenException("invalid media bytes") }
|
|
72
|
-
var bytes = Data(capacity: chars.count / 2)
|
|
73
|
-
for i in stride(from: 0, to: chars.count, by: 2) {
|
|
74
|
-
guard let byte = UInt8(String(bytes: chars[i...i+1], encoding: .utf8) ?? "", radix: 16) else { throw Snapback4OpenException("invalid media bytes") }
|
|
75
|
-
bytes.append(byte)
|
|
76
|
-
}
|
|
77
|
-
try FileManager.default.createDirectory(at: file.deletingLastPathComponent(), withIntermediateDirectories: true)
|
|
78
|
-
try bytes.write(to: file, options: .atomic)
|
|
63
|
+
try self.withMedia(handle) { try $0.write(id, hex: hex) }
|
|
79
64
|
}
|
|
80
65
|
AsyncFunction("removeAsset") { (handle: Int, id: String) in
|
|
81
|
-
self.
|
|
82
|
-
defer { self.lock.unlock() }
|
|
83
|
-
let file = try self.assetFile(handle, id)
|
|
84
|
-
if FileManager.default.fileExists(atPath: file.path) { try FileManager.default.removeItem(at: file) }
|
|
66
|
+
try self.withMedia(handle) { try $0.remove(id) }
|
|
85
67
|
}
|
|
86
68
|
Function("assetUri") { (handle: Int, id: String) -> String in
|
|
87
|
-
self.
|
|
88
|
-
defer { self.lock.unlock() }
|
|
89
|
-
return try self.assetFile(handle, id).absoluteString
|
|
69
|
+
try self.withMedia(handle) { try $0.uri(id) }
|
|
90
70
|
}
|
|
91
71
|
|
|
92
72
|
Function("close") { (handle: Int) in
|
|
93
73
|
self.lock.lock()
|
|
94
74
|
defer { self.lock.unlock() }
|
|
95
|
-
self.
|
|
75
|
+
self.mediaFiles.removeValue(forKey: handle)?.close()
|
|
96
76
|
if let device = self.devices.removeValue(forKey: handle) {
|
|
97
77
|
sb4_close(device)
|
|
98
78
|
}
|
|
99
79
|
}
|
|
100
80
|
}
|
|
101
|
-
private func
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
else { throw Snapback4OpenException("invalid media id or closed device") }
|
|
105
|
-
return
|
|
81
|
+
private func withMedia<T>(_ handle: Int, _ body: (MediaFiles) throws -> T) throws -> T {
|
|
82
|
+
lock.lock()
|
|
83
|
+
defer { lock.unlock() }
|
|
84
|
+
guard let files = mediaFiles[handle] else { throw Snapback4OpenException("invalid media id or closed device") }
|
|
85
|
+
do { return try body(files) }
|
|
86
|
+
catch let failure as MediaFiles.Failure { throw Snapback4OpenException(failure.rawValue) }
|
|
106
87
|
}
|
|
107
88
|
}
|
|
108
89
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snapback4-expo",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.24",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Snapback 4's native module for Expo: the device in Rust (the partition in its own SQLite, the stream applied, the outbox kept, queries and predictions answered) behind one JSON call, and the opener `snapback4/native` takes.",
|
|
6
6
|
"main": "src/index.ts",
|