agent-device 0.20.4 → 0.20.6
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 +2 -0
- package/android/ime-helper/dist/{agent-device-android-ime-helper-0.20.4.apk → agent-device-android-ime-helper-0.20.6.apk} +0 -0
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.6.apk.sha256 +1 -0
- package/android/ime-helper/dist/{agent-device-android-ime-helper-0.20.4.manifest.json → agent-device-android-ime-helper-0.20.6.manifest.json} +4 -4
- package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.20.4.apk → agent-device-android-snapshot-helper-0.20.6.apk} +0 -0
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.6.apk.sha256 +1 -0
- package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.20.4.manifest.json → agent-device-android-snapshot-helper-0.20.6.manifest.json} +6 -6
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +28 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +1 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +41 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +298 -92
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +126 -187
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedTextEntry.h +32 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedTextEntry.m +212 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +118 -8
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +134 -27
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Keyboard.swift +23 -5
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +10 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+LifecycleCacheTests.swift +0 -67
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +11 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift +2 -22
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +48 -11
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotTraversalIdentityTests.swift +0 -20
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedTextEntry.swift +177 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +105 -448
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntryPolicyTests.swift +4 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextTyping.swift +587 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +18 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.h +72 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.m +89 -0
- package/dist/src/agent-device-client.js +2 -2
- package/dist/src/android.js +1 -1
- package/dist/src/app-log-request-scope.js +1 -1
- package/dist/src/app-resolution.js +1 -1
- package/dist/src/apps.js +5 -5
- package/dist/src/cli-config.js +1 -1
- package/dist/src/cli-help.js +34 -24
- package/dist/src/cli-runner.js +1 -1
- package/dist/src/cli.js +15 -15
- package/dist/src/command-metadata.js +1 -1
- package/dist/src/command-schema.js +3 -3
- package/dist/src/context.js +1 -1
- package/dist/src/debug-symbols.js +1 -1
- package/dist/src/find.js +1 -1
- package/dist/src/finders.js +1 -1
- package/dist/src/generic-settle.js +1 -0
- package/dist/src/generic.js +1 -1
- package/dist/src/ime-lifecycle.js +3 -3
- package/dist/src/index.d.ts +27 -7
- package/dist/src/input-actions.js +4 -4
- package/dist/src/interaction-runtime.js +1 -0
- package/dist/src/interaction-snapshot.js +1 -1
- package/dist/src/interaction-touch-reference-frame.js +1 -0
- package/dist/src/interaction.js +1 -1
- package/dist/src/interactor.js +2 -2
- package/dist/src/interactor2.js +1 -1
- package/dist/src/internal/bin.js +2 -2
- package/dist/src/internal/daemon.js +15 -15
- package/dist/src/lease-context.js +1 -1
- package/dist/src/lease.js +1 -1
- package/dist/src/limrun.d.ts +23 -2
- package/dist/src/linux.js +1 -1
- package/dist/src/process-exit.js +1 -0
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-trace.js +1 -1
- package/dist/src/register-builtins.js +1 -1
- package/dist/src/registry.js +25 -24
- package/dist/src/remote-config.js +1 -1
- package/dist/src/remote-config2.js +1 -1
- package/dist/src/reporting.js +2 -2
- package/dist/src/runner-client.js +1 -1
- package/dist/src/runner-disposal.js +1 -1
- package/dist/src/runner-lease.js +1 -1
- package/dist/src/runtime.js +3 -3
- package/dist/src/screenshot-geometry.js +1 -1
- package/dist/src/screenshot-result.js +4 -4
- package/dist/src/sdk-android-adb.d.ts +20 -2
- package/dist/src/sdk-android-adb.js +1 -1
- package/dist/src/sdk-batch-runner.js +3 -3
- package/dist/src/sdk-contracts.d.ts +17 -2
- package/dist/src/sdk-finders.d.ts +4 -20
- package/dist/src/sdk-finders.js +1 -1
- package/dist/src/{interaction.d.ts → sdk-selectors.d.ts} +98 -3
- package/dist/src/sdk-selectors.js +1 -1
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/selector-vocabulary.js +1 -1
- package/dist/src/selectors.d.ts +1 -46
- package/dist/src/selectors.js +1 -1
- package/dist/src/server.js +3 -3
- package/dist/src/session-target-evidence.js +1 -1
- package/dist/src/session.js +9 -8
- package/dist/src/snapshot2.js +1 -1
- package/dist/src/text-surface.js +1 -0
- package/dist/src/viewport-dimension.js +13 -12
- package/dist/src/web.js +1 -1
- package/package.json +42 -34
- package/server.json +3 -3
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.4.apk.sha256 +0 -1
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.4.apk.sha256 +0 -1
- package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/recording-resize.swift +0 -225
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-device",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.6",
|
|
4
4
|
"description": "Agent-native CLI for AI app automation across iOS, Android, tvOS, Android TV, macOS, Linux, and web.",
|
|
5
|
-
"mcpName": "io.github.
|
|
5
|
+
"mcpName": "io.github.callstack/agent-device",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Callstack",
|
|
8
8
|
"homepage": "https://agent-device.dev/",
|
|
@@ -19,52 +19,52 @@
|
|
|
19
19
|
"types": "dist/src/index.d.ts",
|
|
20
20
|
"exports": {
|
|
21
21
|
".": {
|
|
22
|
-
"
|
|
23
|
-
"
|
|
22
|
+
"types": "./dist/src/index.d.ts",
|
|
23
|
+
"import": "./dist/src/index.js"
|
|
24
24
|
},
|
|
25
25
|
"./io": {
|
|
26
|
-
"
|
|
27
|
-
"
|
|
26
|
+
"types": "./dist/src/io.d.ts",
|
|
27
|
+
"import": "./dist/src/io.js"
|
|
28
28
|
},
|
|
29
29
|
"./artifacts": {
|
|
30
|
-
"
|
|
31
|
-
"
|
|
30
|
+
"types": "./dist/src/artifacts.d.ts",
|
|
31
|
+
"import": "./dist/src/artifacts.js"
|
|
32
32
|
},
|
|
33
33
|
"./metro": {
|
|
34
|
-
"
|
|
35
|
-
"
|
|
34
|
+
"types": "./dist/src/metro.d.ts",
|
|
35
|
+
"import": "./dist/src/metro.js"
|
|
36
36
|
},
|
|
37
37
|
"./batch": {
|
|
38
|
-
"
|
|
39
|
-
"
|
|
38
|
+
"types": "./dist/src/batch.d.ts",
|
|
39
|
+
"import": "./dist/src/batch.js"
|
|
40
40
|
},
|
|
41
41
|
"./remote-config": {
|
|
42
|
-
"
|
|
43
|
-
"
|
|
42
|
+
"types": "./dist/src/remote-config.d.ts",
|
|
43
|
+
"import": "./dist/src/remote-config.js"
|
|
44
44
|
},
|
|
45
45
|
"./install-source": {
|
|
46
|
-
"
|
|
47
|
-
"
|
|
46
|
+
"types": "./dist/src/install-source.d.ts",
|
|
47
|
+
"import": "./dist/src/install-source.js"
|
|
48
48
|
},
|
|
49
49
|
"./android-adb": {
|
|
50
|
-
"
|
|
51
|
-
"
|
|
50
|
+
"types": "./dist/src/android-adb.d.ts",
|
|
51
|
+
"import": "./dist/src/android-adb.js"
|
|
52
52
|
},
|
|
53
53
|
"./limrun": {
|
|
54
|
-
"
|
|
55
|
-
"
|
|
54
|
+
"types": "./dist/src/limrun.d.ts",
|
|
55
|
+
"import": "./dist/src/limrun.js"
|
|
56
56
|
},
|
|
57
57
|
"./contracts": {
|
|
58
|
-
"
|
|
59
|
-
"
|
|
58
|
+
"types": "./dist/src/contracts.d.ts",
|
|
59
|
+
"import": "./dist/src/contracts.js"
|
|
60
60
|
},
|
|
61
61
|
"./selectors": {
|
|
62
|
-
"
|
|
63
|
-
"
|
|
62
|
+
"types": "./dist/src/selectors.d.ts",
|
|
63
|
+
"import": "./dist/src/selectors.js"
|
|
64
64
|
},
|
|
65
65
|
"./finders": {
|
|
66
|
-
"
|
|
67
|
-
"
|
|
66
|
+
"types": "./dist/src/finders.d.ts",
|
|
67
|
+
"import": "./dist/src/finders.js"
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
70
|
"engines": {
|
|
@@ -103,14 +103,15 @@
|
|
|
103
103
|
"package:android-ime-helper:npm": "rm -rf android/ime-helper/dist && AGENT_DEVICE_ANDROID_HELPER=ime sh ./scripts/package-android-helper.sh $(node -p \"require('./package.json').version\") android/ime-helper/dist",
|
|
104
104
|
"build:macos-helper": "swift build -c release --package-path apple/macos-helper",
|
|
105
105
|
"build:macos-helper:clean": "swift package --package-path apple/macos-helper clean && pnpm build:macos-helper",
|
|
106
|
-
"package
|
|
106
|
+
"build:package": "pnpm build && pnpm build:xcuitest:ios && pnpm build:xcuitest:macos && pnpm build:xcuitest:tvos && pnpm build:xcuitest:visionos && pnpm build:macos-helper:clean && pnpm package:apple-runner:npm && pnpm build:android",
|
|
107
|
+
"package:npm": "pnpm build:package && pnpm check:package",
|
|
108
|
+
"release:prepare": "rm -rf .tmp/release && pnpm check:mcp-metadata && pnpm build:package && pnpm check:package -- --pack-destination .tmp/release",
|
|
109
|
+
"release:publish": "pnpm release:prepare && npm publish --ignore-scripts .tmp/release/*.tgz",
|
|
107
110
|
"ad": "node bin/agent-device.mjs",
|
|
108
111
|
"bench:help-conformance": "node scripts/help-conformance-bench.mjs",
|
|
109
112
|
"maestro:conformance": "node --experimental-strip-types --test packages/maestro/test/conformance/verify.test.ts packages/maestro/test/conformance/differential/run.test.ts packages/maestro/test/conformance/differential/invariants.test.ts",
|
|
110
113
|
"maestro:conformance:regenerate": "node --experimental-strip-types scripts/maestro-conformance/regenerate.mjs",
|
|
111
114
|
"maestro:conformance:differential": "node --experimental-strip-types packages/maestro/test/conformance/differential/run.ts",
|
|
112
|
-
"prototype:program-boundary": "node --experimental-strip-types scripts/prototypes/daemon-boundaries/program-boundary.ts",
|
|
113
|
-
"prototype:session-boundary": "node --experimental-strip-types scripts/prototypes/daemon-boundaries/session-boundary.ts",
|
|
114
115
|
"size": "node scripts/size-report.mjs",
|
|
115
116
|
"perf": "node --experimental-strip-types scripts/perf/run.ts",
|
|
116
117
|
"mutation:run": "node --experimental-strip-types scripts/mutation/run.ts",
|
|
@@ -129,23 +130,26 @@
|
|
|
129
130
|
"check:affected:test": "node --experimental-strip-types --test scripts/check-affected/model.test.ts scripts/check-affected/run.test.ts",
|
|
130
131
|
"check:coverage-changed": "node --experimental-strip-types scripts/coverage-changed/run.ts",
|
|
131
132
|
"check:coverage-changed:test": "node --experimental-strip-types --test scripts/coverage-changed/model.test.ts scripts/coverage-changed/run.test.ts",
|
|
132
|
-
"check:layering": "node --experimental-strip-types --test scripts/layering/model.test.ts scripts/layering/zone-policy.test.ts scripts/layering/daemon-modularity.test.ts scripts/layering/package-boundaries.test.ts && node --experimental-strip-types scripts/layering/check.ts",
|
|
133
|
+
"check:layering": "node --experimental-strip-types --test scripts/layering/model.test.ts scripts/layering/zone-policy.test.ts scripts/layering/daemon-modularity.test.ts scripts/layering/package-boundaries.test.ts scripts/layering/facade-exports.test.ts scripts/layering/bin-alias-fast-path.test.ts && node --experimental-strip-types scripts/layering/check.ts",
|
|
133
134
|
"depgraph": "node --experimental-strip-types scripts/depgraph/build.ts",
|
|
134
135
|
"depgraph:test": "node --experimental-strip-types --test scripts/depgraph/model.test.ts scripts/depgraph/affected.test.ts",
|
|
135
136
|
"check:production-exports": "fallow dead-code --config fallow-production-exports.json --production --unused-exports --fail-on-issues",
|
|
136
137
|
"check:bundle-owner-files": "node --experimental-strip-types scripts/check-bundle-owner-files.ts",
|
|
138
|
+
"check:package": "node --experimental-strip-types scripts/check-package.ts",
|
|
137
139
|
"check:command-docs": "vitest run --project unit-core src/__tests__/command-doc-coverage.test.ts",
|
|
138
140
|
"check:replay-compat": "node --experimental-strip-types scripts/check-replay-compat-provenance.ts",
|
|
141
|
+
"check:tmpdir-leaks": "node --experimental-strip-types scripts/check-tmpdir-leaks.ts",
|
|
142
|
+
"check:tmpdir-leaks:test": "node --experimental-strip-types --test scripts/check-tmpdir-leaks-model.test.ts scripts/vitest-tmpdir-global-setup.test.ts",
|
|
139
143
|
"check:freerange": "fr",
|
|
140
144
|
"check:quick": "pnpm lint && pnpm typecheck",
|
|
141
145
|
"sync:mcp-metadata": "node scripts/sync-mcp-metadata.mjs",
|
|
142
146
|
"check:mcp-metadata": "node scripts/sync-mcp-metadata.mjs --check",
|
|
143
147
|
"version": "pnpm sync:mcp-metadata && git add server.json",
|
|
144
|
-
"check:tooling": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm check:layering && pnpm depgraph:test && pnpm check:production-exports && pnpm check:mcp-metadata && pnpm build && pnpm check:bundle-owner-files",
|
|
145
|
-
"check:unit": "pnpm check:contention-retry && pnpm test:unit && pnpm test:smoke",
|
|
148
|
+
"check:tooling": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm check:layering && pnpm depgraph:test && pnpm check:production-exports && pnpm check:tmpdir-leaks:test && pnpm check:mcp-metadata && pnpm build && pnpm check:bundle-owner-files && pnpm check:package",
|
|
149
|
+
"check:unit": "pnpm check:contention-retry && pnpm test:unit && pnpm check:tmpdir-leaks && pnpm test:smoke",
|
|
146
150
|
"check": "pnpm check:tooling && pnpm check:fallow && pnpm check:unit",
|
|
147
151
|
"prepack": "pnpm check:mcp-metadata && pnpm package:npm",
|
|
148
|
-
"typecheck": "tsc -b packages/xml packages/kernel packages/contracts packages/ad-script packages/maestro packages/replay-test packages/provider-webdriver packages/provider-limrun && tsc -p tsconfig.json && tsc -p examples/sdk/tsconfig.json",
|
|
152
|
+
"typecheck": "tsc -b packages/xml packages/kernel packages/contracts packages/ad-script packages/selectors packages/ad-replay packages/maestro packages/replay-test packages/provider-webdriver packages/provider-limrun && tsc -p tsconfig.json && tsc -p examples/sdk/tsconfig.json",
|
|
149
153
|
"test-app:install": "pnpm install --dir examples/test-app",
|
|
150
154
|
"test-app:start": "pnpm --dir examples/test-app start",
|
|
151
155
|
"test-app:ios": "pnpm --dir examples/test-app ios",
|
|
@@ -241,18 +245,20 @@
|
|
|
241
245
|
],
|
|
242
246
|
"dependencies": {
|
|
243
247
|
"@limrun/api": "^0.24.5",
|
|
244
|
-
"pngjs": "^7.0.0",
|
|
245
248
|
"yaml": "^2.9.0"
|
|
246
249
|
},
|
|
247
250
|
"devDependencies": {
|
|
251
|
+
"@agent-device/ad-replay": "workspace:*",
|
|
248
252
|
"@agent-device/ad-script": "workspace:*",
|
|
249
253
|
"@agent-device/contracts": "workspace:*",
|
|
250
254
|
"@agent-device/kernel": "workspace:*",
|
|
255
|
+
"@agent-device/selectors": "workspace:*",
|
|
251
256
|
"@agent-device/maestro": "workspace:*",
|
|
252
257
|
"@agent-device/provider-limrun": "workspace:*",
|
|
253
258
|
"@agent-device/provider-webdriver": "workspace:*",
|
|
254
259
|
"@agent-device/replay-test": "workspace:*",
|
|
255
260
|
"@agent-device/xml": "workspace:*",
|
|
261
|
+
"@arethetypeswrong/cli": "^0.18.5",
|
|
256
262
|
"@chenglou/freerange": "^0.0.1",
|
|
257
263
|
"@stryker-mutator/core": "9.6.1",
|
|
258
264
|
"@stryker-mutator/vitest-runner": "9.6.1",
|
|
@@ -264,6 +270,8 @@
|
|
|
264
270
|
"oxc-parser": "^0.138.0",
|
|
265
271
|
"oxfmt": "^0.42.0",
|
|
266
272
|
"oxlint": "^1.69.0",
|
|
273
|
+
"pngjs": "^7.0.0",
|
|
274
|
+
"publint": "^0.3.22",
|
|
267
275
|
"tsdown": "^0.22.4",
|
|
268
276
|
"typescript": "^7.0.2",
|
|
269
277
|
"vite": "^8.0.16",
|
package/server.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
-
"name": "io.github.
|
|
3
|
+
"name": "io.github.callstack/agent-device",
|
|
4
4
|
"title": "agent-device",
|
|
5
5
|
"description": "Let AI agents inspect, control, and debug real iOS, Android, desktop, and TV apps",
|
|
6
6
|
"repository": {
|
|
7
7
|
"url": "https://github.com/callstack/agent-device",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.20.
|
|
10
|
+
"version": "0.20.6",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "agent-device",
|
|
15
|
-
"version": "0.20.
|
|
15
|
+
"version": "0.20.6",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
019ebbda52976df5cff05f9e5a764c8d6e280be3640e8dea0a2d7cca6c25021f agent-device-android-ime-helper-0.20.4.apk
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
e3799afe5549776e047744efe46939a9146a4742a5328bb2e22f0b9d6abd2b52 agent-device-android-snapshot-helper-0.20.4.apk
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
import AVFoundation
|
|
2
|
-
import Foundation
|
|
3
|
-
|
|
4
|
-
enum ResizeError: Error, CustomStringConvertible {
|
|
5
|
-
case invalidArgs(String)
|
|
6
|
-
case missingVideoTrack
|
|
7
|
-
case exportFailed(String)
|
|
8
|
-
|
|
9
|
-
var description: String {
|
|
10
|
-
switch self {
|
|
11
|
-
case .invalidArgs(let message):
|
|
12
|
-
return message
|
|
13
|
-
case .missingVideoTrack:
|
|
14
|
-
return "Input video does not contain a video track."
|
|
15
|
-
case .exportFailed(let message):
|
|
16
|
-
return message
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
do {
|
|
22
|
-
try run()
|
|
23
|
-
} catch {
|
|
24
|
-
fputs("recording-resize: \(error)\n", stderr)
|
|
25
|
-
exit(1)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
func run() throws {
|
|
29
|
-
let arguments = Array(CommandLine.arguments.dropFirst())
|
|
30
|
-
let parsedArgs = try parseArguments(arguments)
|
|
31
|
-
let inputURL = URL(fileURLWithPath: parsedArgs.inputPath)
|
|
32
|
-
let outputURL = URL(fileURLWithPath: parsedArgs.outputPath)
|
|
33
|
-
|
|
34
|
-
if FileManager.default.fileExists(atPath: outputURL.path) {
|
|
35
|
-
try FileManager.default.removeItem(at: outputURL)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
let asset = AVURLAsset(url: inputURL)
|
|
39
|
-
guard let sourceVideoTrack = asset.tracks(withMediaType: .video).first else {
|
|
40
|
-
throw ResizeError.missingVideoTrack
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
let sourceRenderSize = resolvedRenderSize(for: sourceVideoTrack)
|
|
44
|
-
if max(sourceRenderSize.width, sourceRenderSize.height) <= CGFloat(parsedArgs.maxSize) {
|
|
45
|
-
try FileManager.default.copyItem(at: inputURL, to: outputURL)
|
|
46
|
-
return
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
let renderSize = scaledRenderSize(sourceRenderSize, maxSize: parsedArgs.maxSize)
|
|
50
|
-
let composition = AVMutableComposition()
|
|
51
|
-
let fullRange = CMTimeRange(start: .zero, duration: asset.duration)
|
|
52
|
-
|
|
53
|
-
guard let compositionVideoTrack = composition.addMutableTrack(
|
|
54
|
-
withMediaType: .video,
|
|
55
|
-
preferredTrackID: kCMPersistentTrackID_Invalid
|
|
56
|
-
) else {
|
|
57
|
-
throw ResizeError.exportFailed("Failed to create composition video track.")
|
|
58
|
-
}
|
|
59
|
-
try compositionVideoTrack.insertTimeRange(fullRange, of: sourceVideoTrack, at: .zero)
|
|
60
|
-
|
|
61
|
-
if let sourceAudioTrack = asset.tracks(withMediaType: .audio).first,
|
|
62
|
-
let compositionAudioTrack = composition.addMutableTrack(
|
|
63
|
-
withMediaType: .audio,
|
|
64
|
-
preferredTrackID: kCMPersistentTrackID_Invalid
|
|
65
|
-
) {
|
|
66
|
-
try? compositionAudioTrack.insertTimeRange(fullRange, of: sourceAudioTrack, at: .zero)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
let scale = renderSize.width / sourceRenderSize.width
|
|
70
|
-
let videoComposition = AVMutableVideoComposition()
|
|
71
|
-
videoComposition.renderSize = renderSize
|
|
72
|
-
videoComposition.frameDuration = resolvedFrameDuration(for: sourceVideoTrack)
|
|
73
|
-
|
|
74
|
-
let instruction = AVMutableVideoCompositionInstruction()
|
|
75
|
-
instruction.timeRange = fullRange
|
|
76
|
-
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: compositionVideoTrack)
|
|
77
|
-
// Scale the full preferred transform (including translation) to match the smaller render canvas.
|
|
78
|
-
let scaledTransform = scaledPreferredTransform(sourceVideoTrack.preferredTransform, scale: scale)
|
|
79
|
-
layerInstruction.setTransform(scaledTransform, at: .zero)
|
|
80
|
-
instruction.layerInstructions = [layerInstruction]
|
|
81
|
-
videoComposition.instructions = [instruction]
|
|
82
|
-
|
|
83
|
-
let presetName = exportPresetName(for: parsedArgs.exportQuality, compatibleWith: composition)
|
|
84
|
-
guard let exporter = AVAssetExportSession(asset: composition, presetName: presetName) else {
|
|
85
|
-
throw ResizeError.exportFailed("Failed to create export session.")
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
exporter.outputURL = outputURL
|
|
89
|
-
exporter.outputFileType = .mp4
|
|
90
|
-
exporter.videoComposition = videoComposition
|
|
91
|
-
exporter.shouldOptimizeForNetworkUse = true
|
|
92
|
-
|
|
93
|
-
let semaphore = DispatchSemaphore(value: 0)
|
|
94
|
-
exporter.exportAsynchronously {
|
|
95
|
-
semaphore.signal()
|
|
96
|
-
}
|
|
97
|
-
if semaphore.wait(timeout: .now() + 120) == .timedOut {
|
|
98
|
-
exporter.cancelExport()
|
|
99
|
-
throw ResizeError.exportFailed("Resize export timed out.")
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if exporter.status != .completed {
|
|
103
|
-
throw ResizeError.exportFailed(exporter.error?.localizedDescription ?? "Resize export failed.")
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
enum ExportQuality: String {
|
|
108
|
-
case medium
|
|
109
|
-
case high
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
func parseArguments(
|
|
113
|
-
_ arguments: [String]
|
|
114
|
-
) throws -> (inputPath: String, outputPath: String, maxSize: Int, exportQuality: ExportQuality) {
|
|
115
|
-
var inputPath: String?
|
|
116
|
-
var outputPath: String?
|
|
117
|
-
var maxSize: Int?
|
|
118
|
-
// Export quality defaults to medium so re-encoded recordings stay fast by default.
|
|
119
|
-
// Pass --quality high to opt into a slower highest-quality export.
|
|
120
|
-
var exportQuality: ExportQuality = .medium
|
|
121
|
-
var index = 0
|
|
122
|
-
|
|
123
|
-
while index < arguments.count {
|
|
124
|
-
let argument = arguments[index]
|
|
125
|
-
let nextIndex = index + 1
|
|
126
|
-
switch argument {
|
|
127
|
-
case "--input":
|
|
128
|
-
guard nextIndex < arguments.count else { throw ResizeError.invalidArgs("--input requires a value") }
|
|
129
|
-
inputPath = arguments[nextIndex]
|
|
130
|
-
index += 2
|
|
131
|
-
case "--output":
|
|
132
|
-
guard nextIndex < arguments.count else { throw ResizeError.invalidArgs("--output requires a value") }
|
|
133
|
-
outputPath = arguments[nextIndex]
|
|
134
|
-
index += 2
|
|
135
|
-
case "--max-size":
|
|
136
|
-
guard nextIndex < arguments.count else { throw ResizeError.invalidArgs("--max-size requires a value") }
|
|
137
|
-
guard let parsed = Int(arguments[nextIndex]), parsed >= 1 else {
|
|
138
|
-
throw ResizeError.invalidArgs("--max-size must be a positive integer")
|
|
139
|
-
}
|
|
140
|
-
maxSize = parsed
|
|
141
|
-
index += 2
|
|
142
|
-
case "--quality":
|
|
143
|
-
guard nextIndex < arguments.count else {
|
|
144
|
-
throw ResizeError.invalidArgs("--quality requires a value")
|
|
145
|
-
}
|
|
146
|
-
guard let parsed = ExportQuality(rawValue: arguments[nextIndex]) else {
|
|
147
|
-
throw ResizeError.invalidArgs("--quality must be one of: medium, high")
|
|
148
|
-
}
|
|
149
|
-
exportQuality = parsed
|
|
150
|
-
index += 2
|
|
151
|
-
default:
|
|
152
|
-
throw ResizeError.invalidArgs("Unknown argument: \(argument)")
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
guard let inputPath, let outputPath, let maxSize else {
|
|
157
|
-
throw ResizeError.invalidArgs(
|
|
158
|
-
"Usage: recording-resize.swift --input <video> --output <video> --max-size <px> [--quality <medium|high>]"
|
|
159
|
-
)
|
|
160
|
-
}
|
|
161
|
-
return (inputPath, outputPath, maxSize, exportQuality)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
func exportPresetName(
|
|
165
|
-
for exportQuality: ExportQuality,
|
|
166
|
-
compatibleWith asset: AVAsset
|
|
167
|
-
) -> String {
|
|
168
|
-
switch exportQuality {
|
|
169
|
-
case .high:
|
|
170
|
-
return AVAssetExportPresetHighestQuality
|
|
171
|
-
case .medium:
|
|
172
|
-
// Mirror the touch-overlay export: prefer the faster medium preset, falling back to
|
|
173
|
-
// highest quality only when medium is not available for this composition.
|
|
174
|
-
let compatible = AVAssetExportSession.exportPresets(compatibleWith: asset)
|
|
175
|
-
return compatible.contains(AVAssetExportPresetMediumQuality)
|
|
176
|
-
? AVAssetExportPresetMediumQuality
|
|
177
|
-
: AVAssetExportPresetHighestQuality
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
func resolvedRenderSize(for track: AVAssetTrack) -> CGSize {
|
|
182
|
-
let transformed = track.naturalSize.applying(track.preferredTransform)
|
|
183
|
-
return CGSize(width: abs(transformed.width), height: abs(transformed.height))
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
func scaledRenderSize(_ renderSize: CGSize, maxSize: Int) -> CGSize {
|
|
187
|
-
let longest = max(renderSize.width, renderSize.height)
|
|
188
|
-
guard longest > CGFloat(maxSize) else { return renderSize }
|
|
189
|
-
let scale = CGFloat(maxSize) / longest
|
|
190
|
-
return CGSize(
|
|
191
|
-
width: scaledDimension(renderSize.width, scale: scale),
|
|
192
|
-
height: scaledDimension(renderSize.height, scale: scale)
|
|
193
|
-
)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
func scaledDimension(_ value: CGFloat, scale: CGFloat) -> CGFloat {
|
|
197
|
-
let evenValue = Int((Double(value * scale) / 2.0).rounded()) * 2
|
|
198
|
-
return CGFloat(max(2, evenValue))
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
func resolvedFrameDuration(for track: AVAssetTrack) -> CMTime {
|
|
202
|
-
let minFrameDuration = track.minFrameDuration
|
|
203
|
-
if minFrameDuration.isValid && !minFrameDuration.isIndefinite && minFrameDuration.seconds > 0 {
|
|
204
|
-
return minFrameDuration
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
let nominalFrameRate = track.nominalFrameRate
|
|
208
|
-
if nominalFrameRate > 0 {
|
|
209
|
-
let timescale = Int32(max(1, round(nominalFrameRate)))
|
|
210
|
-
return CMTime(value: 1, timescale: timescale)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return CMTime(value: 1, timescale: 60)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
func scaledPreferredTransform(_ transform: CGAffineTransform, scale: CGFloat) -> CGAffineTransform {
|
|
217
|
-
CGAffineTransform(
|
|
218
|
-
a: transform.a * scale,
|
|
219
|
-
b: transform.b * scale,
|
|
220
|
-
c: transform.c * scale,
|
|
221
|
-
d: transform.d * scale,
|
|
222
|
-
tx: transform.tx * scale,
|
|
223
|
-
ty: transform.ty * scale
|
|
224
|
-
)
|
|
225
|
-
}
|