@u4/adbkit
Version:
A Typescript client for the Android Debug Bridge.
301 lines • 13.4 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const stream_1 = require("stream");
const promise_duplex_1 = __importDefault(require("promise-duplex"));
const ThirdUtils_1 = __importDefault(require("../ThirdUtils"));
const utils_1 = __importDefault(require("../../utils"));
const fs = __importStar(require("fs"));
const debug = utils_1.default.debug('adb:minicap');
class Minicap extends stream_1.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;
let soFile = '';
try {
binFile = require.resolve(`/minicap-prebuilt/prebuilt/${abi}/bin/${minicapName}`);
}
catch (e) {
throw Error(`minicap not found in /minicap-prebuilt/prebuilt/${abi}/bin/ please install /minicap-prebuilt to use minicap`);
}
try {
if (sdkLevel === 32) {
soFile = require.resolve(`/minicap-prebuilt/prebuilt/${abi}/lib/android-${sdkLevel}/minicap.so`);
}
else {
soFile = require.resolve(`/minicap-prebuilt/prebuilt/${abi}/lib/android-${sdkLevel}/minicap.so`);
}
}
catch (e) {
throw Error(`minicap.so for your device check for /minicap-prebuilt update that support android-${sdkLevel}, ${soFile} is missing`);
}
// 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 presentin ${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_1.default.getScreenSize(this.client);
const dim = `${x}x${y}`;
args.push(`${dim}@${dim}/0`);
}
else {
args.push(this.config.dimention);
}
}
this.minicapServer = new promise_duplex_1.default(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_1.default.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_1.default.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_1.default.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_1.default.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_1.default.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;
}
}
exports.default = Minicap;
//# sourceMappingURL=Minicap.js.map