@expo/build-tools 20.4.0 → 21.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 (69) hide show
  1. package/dist/android/gradle.d.ts +5 -0
  2. package/dist/android/gradle.js +13 -33
  3. package/dist/common/installDependencies.js +21 -18
  4. package/dist/common/projectSources.js +1 -1
  5. package/dist/context.d.ts +2 -2
  6. package/dist/context.js +31 -1
  7. package/dist/ios/fastlane.d.ts +1 -1
  8. package/dist/steps/easFunctions.js +18 -0
  9. package/dist/steps/functions/capturePosthogEvent.d.ts +2 -0
  10. package/dist/steps/functions/capturePosthogEvent.js +79 -0
  11. package/dist/steps/functions/createPosthogAnnotation.d.ts +2 -0
  12. package/dist/steps/functions/createPosthogAnnotation.js +75 -0
  13. package/dist/steps/functions/createSubmissionEntity.js +25 -1
  14. package/dist/steps/functions/downloadArtifact.js +13 -3
  15. package/dist/steps/functions/finishIosSimulatorRecordings.d.ts +2 -0
  16. package/dist/steps/functions/finishIosSimulatorRecordings.js +24 -0
  17. package/dist/steps/functions/repack.js +12 -7
  18. package/dist/steps/functions/restoreCache.js +2 -1
  19. package/dist/steps/functions/rolloutPosthogFlag.d.ts +2 -0
  20. package/dist/steps/functions/rolloutPosthogFlag.js +208 -0
  21. package/dist/steps/functions/saveCache.js +3 -2
  22. package/dist/steps/functions/startAgentDeviceRemoteSession.js +21 -8
  23. package/dist/steps/functions/startArgentRemoteSession.d.ts +14 -0
  24. package/dist/steps/functions/startArgentRemoteSession.js +105 -44
  25. package/dist/steps/functions/startIosSimulator.js +19 -0
  26. package/dist/steps/functions/startIosSimulatorRecordings.d.ts +2 -0
  27. package/dist/steps/functions/startIosSimulatorRecordings.js +20 -0
  28. package/dist/steps/functions/startServeSimRemoteSession.js +7 -5
  29. package/dist/steps/functions/uploadDeviceRunSessionScreenRecordings.d.ts +3 -0
  30. package/dist/steps/functions/uploadDeviceRunSessionScreenRecordings.js +91 -0
  31. package/dist/steps/functions/uploadPosthogSourcemaps.d.ts +2 -0
  32. package/dist/steps/functions/uploadPosthogSourcemaps.js +83 -0
  33. package/dist/steps/functions/waitForPosthogMetric.d.ts +2 -0
  34. package/dist/steps/functions/waitForPosthogMetric.js +135 -0
  35. package/dist/steps/functions/waitForPosthogQuery.d.ts +2 -0
  36. package/dist/steps/functions/waitForPosthogQuery.js +95 -0
  37. package/dist/steps/utils/IosSimulatorRecordingUtils.d.ts +17 -0
  38. package/dist/steps/utils/IosSimulatorRecordingUtils.js +218 -0
  39. package/dist/steps/utils/PosthogClient.d.ts +46 -0
  40. package/dist/steps/utils/PosthogClient.js +156 -0
  41. package/dist/steps/utils/PosthogUtils.d.ts +41 -0
  42. package/dist/steps/utils/PosthogUtils.js +65 -0
  43. package/dist/steps/utils/agentDeviceArtifacts.d.ts +27 -0
  44. package/dist/steps/utils/agentDeviceArtifacts.js +133 -0
  45. package/dist/steps/utils/android/gradle.d.ts +5 -0
  46. package/dist/steps/utils/android/gradle.js +13 -62
  47. package/dist/steps/utils/argentArtifacts.d.ts +5 -3
  48. package/dist/steps/utils/argentArtifacts.js +95 -23
  49. package/dist/steps/utils/deviceRunSessionArtifacts.d.ts +3 -1
  50. package/dist/steps/utils/deviceRunSessionArtifacts.js +6 -2
  51. package/dist/steps/utils/remoteDeviceRunSession.d.ts +8 -0
  52. package/dist/steps/utils/remoteDeviceRunSession.js +62 -1
  53. package/dist/utils/AndroidEmulatorUtils.js +3 -0
  54. package/dist/utils/IosSimulatorUtils.d.ts +5 -0
  55. package/dist/utils/IosSimulatorUtils.js +81 -2
  56. package/dist/utils/expoUpdates.js +6 -1
  57. package/dist/utils/expoUpdatesEmbedded.js +4 -0
  58. package/dist/utils/processes.d.ts +1 -0
  59. package/dist/utils/processes.js +23 -0
  60. package/package.json +9 -8
  61. package/resources/record-sim/Package.swift +28 -0
  62. package/resources/record-sim/README.md +79 -0
  63. package/resources/record-sim/Sources/RecordSim/FramebufferDisplaySource.swift +192 -0
  64. package/resources/record-sim/Sources/RecordSim/PrivateSimulatorServices.swift +96 -0
  65. package/resources/record-sim/Sources/RecordSim/RecordingModels.swift +70 -0
  66. package/resources/record-sim/Sources/RecordSim/RecordingOutputWriter.swift +136 -0
  67. package/resources/record-sim/Sources/RecordSim/SimulatorRecorder.swift +632 -0
  68. package/resources/record-sim/Sources/RecordSim/Utilities.swift +60 -0
  69. package/resources/record-sim/Sources/record-sim/main.swift +143 -0
