UNPKG

cadb

Version:

安卓/鸿蒙系统截图/录屏工具

342 lines (325 loc) 10.7 kB
/** * 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'); }