Create connection
TCPSocket
Direct Socket API is a new Web API that provides TCP and UDP sockets. The TCPSocket
class from this API can be used to create a TCP connection to an Android device.
- Web
- Node.js
As of September 2024, it's still not clear how Direct Socket API will be implemented. The current proposal requires the Web app to be bundled and signed by the developer, then manually installed by users. This is not a practical solution for general Web apps.
Install @types/node
package for code suggestions and type checking of Node.js APIs:
- npm
- Yarn
- pnpm
npm i -D @types/node
yarn add --dev @types/node
pnpm add -D @types/node
The following code converts Node.js's net.Socket
to a TCPSocket
:
- JavaScript
- TypeScript
import { PromiseResolver } from "@yume-chan/async";
import { PushReadableStream, WritableStream } from "@yume-chan/stream-extra";
import { connect } from "node:net";
export class TCPSocket {
#socket;
#opened = new PromiseResolver();
get opened() {
return this.#opened.promise;
}
constructor(remoteAddress, remotePort, options) {
this.#socket = connect(remotePort, remoteAddress);
if (options?.noDelay) {
this.#socket.setNoDelay(true);
}
if (options?.unref) {
this.#socket.unref();
}
this.#socket.on("connect", () => {
const readable = new PushReadableStream((controller) => {
this.#socket.on("data", async (data) => {
this.#socket.pause();
await controller.enqueue(data);
this.#socket.resume();
});
this.#socket.on("end", () => {
try {
controller.close();
} catch {}
});
controller.abortSignal.addEventListener("abort", () => {
this.#socket.end();
});
});
this.#opened.resolve({
remoteAddress,
remotePort,
localAddress: this.#socket.localAddress,
localPort: this.#socket.localPort,
readable,
writable: new WritableStream({
write: async (chunk) => {
return new Promise((resolve, reject) => {
if (!this.#socket.write(chunk)) {
this.#socket.once("drain", resolve);
} else {
resolve();
}
});
},
close: async () => {
this.#socket.end();
},
}),
});
});
this.#socket.on("error", (error) => {
this.#opened.reject(error);
});
}
}
import { PromiseResolver } from "@yume-chan/async";
import { PushReadableStream, WritableStream, type ReadableStream } from "@yume-chan/stream-extra";
import { connect, type Socket } from "node:net";
export interface TCPSocketOptions {
noDelay?: boolean;
unref?: boolean;
}
export interface TCPSocketOpenInfo {
readable: ReadableStream<Uint8Array>;
writable: WritableStream<Uint8Array>;
remoteAddress: string;
remotePort: number;
localAddress: string;
localPort: number;
}
export class TCPSocket {
#socket: Socket;
#opened = new PromiseResolver<TCPSocketOpenInfo>();
get opened(): Promise<TCPSocketOpenInfo> {
return this.#opened.promise;
}
constructor(remoteAddress: string, remotePort: number, options?: TCPSocketOptions) {
this.#socket = connect(remotePort, remoteAddress);
if (options?.noDelay) {
this.#socket.setNoDelay(true);
}
if (options?.unref) {
this.#socket.unref();
}
this.#socket.on("connect", () => {
const readable = new PushReadableStream<Uint8Array>((controller) => {
this.#socket.on("data", async (data) => {
this.#socket.pause();
await controller.enqueue(data);
this.#socket.resume();
});
this.#socket.on("end", () => {
try {
controller.close();
} catch {}
});
controller.abortSignal.addEventListener("abort", () => {
this.#socket.end();
});
});
this.#opened.resolve({
remoteAddress,
remotePort,
localAddress: this.#socket.localAddress!,
localPort: this.#socket.localPort!,
readable,
writable: new WritableStream({
write: async (chunk) => {
return new Promise<void>((resolve, reject) => {
if (!this.#socket.write(chunk)) {
this.#socket.once("drain", resolve);
} else {
resolve();
}
});
},
close: async () => {
this.#socket.end();
},
}),
});
});
this.#socket.on("error", (error) => {
this.#opened.reject(error);
});
}
}
AdbDaemonDirectSocketsDevice
The following code implements an AdbDaemonDevice
using a TCPSocket
:
This code is based on Custom connection.
- JavaScript
- TypeScript
import { AdbPacket, AdbPacketSerializeStream } from "@yume-chan/adb";
import {
StructDeserializeStream,
MaybeConsumable,
WrapReadableStream,
WrapWritableStream,
} from "@yume-chan/stream-extra";
export class AdbDaemonDirectSocketsDevice {
static isSupported() {
return true;
}
#options;
serial;
get host() {
return this.#options.host;
}
port;
get name() {
return this.#options.name;
}
constructor(options) {
this.#options = options;
this.port = options.port ?? 5555;
this.serial = `${this.host}:${this.port}`;
}
async connect() {
const socket = new TCPSocket(this.host, this.port, {
noDelay: true,
unref: this.#options.unref,
});
const { readable, writable } = await socket.opened;
return {
readable: new WrapReadableStream(readable).pipeThrough(
new StructDeserializeStream(AdbPacket),
),
writable: new WrapWritableStream(writable)
.bePipedThroughFrom(new MaybeConsumable.UnwrapStream())
.bePipedThroughFrom(new AdbPacketSerializeStream()),
};
}
}
import type { AdbDaemonDevice } from "@yume-chan/adb";
import { AdbPacket, AdbPacketSerializeStream } from "@yume-chan/adb";
import {
StructDeserializeStream,
MaybeConsumable,
WrapReadableStream,
WrapWritableStream,
} from "@yume-chan/stream-extra";
export interface AdbDaemonDirectSocketDeviceOptions {
host: string;
port?: number;
name?: string;
unref?: boolean;
}
export class AdbDaemonDirectSocketsDevice implements AdbDaemonDevice {
static isSupported(): boolean {
return true;
}
#options: AdbDaemonDirectSocketDeviceOptions;
readonly serial: string;
get host(): string {
return this.#options.host;
}
readonly port: number;
get name(): string | undefined {
return this.#options.name;
}
constructor(options: AdbDaemonDirectSocketDeviceOptions) {
this.#options = options;
this.port = options.port ?? 5555;
this.serial = `${this.host}:${this.port}`;
}
async connect() {
const socket = new TCPSocket(this.host, this.port, {
noDelay: true,
unref: this.#options.unref,
});
const { readable, writable } = await socket.opened;
return {
readable: new WrapReadableStream(readable).pipeThrough(
new StructDeserializeStream(AdbPacket),
),
writable: new WrapWritableStream(writable)
.bePipedThroughFrom(new MaybeConsumable.UnwrapStream())
.bePipedThroughFrom(new AdbPacketSerializeStream()),
};
}
}
Usage
The following code creates a connection:
- JavaScript
- TypeScript
const device = new AdbDaemonDirectSocketsDevice({
host: "192.168.50.30",
port: 5555,
});
const connection = await device.connect();
import type { ReadableWritablePair } from "@yume-chan/stream-extra";
const device: AdbDaemonDirectSocketsDevice = new AdbDaemonDirectSocketsDevice({
host: "192.168.50.30",
port: 5555,
});
const connection: ReadableWritablePair<
AdbPacketData,
Consumable<AdbPacketInit>
> = await device.connect();
Next Step