@@ -0,0 +1,79 @@
1
+ # RecordSim
2
+
3
+ Small Swift library for non-exclusive iOS Simulator screen recording.
4
+
5
+ It implements one recording path:
6
+
7
+ 1. Listen to SimRenderServer framebuffer callbacks.
8
+ 2. Copy the live `IOSurface` immediately into an owned `CVPixelBuffer`.
9
+ 3. Feed one continuous `AVAssetWriter`.
10
+ 4. Emit Apple HLS/CMAF-style fragmented MP4 segments.
11
+
12
+ The output is suitable for uploading while recording. `session.json` contains
13
+ the ordered segment metadata needed to build a playlist later.
14
+
15
+ ## Output
16
+
17
+ ```text
18
+ session/
19
+ init.mp4
20
+ session.json
21
+ segments/
22
+ segment-000000.m4s
23
+ segment-000001.m4s
24
+ ```
25
+
26
+ `init.mp4` is the fMP4 initialization segment. Individual `.m4s` files are not
27
+ standalone MP4 files. `session.json` stores the wall-clock timestamp for the
28
+ first video frame plus the metadata needed to build a playlist: HLS version,
29
+ target duration, media sequence, `initSegment`, and the ordered `segments` array
30
+ with file path and duration for each media segment.
31
+
32
+ ## CLI
33
+
34
+ ```bash
35
+ swift run record-sim \
36
+ --udid <BOOTED_SIMULATOR_UDID> \
37
+ --output /tmp/sim-recording \
38
+ --segment-duration 120 \
39
+ --fps 30 \
40
+ --bitrate 30000000 \
41
+ --codec h264
42
+ ```
43
+
44
+ For sparse 2h sessions, `--segment-duration 120` is a reasonable default: about
45
+ 60 media objects plus the init segment. The recorder appends a single duplicate
46
+ hold frame near each segment boundary when the simulator is idle, so long idle
47
+ periods still produce uploadable segments without encoding idle frames at 60fps.
48
+ Use `--segment-duration 0` to write one continuous `recording.mp4` instead of
49
+ `init.mp4` plus media segments.
50
+
51
+ Frame timestamps use monotonic host time, not wall-clock time. The recorder also
52
+ probes the framebuffer seed once per second; if the surface changes without
53
+ callbacks for 5 seconds, it captures that frame and rewires the private callback
54
+ registration.
55
+
56
+ ## Library
57
+
58
+ ```swift
59
+ let recorder = SimulatorRecorder(
60
+ configuration: SimulatorRecordingConfiguration(
61
+ deviceUDID: udid,
62
+ outputDirectory: sessionDirectory,
63
+ segmentDuration: 120
64
+ )
65
+ )
66
+
67
+ recorder.onSegment = { segment in
68
+ // Enqueue upload of segment.url.
69
+ }
70
+
71
+ try recorder.start()
72
+ try recorder.waitUntilFirstFrame()
73
+ // ...
74
+ let manifest = try recorder.stop()
75
+ ```
76
+
77
+ `session.json` contains the precise wall-clock time for video PTS zero. In
78
+ segmented mode it also contains the init segment and ordered media segment
79
+ file/duration entries; in single-file mode it points at `recording.mp4`.
@@ -0,0 +1,192 @@
1
+ import Foundation
2
+ import IOSurface
3
+ import ObjectiveC
4
+
5
+ final class FramebufferDisplaySource {
6
+ struct SurfaceSnapshot {
7
+ let surface: IOSurface
8
+ let width: Int
9
+ let height: Int
10
+ let seed: UInt32
11
+ }
12
+
13
+ private let deviceUDID: String
14
+ private let callbackQueue: DispatchQueue
15
+ private let onFrame: () -> Void
16
+ private let onSurfaceChange: () -> Void
17
+ private var descriptors: [NSObject] = []
18
+ private var callbackUUIDs: [ObjectIdentifier: NSUUID] = [:]
19
+ private var retainedBlocks: [AnyObject] = []
20
+ private var ioClient: NSObject?
21
+
22
+ init(
23
+ deviceUDID: String,
24
+ callbackQueue: DispatchQueue,
25
+ onFrame: @escaping () -> Void,
26
+ onSurfaceChange: @escaping () -> Void
27
+ ) {
28
+ self.deviceUDID = deviceUDID
29
+ self.callbackQueue = callbackQueue
30
+ self.onFrame = onFrame
31
+ self.onSurfaceChange = onSurfaceChange
32
+ }
33
+
34
+ func start() throws {
35
+ try PrivateSimulatorFrameworks.load()
36
+ guard let device = SimulatorDeviceLookup.find(udid: deviceUDID) else {
37
+ throw RecorderError.make(1, "Simulator \(deviceUDID) not found")
38
+ }
39
+ let state = device.value(forKey: "stateString") as? String ?? "unknown"
40
+ guard state == "Booted" else {
41
+ throw RecorderError.make(2, "Simulator \(deviceUDID) is not booted (state: \(state))")
42
+ }
43
+ guard
44
+ let io = device.perform(NSSelectorFromString("io"))?.takeUnretainedValue() as? NSObject
45
+ else {
46
+ throw RecorderError.make(3, "Failed to get simulator IO client")
47
+ }
48
+ ioClient = io
49
+ try wireUpFramebuffer()
50
+ }
51
+
52
+ func stop() {
53
+ unregisterCallbacks()
54
+ descriptors.removeAll()
55
+ retainedBlocks.removeAll()
56
+ ioClient = nil
57
+ }
58
+
59
+ func surfaceSnapshot() -> SurfaceSnapshot? {
60
+ let surfaceSelector = NSSelectorFromString("framebufferSurface")
61
+ var bestSnapshot: SurfaceSnapshot?
62
+ var bestArea = 0
63
+ for descriptor in descriptors {
64
+ guard let surfaceObject = descriptor.perform(surfaceSelector)?.takeUnretainedValue()
65
+ else {
66
+ continue
67
+ }
68
+ let surface = unsafeBitCast(surfaceObject, to: IOSurface.self)
69
+ let width = IOSurfaceGetWidth(surface)
70
+ let height = IOSurfaceGetHeight(surface)
71
+ let area = width * height
72
+ if area > bestArea {
73
+ bestSnapshot = SurfaceSnapshot(
74
+ surface: surface,
75
+ width: width,
76
+ height: height,
77
+ seed: IOSurfaceGetSeed(surface)
78
+ )
79
+ bestArea = area
80
+ }
81
+ }
82
+ return bestSnapshot
83
+ }
84
+
85
+ func rewireFramebuffer() throws {
86
+ try wireUpFramebuffer()
87
+ }
88
+
89
+ private func wireUpFramebuffer() throws {
90
+ guard let io = ioClient else {
91
+ throw RecorderError.make(3, "No simulator IO client")
92
+ }
93
+ io.perform(NSSelectorFromString("updateIOPorts"))
94
+ let nextDescriptors = try findFramebufferDescriptors(io: io)
95
+ unregisterCallbacks()
96
+ descriptors = nextDescriptors
97
+ retainedBlocks.removeAll()
98
+ do {
99
+ for descriptor in descriptors {
100
+ try registerCallbacks(descriptor: descriptor)
101
+ }
102
+ } catch {
103
+ unregisterCallbacks()
104
+ descriptors.removeAll()
105
+ retainedBlocks.removeAll()
106
+ throw error
107
+ }
108
+ }
109
+
110
+ private func findFramebufferDescriptors(io: NSObject) throws -> [NSObject] {
111
+ guard let ports = io.value(forKey: "deviceIOPorts") as? [NSObject] else {
112
+ throw RecorderError.make(4, "Failed to read simulator IO ports")
113
+ }
114
+ let portIdentifierSelector = NSSelectorFromString("portIdentifier")
115
+ let descriptorSelector = NSSelectorFromString("descriptor")
116
+ let surfaceSelector = NSSelectorFromString("framebufferSurface")
117
+
118
+ var candidates: [NSObject] = []
119
+ for port in ports {
120
+ guard port.responds(to: portIdentifierSelector),
121
+ let portIdentifier = port.perform(portIdentifierSelector)?.takeUnretainedValue(),
122
+ "\(portIdentifier)" == "com.apple.framebuffer.display",
123
+ port.responds(to: descriptorSelector),
124
+ let descriptor = port.perform(descriptorSelector)?.takeUnretainedValue()
125
+ as? NSObject,
126
+ descriptor.responds(to: surfaceSelector)
127
+ else {
128
+ continue
129
+ }
130
+ candidates.append(descriptor)
131
+ }
132
+ if candidates.isEmpty {
133
+ throw RecorderError.make(5, "No framebuffer display descriptor found")
134
+ }
135
+ return candidates
136
+ }
137
+
138
+ private func registerCallbacks(descriptor: NSObject) throws {
139
+ let selector = NSSelectorFromString(
140
+ "registerScreenCallbacksWithUUID:callbackQueue:frameCallback:surfacesChangedCallback:propertiesChangedCallback:"
141
+ )
142
+ guard descriptor.responds(to: selector) else {
143
+ throw RecorderError.make(6, "Framebuffer descriptor does not support screen callbacks")
144
+ }
145
+ guard let msgSendPointer = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "objc_msgSend")
146
+ else {
147
+ throw RecorderError.make(7, "objc_msgSend not found")
148
+ }
149
+ typealias MsgSend =
150
+ @convention(c) (
151
+ AnyObject, Selector, AnyObject, AnyObject, AnyObject, AnyObject, AnyObject
152
+ ) -> Void
153
+ let msgSend = unsafeBitCast(msgSendPointer, to: MsgSend.self)
154
+
155
+ let uuid = NSUUID()
156
+ callbackUUIDs[ObjectIdentifier(descriptor)] = uuid
157
+
158
+ let frameCallback: @convention(block) () -> Void = { [weak self] in
159
+ self?.onFrame()
160
+ }
161
+ let surfacesCallback: @convention(block) (AnyObject?, AnyObject?) -> Void = {
162
+ [weak self] _, _ in
163
+ self?.onSurfaceChange()
164
+ }
165
+ let propertiesCallback: @convention(block) () -> Void = {}
166
+ retainedBlocks.append(frameCallback as AnyObject)
167
+ retainedBlocks.append(surfacesCallback as AnyObject)
168
+ retainedBlocks.append(propertiesCallback as AnyObject)
169
+
170
+ msgSend(
171
+ descriptor,
172
+ selector,
173
+ uuid,
174
+ callbackQueue as AnyObject,
175
+ frameCallback as AnyObject,
176
+ surfacesCallback as AnyObject,
177
+ propertiesCallback as AnyObject
178
+ )
179
+ }
180
+
181
+ private func unregisterCallbacks() {
182
+ let selector = NSSelectorFromString("unregisterScreenCallbacksWithUUID:")
183
+ for descriptor in descriptors {
184
+ if let uuid = callbackUUIDs[ObjectIdentifier(descriptor)],
185
+ descriptor.responds(to: selector)
186
+ {
187
+ descriptor.perform(selector, with: uuid)
188
+ }
189
+ }
190
+ callbackUUIDs.removeAll()
191
+ }
192
+ }
@@ -0,0 +1,96 @@
1
+ import Foundation
2
+ import ObjectiveC
3
+
4
+ enum XcodeDeveloperDirectory {
5
+ static let current: String = {
6
+ if let developerDir = ProcessInfo.processInfo.environment["DEVELOPER_DIR"],
7
+ !developerDir.isEmpty
8
+ {
9
+ return developerDir
10
+ }
11
+
12
+ let fallback = "/Applications/Xcode.app/Contents/Developer"
13
+ let pipe = Pipe()
14
+ let process = Process()
15
+ process.executableURL = URL(fileURLWithPath: "/usr/bin/xcode-select")
16
+ process.arguments = ["-p"]
17
+ process.standardOutput = pipe
18
+ do {
19
+ try process.run()
20
+ } catch {
21
+ return fallback
22
+ }
23
+ process.waitUntilExit()
24
+ return String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)?
25
+ .trimmingCharacters(in: .whitespacesAndNewlines) ?? fallback
26
+ }()
27
+ }
28
+
29
+ enum PrivateSimulatorFrameworks {
30
+ static func load() throws {
31
+ try loadResult.get()
32
+ }
33
+
34
+ private static let loadResult: Result<Void, Error> = {
35
+ let developerDir = XcodeDeveloperDirectory.current
36
+ let candidates = [
37
+ "/Library/Developer/PrivateFrameworks/CoreSimulator.framework/CoreSimulator",
38
+ "\(developerDir)/Library/PrivateFrameworks/CoreSimulator.framework/CoreSimulator",
39
+ "\(developerDir)/../SharedFrameworks/SimulatorKit.framework/SimulatorKit",
40
+ "\(developerDir)/Library/PrivateFrameworks/SimulatorKit.framework/SimulatorKit",
41
+ ]
42
+ var failures: [String] = []
43
+ for path in candidates {
44
+ if dlopen(path, RTLD_NOW) == nil {
45
+ let message = dlerror().map { String(cString: $0) } ?? "unknown error"
46
+ failures.append("\(path): \(message)")
47
+ }
48
+ }
49
+ guard NSClassFromString("SimServiceContext") != nil else {
50
+ let tried = candidates.joined(separator: ", ")
51
+ let errors = failures.isEmpty ? "no dlopen errors" : failures.joined(separator: "; ")
52
+ return .failure(
53
+ RecorderError.make(
54
+ 8,
55
+ "Failed to load CoreSimulator/SimulatorKit (tried: \(tried); errors: \(errors))"
56
+ )
57
+ )
58
+ }
59
+ return .success(())
60
+ }()
61
+ }
62
+
63
+ enum SimulatorDeviceLookup {
64
+ private static let context: NSObject? = {
65
+ guard let contextClass = NSClassFromString("SimServiceContext") as? NSObject.Type else {
66
+ return nil
67
+ }
68
+ let sharedSelector = NSSelectorFromString("sharedServiceContextForDeveloperDir:error:")
69
+ return contextClass.perform(
70
+ sharedSelector, with: XcodeDeveloperDirectory.current, with: nil)?
71
+ .takeUnretainedValue() as? NSObject
72
+ }()
73
+
74
+ static func find(udid: String) -> NSObject? {
75
+ guard let context else {
76
+ return nil
77
+ }
78
+ let deviceSetSelector = NSSelectorFromString("defaultDeviceSetWithError:")
79
+ guard
80
+ let deviceSet = context.perform(deviceSetSelector, with: nil)?
81
+ .takeUnretainedValue() as? NSObject
82
+ else {
83
+ return nil
84
+ }
85
+ guard let devices = deviceSet.value(forKey: "devices") as? [NSObject] else {
86
+ return nil
87
+ }
88
+ return devices.first {
89
+ ($0.value(forKey: "UDID") as? NSUUID)?.uuidString == udid
90
+ }
91
+ }
92
+
93
+ static func stateString(udid: String) -> String? {
94
+ find(udid: udid)?.value(forKey: "stateString") as? String
95
+ }
96
+ }
@@ -0,0 +1,70 @@
1
+ import Foundation
2
+
3
+ public enum RecorderCodec: String, Codable, Sendable {
4
+ case h264
5
+ case hevc
6
+ }
7
+
8
+ public struct SimulatorRecordingConfiguration: Sendable {
9
+ public var deviceUDID: String
10
+ public var outputDirectory: URL
11
+ public var fps: Int
12
+ public var bitrate: Int
13
+ public var codec: RecorderCodec
14
+ public var segmentDuration: TimeInterval
15
+ public var maxPendingFrames: Int
16
+ public var overwrite: Bool
17
+
18
+ public init(
19
+ deviceUDID: String,
20
+ outputDirectory: URL,
21
+ fps: Int = 30,
22
+ bitrate: Int = 30_000_000,
23
+ codec: RecorderCodec = .h264,
24
+ segmentDuration: TimeInterval = 120,
25
+ maxPendingFrames: Int = 8,
26
+ overwrite: Bool = true
27
+ ) {
28
+ self.deviceUDID = deviceUDID
29
+ self.outputDirectory = outputDirectory
30
+ self.fps = fps
31
+ self.bitrate = bitrate
32
+ self.codec = codec
33
+ self.segmentDuration = segmentDuration
34
+ self.maxPendingFrames = maxPendingFrames
35
+ self.overwrite = overwrite
36
+ }
37
+ }
38
+
39
+ public enum SegmentKind: String, Codable, Sendable {
40
+ case initialization
41
+ case media
42
+ }
43
+
44
+ public struct SegmentOutput: Sendable {
45
+ public let kind: SegmentKind
46
+ public let relativePath: String
47
+ public let url: URL
48
+ public let byteCount: Int
49
+ public let durationSeconds: Double?
50
+ }
51
+
52
+ public struct MediaSegmentRecord: Codable, Sendable {
53
+ public let file: String
54
+ public let durationSeconds: Double
55
+ }
56
+
57
+ public struct FirstFrameWallClock: Codable, Sendable {
58
+ public let unixMs: Int64
59
+ public let iso8601: String
60
+ }
61
+
62
+ public struct RecordingManifest: Codable, Sendable {
63
+ public let firstFrameWallClock: FirstFrameWallClock
64
+ public let hlsVersion: Int?
65
+ public let hlsTargetDurationSeconds: Int?
66
+ public let hlsMediaSequence: Int?
67
+ public let recording: String?
68
+ public let initSegment: String?
69
+ public let segments: [MediaSegmentRecord]
70
+ }
@@ -0,0 +1,136 @@
1
+ import AVFoundation
2
+ import Foundation
3
+
4
+ final class RecordingOutputWriter: NSObject, AVAssetWriterDelegate {
5
+ private let rootDirectory: URL
6
+ private let segmentsDirectory: URL
7
+ let singleRecordingURL: URL
8
+ private let stateQueue = DispatchQueue(label: "record-sim.segment-writer")
9
+ private let notificationQueue = DispatchQueue(label: "record-sim.segment-notifications")
10
+ private let onSegment: ((SegmentOutput) -> Void)?
11
+ private var initSegmentPath: String?
12
+ private var segments: [MediaSegmentRecord] = []
13
+ private var nextMediaSegmentIndex = 0
14
+ private var firstError: Error?
15
+
16
+ init(rootDirectory: URL, onSegment: ((SegmentOutput) -> Void)?) {
17
+ self.rootDirectory = rootDirectory
18
+ self.segmentsDirectory = rootDirectory.appendingPathComponent("segments", isDirectory: true)
19
+ self.singleRecordingURL = rootDirectory.appendingPathComponent("recording.mp4")
20
+ self.onSegment = onSegment
21
+ super.init()
22
+ }
23
+
24
+ func prepare(overwrite: Bool, segmented: Bool) throws {
25
+ if overwrite {
26
+ try? FileManager.default.removeItem(at: rootDirectory)
27
+ }
28
+ try FileManager.default.createDirectory(at: rootDirectory, withIntermediateDirectories: true)
29
+ if segmented {
30
+ try FileManager.default.createDirectory(at: segmentsDirectory, withIntermediateDirectories: true)
31
+ }
32
+ }
33
+
34
+ var error: Error? {
35
+ stateQueue.sync { firstError }
36
+ }
37
+
38
+ func assetWriter(
39
+ _ writer: AVAssetWriter,
40
+ didOutputSegmentData segmentData: Data,
41
+ segmentType: AVAssetSegmentType,
42
+ segmentReport: AVAssetSegmentReport?
43
+ ) {
44
+ var notification: SegmentOutput?
45
+
46
+ stateQueue.sync {
47
+ if firstError != nil {
48
+ return
49
+ }
50
+ do {
51
+ switch segmentType {
52
+ case .initialization:
53
+ let relativePath = "init.mp4"
54
+ let url = rootDirectory.appendingPathComponent(relativePath)
55
+ try segmentData.write(to: url, options: .atomic)
56
+ initSegmentPath = relativePath
57
+ notification = SegmentOutput(
58
+ kind: .initialization,
59
+ relativePath: relativePath,
60
+ url: url,
61
+ byteCount: segmentData.count,
62
+ durationSeconds: nil
63
+ )
64
+ case .separable:
65
+ let index = nextMediaSegmentIndex
66
+ let relativePath = String(format: "segments/segment-%06d.m4s", index)
67
+ let url = rootDirectory.appendingPathComponent(relativePath)
68
+ try segmentData.write(to: url, options: .atomic)
69
+
70
+ let videoReport = segmentReport?.trackReports.first(where: { $0.mediaType == .video })
71
+ let duration = videoReport.flatMap { timeSeconds($0.duration) } ?? 0
72
+ let record = MediaSegmentRecord(
73
+ file: relativePath,
74
+ durationSeconds: duration
75
+ )
76
+ nextMediaSegmentIndex += 1
77
+ segments.append(record)
78
+ notification = SegmentOutput(
79
+ kind: .media,
80
+ relativePath: relativePath,
81
+ url: url,
82
+ byteCount: segmentData.count,
83
+ durationSeconds: duration
84
+ )
85
+ default:
86
+ break
87
+ }
88
+ } catch {
89
+ firstError = error
90
+ }
91
+ }
92
+
93
+ if let notification, let onSegment {
94
+ notificationQueue.async {
95
+ onSegment(notification)
96
+ }
97
+ }
98
+ }
99
+
100
+ @discardableResult
101
+ func writeManifest(
102
+ configuration: SimulatorRecordingConfiguration,
103
+ firstFrameWallClock: Date
104
+ ) throws -> RecordingManifest {
105
+ try stateQueue.sync {
106
+ if let firstError {
107
+ throw firstError
108
+ }
109
+ let segmented = configuration.segmentDuration > 0
110
+ if segmented, initSegmentPath == nil {
111
+ throw RecorderError.make(50, "Missing initialization segment")
112
+ }
113
+ if segmented, segments.isEmpty {
114
+ throw RecorderError.make(51, "No media segments were written")
115
+ }
116
+ let targetDuration = segmented
117
+ ? max(1, Int(ceil(segments.map(\.durationSeconds).max() ?? configuration.segmentDuration)))
118
+ : nil
119
+ let manifest = RecordingManifest(
120
+ firstFrameWallClock: FirstFrameWallClock(
121
+ unixMs: unixMs(firstFrameWallClock),
122
+ iso8601: iso8601(firstFrameWallClock)
123
+ ),
124
+ hlsVersion: segmented ? 7 : nil,
125
+ hlsTargetDurationSeconds: targetDuration,
126
+ hlsMediaSequence: segmented ? 0 : nil,
127
+ recording: segmented ? nil : "recording.mp4",
128
+ initSegment: segmented ? initSegmentPath : nil,
129
+ segments: segments
130
+ )
131
+ let data = try JSONEncoder.prettySorted.encode(manifest)
132
+ try data.write(to: rootDirectory.appendingPathComponent("session.json"), options: .atomic)
133
+ return manifest
134
+ }
135
+ }
136
+ }