@@ -30,40 +30,33 @@ struct GestureEvent: Decodable {
3030 let edge : String ?
3131}
3232
33- enum OverlayError : Error , CustomStringConvertible {
34- case invalidArgs ( String )
35- case missingVideoTrack
36- case exportFailed ( String )
33+ enum ExportQuality : String {
34+ case medium
35+ case high
36+ }
3737
38- var description : String {
39- switch self {
40- case . invalidArgs( let message) :
41- return message
42- case . missingVideoTrack:
43- return " Input video does not contain a video track. "
44- case . exportFailed( let message) :
45- return message
38+ /// Entry point: `@main` because multi-file swiftc compilation reserves top-level statements for
39+ /// `main.swift`, which cannot be shared per-script.
40+ @main
41+ enum RecordingOverlay {
42+ static func main( ) {
43+ do {
44+ try run ( )
45+ } catch {
46+ fputs ( " recording-overlay: \( error) \n " , stderr)
47+ exit ( 1 )
4648 }
4749 }
4850}
4951
50- do {
51- try run ( )
52- } catch {
53- fputs ( " recording-overlay: \( error) \n " , stderr)
54- exit ( 1 )
55- }
56-
5752func run( ) throws {
5853 let arguments = Array ( CommandLine . arguments. dropFirst ( ) )
5954 let parsedArgs = try parseArguments ( arguments)
6055 let inputURL = URL ( fileURLWithPath: parsedArgs. inputPath)
6156 let outputURL = URL ( fileURLWithPath: parsedArgs. outputPath)
6257 let eventsURL = URL ( fileURLWithPath: parsedArgs. eventsPath)
6358
64- if FileManager . default. fileExists ( atPath: outputURL. path) {
65- try FileManager . default. removeItem ( at: outputURL)
66- }
59+ try removeStaleOutput ( outputURL)
6760
6861 let payload = try Data ( contentsOf: eventsURL)
6962 let envelope = try JSONDecoder ( ) . decode ( GestureEnvelope . self, from: payload)
@@ -74,27 +67,15 @@ func run() throws {
7467 }
7568
7669 let asset = AVURLAsset ( url: inputURL)
77- guard let sourceVideoTrack = asset. tracks ( withMediaType: . video) . first else {
78- throw OverlayError . missingVideoTrack
79- }
80-
81- let composition = AVMutableComposition ( )
82- guard let compositionVideoTrack = composition. addMutableTrack (
83- withMediaType: . video,
84- preferredTrackID: kCMPersistentTrackID_Invalid
85- ) else {
86- throw OverlayError . exportFailed ( " Failed to create composition video track. " )
87- }
88-
70+ let sourceVideoTrack = try sourceVideoTrack ( of: asset)
8971 let fullRange = CMTimeRange ( start: . zero, duration: asset. duration)
90- try compositionVideoTrack. insertTimeRange ( fullRange, of: sourceVideoTrack, at: . zero)
91-
92- if let sourceAudioTrack = asset. tracks ( withMediaType: . audio) . first,
93- let compositionAudioTrack = composition. addMutableTrack (
94- withMediaType: . audio,
95- preferredTrackID: kCMPersistentTrackID_Invalid
96- ) {
97- try ? compositionAudioTrack. insertTimeRange ( fullRange, of: sourceAudioTrack, at: . zero)
72+ let composition = try makeRecordingComposition (
73+ asset: asset,
74+ videoTrack: sourceVideoTrack,
75+ timeRange: fullRange
76+ )
77+ guard let compositionVideoTrack = composition. tracks ( withMediaType: . video) . first else {
78+ throw RecordingScriptError . exportFailed ( " Failed to create composition video track. " )
9879 }
9980
10081 let renderSize = resolvedRenderSize ( for: sourceVideoTrack)
@@ -149,32 +130,17 @@ func run() throws {
149130 // while avoiding very slow highest-quality exports. Pass --quality high to opt into
150131 // the slower highest-quality export.
151132 let presetName = exportPresetName ( for: parsedArgs. exportQuality, compatibleWith: composition)
152- guard let exporter = AVAssetExportSession ( asset: composition, presetName: presetName) else {
153- throw OverlayError . exportFailed ( " Failed to create export session. " )
154- }
155-
156- exporter. outputURL = outputURL
157- exporter. outputFileType = . mp4
158- exporter. videoComposition = videoComposition
159- exporter. shouldOptimizeForNetworkUse = true
160-
161- let semaphore = DispatchSemaphore ( value: 0 )
162- exporter. exportAsynchronously {
163- semaphore. signal ( )
164- }
165- if semaphore. wait ( timeout: . now( ) + 120 ) == . timedOut {
166- exporter. cancelExport ( )
167- throw OverlayError . exportFailed ( " Touch overlay export timed out. " )
168- }
169-
170- if exporter. status != . completed {
171- throw OverlayError . exportFailed ( exporter. error? . localizedDescription ?? " Touch overlay export failed. " )
172- }
173- }
174-
175- enum ExportQuality : String {
176- case medium
177- case high
133+ let exporter = try makeRecordingExporter (
134+ composition,
135+ presetName: presetName,
136+ outputURL: outputURL,
137+ videoComposition: videoComposition
138+ )
139+ try runRecordingExport (
140+ exporter,
141+ timeoutMessage: " Touch overlay export timed out. " ,
142+ failureMessage: " Touch overlay export failed. "
143+ )
178144}
179145
180146func parseArguments(
@@ -193,33 +159,28 @@ func parseArguments(
193159 let nextIndex = index + 1
194160 switch argument {
195161 case " --input " :
196- guard nextIndex < arguments. count else { throw OverlayError . invalidArgs ( " --input requires a value " ) }
197- inputPath = arguments [ nextIndex]
162+ inputPath = try recordingOptionValue ( arguments, nextIndex, " --input " )
198163 index += 2
199164 case " --output " :
200- guard nextIndex < arguments. count else { throw OverlayError . invalidArgs ( " --output requires a value " ) }
201- outputPath = arguments [ nextIndex]
165+ outputPath = try recordingOptionValue ( arguments, nextIndex, " --output " )
202166 index += 2
203167 case " --events " :
204- guard nextIndex < arguments. count else { throw OverlayError . invalidArgs ( " --events requires a value " ) }
205- eventsPath = arguments [ nextIndex]
168+ eventsPath = try recordingOptionValue ( arguments, nextIndex, " --events " )
206169 index += 2
207170 case " --quality " :
208- guard nextIndex < arguments. count else {
209- throw OverlayError . invalidArgs ( " --quality requires a value " )
210- }
211- guard let parsed = ExportQuality ( rawValue: arguments [ nextIndex] ) else {
212- throw OverlayError . invalidArgs ( " --quality must be one of: medium, high " )
171+ let rawValue = try recordingOptionValue ( arguments, nextIndex, " --quality " )
172+ guard let parsed = ExportQuality ( rawValue: rawValue) else {
173+ throw RecordingScriptError . invalidArgs ( " --quality must be one of: medium, high " )
213174 }
214175 exportQuality = parsed
215176 index += 2
216177 default :
217- throw OverlayError . invalidArgs ( " Unknown argument: \( argument) " )
178+ throw RecordingScriptError . invalidArgs ( " Unknown argument: \( argument) " )
218179 }
219180 }
220181
221182 guard let inputPath, let outputPath, let eventsPath else {
222- throw OverlayError . invalidArgs (
183+ throw RecordingScriptError . invalidArgs (
223184 " Usage: recording-overlay.swift --input <video> --output <video> --events <json> [--quality <medium|high>] "
224185 )
225186 }
0 commit comments