@u4/adbkit
Version:
A Typescript client for the Android Debug Bridge.
291 lines • 12.7 kB
JavaScript
import { EventEmitter } from 'node:events';
import * as fs from 'node:fs';
import { Buffer } from 'node:buffer';
import PromiseDuplex from 'promise-duplex';
import ThirdUtils from "../ThirdUtils.js";
import Utils from '../../utils.js';
// import { resolve } from 'import-meta-resolve';
import prebuilds from "@u4/minicap-prebuilt";
const debug = Utils.debug('adb:minicap');
// fs.ReadStream |
// function getResource(path: string): string | null {
// try {
// return require.resolve(path);
// } catch (e) {
// try {
// if (e instanceof Error && e.message.includes('require is not defined')) {
// const url = resolve(path, import.meta.url);
// return new URL(url).pathname;
// }
// } catch (e) {
// return null;
// }
// return null;
// }
// }
export default class Minicap extends EventEmitter {
constructor(client, config = {}) {
super();
this.client = client;
this.setFirstFrame = null;
/**
* closed had been call stop all new activity
*/
this.closed = false;
this.on = (event, listener) => super.on(event, listener);
this.off = (event, listener) => super.off(event, listener);
this.once = (event, listener) => super.once(event, listener);
this.emit = (event, ...args) => super.emit(event, ...args);
this.config = {
dimention: '',
...config,
};
this._version = new Promise((resolve) => this.setVersion = resolve);
this._pid = new Promise((resolve) => this.setPid = resolve);
this._realWidth = new Promise((resolve) => this.setRealWidth = resolve);
this._realHigth = new Promise((resolve) => this.setRealHigth = resolve);
this._virtualWidth = new Promise((resolve) => this.setVirtualWidth = resolve);
this._virtualHigth = new Promise((resolve) => this.setVirtualHigth = resolve);
this._orientation = new Promise((resolve) => this.setOrientation = resolve);
this._bitflags = new Promise((resolve) => this.setBitflags = resolve);
this._firstFrame = new Promise((resolve) => this.setFirstFrame = resolve);
}
get version() { return this._version; }
get pid() { return this._pid; }
get realwidth() { return this._realWidth; }
get realheight() { return this._realHigth; }
get vitualWidth() { return this._virtualWidth; }
get vitualHeight() { return this._virtualHigth; }
/**
* return virtual width
*/
get width() { return this._virtualWidth; }
/**
* return virtual heigth
*/
get height() { return this._virtualHigth; }
get orientation() { return this._orientation; }
/**
* return full bitflags, QuickDumb, QuickAlwaysUpright and QuickTear can be used.
*/
get bitflags() { return this._bitflags; }
/**
* Frames will get sent even if there are no changes from the previous frame. Informative, doesn't require any actions on your part. You can limit the capture rate by reading frame data slower in your own code if you wish.
*/
get QuickDumb() { return this.bitflags.then(v => !!(v & 1)); }
/**
* The frame will always be in upright orientation regardless of the device orientation. This needs to be taken into account when rendering the image.
*/
get QuickAlwaysUpright() { return this.bitflags.then(v => !!(v & 2)); }
/**
* Frame tear might be visible. Informative, no action required. Neither of our current two methods exhibit this behavior.
*/
get QuickTear() { return this.bitflags.then(v => !!(v & 4)); }
/**
* Promise to the first emited frame
* can be used to unsure that scrcpy propery start
*/
get firstFrame() { return this._firstFrame; }
/**
*
* @returns resolved once localabstract:minicap is connected
*/
async start() {
if (this.closed) // can not start once stop called
return this;
const props = await this.client.getProperties();
const abi = props['ro.product.cpu.abi'];
const sdkLevel = parseInt(props['ro.build.version.sdk']);
// const minicapName = (sdkLevel >= 16) ? 'minicap' : 'minicap-nopie';
let binFile = null;
let soFile = null;
binFile = prebuilds.getMinicapBin(abi, sdkLevel); // getResource(`@devicefarmer/minicap-prebuilt/prebuilt/${abi}/bin/${minicapName}`);
if (!binFile)
throw Error(`minicap not found in /minicap-prebuilt/prebuilt/${abi}/bin/ please install /minicap-prebuilt to use minicap`);
{
const stats = await fs.promises.stat(binFile).catch(() => null);
if (!stats)
throw Error(`Minicap binary not found in ${binFile} for hour host in /minicap-prebuilt`);
}
soFile = prebuilds.getMinicapSO(abi, sdkLevel);
//if (!soFile) {
// throw Error(`minicap.so for your device check for @devicefarmer/minicap-prebuilt update that support android-${sdkLevel}, ${soFile} is missing`);
//}
{
const stats = await fs.promises.stat(soFile).catch(() => null);
if (!stats)
throw Error(`minicap.so not found in ${soFile} for hour host in /minicap-prebuilt`);
}
// only upload minicap binary in tmp filder if file is missing
try {
await this.client.stat('/data/local/tmp/minicap');
debug(`/data/local/tmp/minicap already present in ${this.client.serial}`);
}
catch {
debug(`pushing minicap binary to ${this.client.serial}`);
const tr = await this.client.push(binFile, '/data/local/tmp/minicap', 0o755);
await tr.waitForEnd();
}
// only upload minicap.so in tmp filder if file is missing
const localStats = fs.statSync(soFile);
let droidStats;
try {
droidStats = await this.client.stat('/data/local/tmp/minicap.so');
}
catch {
droidStats = undefined;
}
if (!droidStats || droidStats.size !== localStats.size) {
if (droidStats)
debug(`minicap.so version mismatch in ${this.client.serial} overwriting file.`);
const tr = await this.client.push(soFile, '/data/local/tmp/minicap.so', 0o755);
await tr.waitForEnd();
}
// await this.client.push(apkFile, '/data/local/tmp/minicap.apk', 0o755);
// adb push libs/$ABI/minicap /data/local/tmp/
const runnings = await this.client.getPs('-A');
const minicapPs = runnings.filter(p => p.NAME === 'minicap');
for (const ps of minicapPs) {
debug(`killing old minicap process ${ps.PID}`);
await this.client.execOut(`kill ${ps.PID}`);
}
const args = ['LD_LIBRARY_PATH=/data/local/tmp/', 'exec', '/data/local/tmp/minicap'];
{
args.push('-P');
if (!this.config.dimention) {
const { x, y } = await ThirdUtils.getScreenSize(this.client);
const dim = `${x}x${y}`;
args.push(`${dim}@${dim}/0`);
}
else {
args.push(this.config.dimention);
}
}
this.minicapServer = new PromiseDuplex(await this.client.shell(args.map(a => a.toString()).join(' ')));
this.minicapServer.once("finish").then(() => {
this.stop(`minicap server finish`);
});
// minicap: PID: 14231
// INFO: Using projection 1080x2376@1080x2376/0
// INFO: (external/MY_minicap/src/minicap_30.cpp:243) Creating SurfaceComposerClient
// INFO: (external/MY_minicap/src/minicap_30.cpp:246) Performing SurfaceComposerClient init check
// INFO: (external/MY_minicap/src/minicap_30.cpp:257) Creating virtual display
// INFO: (external/MY_minicap/src/minicap_30.cpp:263) Creating buffer queue
// INFO: (external/MY_minicap/src/minicap_30.cpp:266) Setting buffer options
// INFO: (external/MY_minicap/src/minicap_30.cpp:270) Creating CPU consumer
// INFO: (external/MY_minicap/src/minicap_30.cpp:274) Creating frame waiter
// INFO: (external/MY_minicap/src/minicap_30.cpp:278) Publishing virtual display
// INFO: (jni/minicap/JpgEncoder.cpp:64) Allocating 7783428 bytes for JPG encoder
// INFO: (jni/minicap/JpgEncoder.cpp:64) Allocating 3458052 bytes for JPG encoder
await Utils.waitforText(this.minicapServer, /JpgEncoder/, 5000);
// Connect videoSocket
if (!this.closed) {
try {
this.videoSocket = await this.client.openLocal2('localabstract:minicap');
}
catch (e) {
debug(`Impossible to connect video Socket localabstract:minicap`, e);
throw e;
}
this.videoSocket.once('end').then(() => { this.stop('connection to localabstract:minicap ended'); });
// this.videoSocket.once('finish').then(() => { this.stop('connection to localabstract:minicap finished') })
void this.startStream(this.videoSocket).catch((e) => this.stop(`stream throws ${e}`));
// wait until first stream chunk is recieved
}
await this.bitflags;
return this;
}
async startStream(videoSocket) {
// first chunk 24
try {
await Utils.waitforReadable(videoSocket);
let firstChunk = await videoSocket.read(2);
if (!firstChunk) {
throw Error('Fail to read firstChunk 2 byte Header.');
}
this.setVersion(firstChunk[0]); // == 1
const len = firstChunk[1]; // == 24
firstChunk = await videoSocket.read(len - 2);
if (!firstChunk) {
throw Error('Fail to read firstChunk data.');
}
this.setPid(firstChunk.readUint32LE(0));
this.setRealWidth(firstChunk.readUint32LE(4));
this.setRealHigth(firstChunk.readUint32LE(8));
this.setVirtualWidth(firstChunk.readUint32LE(12));
this.setVirtualHigth(firstChunk.readUint32LE(16));
this.setOrientation(firstChunk.readUint8(20));
this.setBitflags(firstChunk.readUint8(21));
}
catch (e) {
debug('Impossible to read first chunk:', e);
throw e;
}
for (;;) {
if (this.closed)
return;
await Utils.waitforReadable(videoSocket);
let chunk = videoSocket.stream.read(4);
if (!chunk)
continue;
let len = chunk.readUint32LE(0);
// len -= 4;
let streamChunk = null;
while (streamChunk === null) {
if (this.closed)
return;
await Utils.waitforReadable(videoSocket);
chunk = videoSocket.stream.read(len);
if (chunk) {
len -= chunk.length;
if (!streamChunk)
streamChunk = chunk;
else {
streamChunk = Buffer.concat([streamChunk, chunk]);
}
if (streamChunk[0] !== 0xFF || streamChunk[1] !== 0xD8) {
console.error('Frame body does not start with JPG header');
return;
}
}
if (len === 0) {
if (this.setFirstFrame) {
this.setFirstFrame();
this.setFirstFrame = null;
}
if (streamChunk)
this.emit('data', streamChunk);
break;
}
else {
await Utils.delay(0);
}
}
}
}
/**
* closed all socket and emit disconnect in if socket closed
* @returns true if videoSocket or minicapServer get closed
*/
stop(cause) {
this.closed = true;
let close = false;
if (this.videoSocket) {
this.videoSocket.destroy();
this.videoSocket = undefined;
close = true;
}
if (this.minicapServer) {
this.minicapServer.destroy();
this.minicapServer = undefined;
close = true;
}
if (close)
this.emit('disconnect', cause || 'close() called');
return close;
}
isRunning() {
return this.videoSocket !== null;
}
}
//# sourceMappingURL=Minicap.js.map