cadb
Version:
安卓/鸿蒙系统截图/录屏工具
342 lines (325 loc) • 10.7 kB
JavaScript
/**
* Created by Niki on 2024/8/14 15:38.
* Email: m13296644326@163.com
*/
const program = require('commander');
const {execSync, execFileSync, execFile, exec} = require('child_process');
const {shellConfigs, ConsoleColors, logRed} = require('../common/constants');
const {
isHarmonyDeviceConnected,
getSystemHomePath,
chooseHarmonyRealDeviceId,
isAndroidDeviceConnected,
chooseAndroidRealDeviceId,
openFileInFinder,
hdcPath,
adbPath,
isWin,
listenAnyKeyInput,
} = require('../common/utils');
const fs = require('fs');
const path = require('path');
const packageInfo = require('./../package.json');
const _ = require('lodash');
const os = require('os');
let destDir = `${getSystemHomePath()}/${packageInfo.name}-videoRecord/`;
if (isWin()) {
destDir = path.join(os.homedir(), `${packageInfo.name}-videoRecord`);
}
// 安卓
const deviceVideoDir = '/sdcard/videoCapture';
let tempDeviceVideoPath = path.join(deviceVideoDir, 'capVideo2.mp4');
if (isWin()) {
// win系统下使用path.join会出现反斜杠,而安卓设备是基于linux的
tempDeviceVideoPath = '/sdcard/videoCapture/capVideo2.mp4';
}
program
.option('-c. --clean', 'clean the screenshot directory')
.action(arg => {
run(arg);
})
.parse(process.argv);
async function run(arg) {
const {clean} = arg;
if (clean) {
// todo 实现清除逻辑
}
let consumed = await doAndroidRecord();
if (!consumed) {
console.log('\r\n尝试连接鸿蒙设备');
await doHarmonyRecord();
}
}
async function doHarmonyRecord() {
let dList = isHarmonyDeviceConnected();
if (!dList) {
return false;
}
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir);
}
const realDeviceId = await chooseHarmonyRealDeviceId(dList);
// 鸿蒙录屏传入固定名字
const fullPath = await exeHMRecord('harmony' + Date.now(), realDeviceId);
setTimeout(() => {
console.log(ConsoleColors.green, `视频录制结束, 文件已保存至${fullPath}`);
// open -R可打开finder并选中文件
openFileInFinder(fullPath);
// TODO: 是否直接打开视频, 走配置文件
// execSync(`open ${fullPath}`);
process.exit();
}, 50);
return true;
}
function exeHMRecord(videoName, realDeviceCmd = '') {
let idParamList = [];
if (realDeviceCmd) {
idParamList = realDeviceCmd.split(' ');
}
/*const runDeviceShotCmd =
`hdc ${realDeviceCmd} shell aa start -b com.huawei.hmos.screenrecorder ` +
'-a com.huawei.hmos.screenrecorder.ServiceExtAbility --ps ' +
`"CustomizedFileName" "${videoName}.mp4"`;*/
const runDeviceShotCmd = [
...idParamList,
'shell',
'aa',
'start',
'-b',
'com.huawei.hmos.screenrecorder',
'-a',
'com.huawei.hmos.screenrecorder.ServiceExtAbility',
'--ps',
'CustomizedFileName',
`${videoName}.mp4`,
];
console.log('execute:', `hdc ${runDeviceShotCmd.join(' ')}`);
console.log(execFileSync(hdcPath, runDeviceShotCmd, shellConfigs));
return new Promise((resolve, reject) => {
console.log(
ConsoleColors.green,
"\r\nYour phone's video recording has started\r\nPress any key to stop recording...",
);
listenAnyKeyInput().then(() => {
// 用户停止了录屏
/*const stopDeviceShotCmd =
`hdc ${realDeviceCmd} shell aa start -b com.huawei.hmos.screenrecorder -a ` +
'com.huawei.hmos.screenrecorder.ServiceExtAbility';*/
const stopDeviceShotCmd = [
...idParamList,
'shell',
'aa',
'start',
'-b',
'com.huawei.hmos.screenrecorder',
'-a',
'com.huawei.hmos.screenrecorder.ServiceExtAbility',
];
console.log('execute:', `hdc ${stopDeviceShotCmd.join(' ')}`);
console.log(execFileSync(hdcPath, stopDeviceShotCmd, shellConfigs));
// 查询录屏文件位置
// const queryVideoCmd = `hdc ${realDeviceCmd} shell mediatool query ${videoName}.mp4`;
const queryVideoCmd = [
...idParamList,
'shell',
'mediatool',
'query',
`${videoName}.mp4`,
'-u',
];
console.log('execute:', `hdc ${queryVideoCmd.join(' ')}`);
const stdout = execFileSync(hdcPath, queryVideoCmd, shellConfigs);
console.log('result', stdout);
let deviceVideoFileUri = parseHMValidVideoPath(stdout);
if (!deviceVideoFileUri) {
const hint = '录屏失败: 未在设备端找到录屏文件';
logRed(hint);
reject(hint);
process.exit();
return;
}
console.log('deviceVideoFileUri:', deviceVideoFileUri);
// 从设备端拉取录屏文件; 必须延迟一下, 否则文件解析错误
setTimeout(() => {
// 必须先移动到手机的/data/local/tmp/目录, 直接从相册拉取会报权限不足
const tempPath = path.join('/data/local/tmp', `${videoName}.mp4`);
const cmdMoveToTemp = [
...idParamList,
'shell',
'mediatool',
'recv',
deviceVideoFileUri,
tempPath,
];
console.log('execute:', `hdc ${cmdMoveToTemp.join(' ')}`);
const resultMove = execFileSync(hdcPath, cmdMoveToTemp, {
...shellConfigs,
cwd: destDir,
});
console.log(resultMove);
// const cmdPull = `hdc ${realDeviceCmd} file recv ${deviceVideoFileUri}`;
const cmdPull = [...idParamList, 'file', 'recv', tempPath];
console.log('execute:', `hdc ${cmdPull.join(' ')}`);
const resultPull = execFileSync(hdcPath, cmdPull, {
...shellConfigs,
cwd: destDir,
});
console.log(resultPull);
// 删掉设备上的原视频文件, 以免储存占用
// todo 删除temp目录的文件
const deleteVideoCmd = [
...idParamList,
'shell',
'mediatool',
'delete',
deviceVideoFileUri,
];
console.log('execute:', `hdc ${deleteVideoCmd.join(' ')}`);
const resultDelete = execFileSync(hdcPath, deleteVideoCmd, {
...shellConfigs,
cwd: destDir,
});
console.log(resultDelete);
let fileName = path.basename(tempPath);
const fullPath = path.join(destDir, fileName);
resolve(fullPath);
}, 50);
});
});
}
function parseHMValidVideoPath(stdout) {
const splitList = stdout
.split('\n')
.filter(t => !!t.trim())
.map(t => t.trim());
const index = _.findIndex(splitList, t => {
return /find \d+ result/.test(t);
});
return splitList[index + 2].trim().replaceAll('"', '');
}
async function doAndroidRecord() {
const dList = isAndroidDeviceConnected();
if (!dList) {
return false;
}
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir);
}
const rightFileName = `android_${parseInt(Date.now() / 1000)}_android`;
const fullPath = await exeAndroidRecord(
rightFileName,
chooseAndroidRealDeviceId(dList),
);
setTimeout(() => {
console.log(
ConsoleColors.green,
`\r\n视频录制结束, 文件已保存至${fullPath}`,
);
// open -R可打开finder并选中文件
openFileInFinder(fullPath);
// TODO: 是否直接打开视频, 走配置文件
// execSync(`open ${fullPath}`);
process.exit();
}, 50);
return true;
}
function exeAndroidRecord(rightFileName, realDeviceCmd = '') {
return new Promise(async (resolve, reject) => {
let idParamList = [];
if (realDeviceCmd) {
idParamList = realDeviceCmd.split(' ');
}
try {
const mkdirCmd = [...idParamList, 'shell', 'mkdir', deviceVideoDir];
console.log('execute:', `adb ${mkdirCmd.join(' ')}`);
console.log(execFileSync(adbPath, mkdirCmd, shellConfigs));
} catch (err) {}
const videoPath = path.join(destDir, `${rightFileName}.mp4`);
const supportAdb = isDeviceSupportAdbRecord(idParamList);
let childProcess;
if (supportAdb) {
childProcess = runAdbRecord(idParamList);
} else {
console.log('无法使用adb screenrecord, 将使用scrcpy录屏');
childProcess = runScrcpyRecord(idParamList, videoPath);
}
console.log(
ConsoleColors.green,
"\r\nYour phone's video recording has started\r\nPress any key to stop recording...",
);
await listenAnyKeyInput();
childProcess.kill();
// 这个延迟必须要加, 否则视频无法播放
setTimeout(() => {
if (supportAdb) {
const pullCmd = [
...idParamList,
'pull',
tempDeviceVideoPath,
videoPath,
];
console.log('execute:', `adb ${pullCmd.join(' ')}`);
console.log(execFileSync(adbPath, pullCmd, shellConfigs));
// 删掉设备上的原视频文件, 以免储存占用
const rmCmd = [...idParamList, 'shell', 'rm', '-r', deviceVideoDir];
console.log('execute:', `adb ${rmCmd.join(' ')}`);
console.log(execFileSync(adbPath, rmCmd, shellConfigs));
} else {
// 校验产物, 以确认scrcpy是否录屏成功
if (!fs.existsSync(videoPath)) {
const hint = `录屏失败: 未在${videoPath}找到录屏文件`;
logRed(hint);
reject(hint);
process.exit();
return;
}
}
resolve(videoPath);
}, 200);
});
}
function runAdbRecord(idParamList) {
const recordCmd = [
...idParamList,
'shell',
'screenrecord',
tempDeviceVideoPath,
];
console.log('execute:', `adb ${recordCmd.join(' ')}`);
return execFile(adbPath, recordCmd, (error, stdout, stderr) => {
if (error) {
// 不要打印它, 会误报错误
// console.log('video record error:', error);
} else if (stdout) {
console.log(stdout);
}
});
}
function runScrcpyRecord(idParamList, videoPath) {
// 先检测scrcpy是否安装
const ckRes = execSync('scrcpy --version', shellConfigs);
console.log('execute:', 'scrcpy --version');
console.log(ckRes.toString());
if (!ckRes || ckRes.includes('command not found')) {
console.log('请先安装scrcpy: brew install scrcpy');
process.exit();
return;
}
const recordCmd = [
'scrcpy',
...idParamList,
'--record',
videoPath,
'--no-playback',
'--no-window',
];
console.log('execute:', recordCmd.join(' '));
return exec(recordCmd.join(' '), shellConfigs);
}
function isDeviceSupportAdbRecord(idParamList) {
const checkCmd = [...idParamList, 'shell', 'ls', '/system/bin/'];
console.log('execute:', `adb ${checkCmd.join(' ')}`);
const lsString = execFileSync(adbPath, checkCmd, shellConfigs);
const dirList = lsString.split(/\s/);
return dirList.includes('screenrecord');
}