-
-
Notifications
You must be signed in to change notification settings - Fork 424
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Video recording using native XCTest backend #2339
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
59205d8
feat: Video recording using native XCTest backend
mykola-mokhnach ea4677d
finalize
mykola-mokhnach 326168e
wording
mykola-mokhnach e91d4ef
Merge branch 'master' of https://github.com/mykola-mokhnach/appium-xc…
mykola-mokhnach 1b0137d
linter
mykola-mokhnach 2473d6b
fix properties
mykola-mokhnach 1f60981
typing
mykola-mokhnach File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
import _ from 'lodash'; | ||
import {fs, util} from 'appium/support'; | ||
import {encodeBase64OrUpload} from '../utils'; | ||
import path from 'node:path'; | ||
import {Devicectl} from '../devicectl'; | ||
|
||
const MOV_EXT = '.mov'; | ||
const FEATURE_NAME = 'xctest_screen_record'; | ||
const DOMAIN_IDENTIFIER = 'com.apple.testmanagerd'; | ||
const DOMAIN_TYPE = 'appDataContainer'; | ||
const USERNAME = 'mobile'; | ||
const SUBDIRECTORY = 'Attachments'; | ||
|
||
/** | ||
* @typedef {Object} XcTestScreenRecordingInfo | ||
* @property {string} uuid Unique identifier of the video being recorded | ||
* @property {number} fps FPS value | ||
* @property {number} codec Video codec, where 0 is h264 | ||
* @property {number} startedAt The timestamp when the screen recording has started in float Unix seconds | ||
*/ | ||
|
||
/** | ||
* @typedef {Object} XcTestScreenRecordingType | ||
* @property {string} payload Base64-encoded content of the recorded media | ||
* file if `remotePath` parameter is empty or null or an empty string otherwise. | ||
* The media is expected to a be a valid QuickTime movie (.mov). | ||
* @typedef {XcTestScreenRecordingInfo & XcTestScreenRecordingType} XcTestScreenRecording | ||
*/ | ||
|
||
/** | ||
* @this {XCUITestDriver} | ||
* @param {string} uuid Unique identifier of the video being recorded | ||
* @returns {Promise<string>} The full path to the screen recording movie | ||
*/ | ||
async function retrieveRecodingFromSimulator(uuid) { | ||
// @ts-ignore The property is there | ||
const device = this.opts.device; | ||
const dataRoot = /** @type {string} */ (device.getDir()); | ||
// On Simulators the path looks like | ||
// $HOME/Library/Developer/CoreSimulator/Devices/F8E1968A-8443-4A9A-AB86-27C54C36A2F6/data/Containers/Data/InternalDaemon/4E3FE8DF-AD0A-41DA-B6EC-C35E5798C219/Attachments/A044DAF7-4A58-4CD5-95C3-29B4FE80C377 | ||
const internalDaemonRoot = path.resolve(dataRoot, 'Containers', 'Data', 'InternalDaemon'); | ||
const videoPaths = await fs.glob(`*/Attachments/${uuid}`, { | ||
cwd: internalDaemonRoot, | ||
absolute: true, | ||
}); | ||
if (_.isEmpty(videoPaths)) { | ||
throw new Error( | ||
`Unable to locate XCTest screen recording identified by '${uuid}' for the Simulator ${device.udid}` | ||
); | ||
} | ||
const videoPath = videoPaths[0]; | ||
const {size} = await fs.stat(videoPath); | ||
this.log.debug(`Located the video at '${videoPath}' (${util.toReadableSizeString(size)})`); | ||
return videoPath; | ||
} | ||
|
||
/** | ||
* @this {XCUITestDriver} | ||
* @param {string} uuid Unique identifier of the video being recorded | ||
* @returns {Promise<string>} The full path to the screen recording movie | ||
*/ | ||
async function retrieveRecodingFromRealDevice(uuid) { | ||
const devicectl = new Devicectl(this.opts.udid, this.log); | ||
const fileNames = await devicectl.listFiles(DOMAIN_TYPE, DOMAIN_IDENTIFIER, { | ||
username: USERNAME, | ||
subdirectory: SUBDIRECTORY, | ||
}); | ||
if (!fileNames.includes(uuid)) { | ||
throw new Error( | ||
`Unable to locate XCTest screen recording identified by '${uuid}' for the device ${this.opts.udid}` | ||
); | ||
} | ||
const videoPath = path.join(/** @type {string} */ (this.opts.tmpDir), `${uuid}${MOV_EXT}`); | ||
await devicectl.pullFile(`${SUBDIRECTORY}/${uuid}`, videoPath, { | ||
username: USERNAME, | ||
domainIdentifier: DOMAIN_IDENTIFIER, | ||
domainType: DOMAIN_TYPE, | ||
}); | ||
const {size} = await fs.stat(videoPath); | ||
this.log.debug(`Pulled the video to '${videoPath}' (${util.toReadableSizeString(size)})`); | ||
return videoPath; | ||
} | ||
|
||
/** | ||
* @this {XCUITestDriver} | ||
* @param {string} uuid Unique identifier of the video being recorded | ||
* @returns {Promise<string>} The full path to the screen recording movie | ||
*/ | ||
async function retrieveXcTestScreenRecording(uuid) { | ||
return this.isRealDevice() | ||
? await retrieveRecodingFromRealDevice.bind(this)(uuid) | ||
: await retrieveRecodingFromSimulator.bind(this)(uuid); | ||
} | ||
|
||
export default { | ||
/** | ||
* Start a new screen recording via XCTest. | ||
* | ||
* Even though the feature is available for real devices | ||
* there is no possibility to delete stored video files yet, | ||
* which may lead to internal storage overload. | ||
* That is why it was put under a security feature flag. | ||
* | ||
* If the recording is already running this API is a noop. | ||
* | ||
* @since Xcode 15/iOS 17 | ||
* @param {number} [fps] FPS value | ||
* @param {number} [codec] Video codec, where 0 is h264, 1 is HEVC | ||
* @returns {Promise<XcTestScreenRecordingInfo>} The information | ||
* about a newly created or a running the screen recording. | ||
* @throws {Error} If screen recording has failed to start. | ||
* @this {XCUITestDriver} | ||
*/ | ||
async mobileStartXctestScreenRecording(fps, codec) { | ||
if (this.isRealDevice()) { | ||
// This feature might be used to abuse real devices as there is no | ||
// reliable way (yet) to cleanup video recordings stored there | ||
// by the testmanagerd daemon | ||
this.assertFeatureEnabled(FEATURE_NAME); | ||
} | ||
|
||
const opts = {}; | ||
if (_.isInteger(codec)) { | ||
opts.codec = codec; | ||
} | ||
if (_.isInteger(fps)) { | ||
opts.fps = fps; | ||
} | ||
const response = /** @type {XcTestScreenRecordingInfo} */ ( | ||
await this.proxyCommand('/wda/video/start', 'POST', opts) | ||
); | ||
this.log.info(`Started a new screen recording: ${JSON.stringify(response)}`); | ||
return response; | ||
}, | ||
|
||
/** | ||
* Retrieves information about the current running screen recording. | ||
* If no screen recording is running then `null` is returned. | ||
* | ||
* @returns {Promise<XcTestScreenRecordingInfo?>} | ||
*/ | ||
async mobileGetXctestScreenRecordingInfo() { | ||
return /** @type {XcTestScreenRecordingInfo?} */ ( | ||
await this.proxyCommand('/wda/video', 'GET') | ||
); | ||
}, | ||
|
||
/** | ||
* Stop screen recording previously started by mobileStartXctestScreenRecording API. | ||
* | ||
* An error is thrown if no screen recording is running. | ||
* | ||
* The resulting movie is returned as base-64 string or is uploaded to | ||
* a remote location if corresponding options have been provided. | ||
* | ||
* The resulting movie is automatically deleted FOR SIMULATORS ONLY. | ||
* In order to clean it up from a real device it is necessary to properly | ||
* shut down XCTest by calling `POST /wda/shutdown` API or by doing factory reset. | ||
* | ||
* @since Xcode 15/iOS 17 | ||
* @param {string} [remotePath] The path to the remote location, where the resulting video should be | ||
* uploaded. | ||
* The following protocols are supported: `http`, `https`, `ftp`. Null or empty | ||
* string value (the default setting) means the content of resulting file | ||
* should be encoded as Base64 and passed as the endpoint response value. An | ||
* exception will be thrown if the generated media file is too big to fit into | ||
* the available process memory. | ||
* @param {string} [user] The name of the user for the remote authentication. | ||
* Only works if `remotePath` is provided. | ||
* @param {string} [pass] The password for the remote authentication. | ||
* Only works if `remotePath` is provided. | ||
* @param {import('@appium/types').HTTPHeaders} [headers] Additional headers mapping for multipart http(s) uploads | ||
* @param {string} [fileFieldName] The name of the form field where the file content BLOB should be stored for | ||
* http(s) uploads | ||
* @param {Record<string, any> | [string, any][]} [formFields] Additional form fields for multipart http(s) uploads | ||
* @param {'PUT' | 'POST' | 'PATCH'} [method='PUT'] The http multipart upload method name. | ||
* Only works if `remotePath` is provided. | ||
* @returns {Promise<XcTestScreenRecording>} | ||
* @throws {Error} If there was an error while retrieving the video | ||
* file or the file content cannot be uploaded to the remote location. | ||
* @this {XCUITestDriver} | ||
*/ | ||
async mobileStopXctestScreenRecording(remotePath, user, pass, headers, fileFieldName, formFields, method) { | ||
const screenRecordingInfo = await this.mobileGetXctestScreenRecordingInfo(); | ||
if (!screenRecordingInfo) { | ||
throw new Error('There is no active screen recording. Did you start one beforehand?'); | ||
} | ||
|
||
this.log.debug(`Stopping the active screen recording: ${JSON.stringify(screenRecordingInfo)}`); | ||
await this.proxyCommand('/wda/video/stop', 'POST', {}); | ||
const videoPath = await retrieveXcTestScreenRecording.bind(this)(screenRecordingInfo.uuid); | ||
const result = /** @type {XcTestScreenRecording} */ (screenRecordingInfo); | ||
try { | ||
result.payload = await encodeBase64OrUpload(videoPath, remotePath, { | ||
user, pass, headers, fileFieldName, formFields, method | ||
}); | ||
} finally { | ||
await fs.rimraf(videoPath); | ||
} | ||
return result; | ||
}, | ||
}; | ||
|
||
/** | ||
* @typedef {import('../driver').XCUITestDriver} XCUITestDriver | ||
*/ |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