Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion src/broadcaster.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { WebSocket } from "ws";
import { buildFrameStatsPacket, buildFramePackets } from "./protocol.js";
import { buildFrameStatsPacket, buildFramePackets, buildCurrentURLPacket } from "./protocol.js";
import type { FrameOut } from "./frameProcessor.js";

type OutFrame = { frameId: number; packets: Buffer[] };
Expand Down Expand Up @@ -62,6 +62,20 @@ export class DeviceBroadcaster {
this._drainAsync(id).catch(() => {});
}

// Send packet with current URL info to connected client:
public sendCurrentURL(id: string, url: string): void {
const peers = this._clients.get(id);
if (!peers || peers.size === 0) return;

// We use the URL packet packer from protocol.js here
const packet = buildCurrentURLPacket(url);
const st = this._ensureState(id);

// We use frameId: 0 since this is a control packet, not an image frame
st.queue.push({ frameId: 0, packets: [packet] });
this._drainAsync(id).catch(() => {});
}

private _ensureState(id: string): BroadcasterState {
let st = this._state.get(id);
if (!st) {
Expand Down
19 changes: 19 additions & 0 deletions src/deviceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,25 @@ export async function ensureDeviceAsync(id: string, cfg: DeviceConfig): Promise<
}
});

// Function to deal with URL changing via either full page refresh or single page # follow
const handleNavigation = (url: string) => {
if (newDevice.url !== url) {
newDevice.url = url;
broadcaster.sendCurrentURL(newDevice.deviceId, url);
console.log(`[device] URL changed to: ${url}`);
}
};
// Triggered on full page loads
session.on('Page.frameNavigated', (evt: any) => {
if (!evt.frame.parentId) { // Only track the main frame, ignore iframes
handleNavigation(evt.frame.url);
}
});
// Triggered on Single Page App (SPA) hash or history API changes
session.on('Page.navigatedWithinDocument', (evt: any) => {
handleNavigation(evt.url);
});

return newDevice;
}

Expand Down
15 changes: 15 additions & 0 deletions src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
// Keepalive message:
// [type u8=5][ver u8=1]
//
// Current URL packet:
// [type u8][ver u8][len u32][url utf8 bytes...]

export const PROTOCOL_VERSION = 1 as const;

Expand All @@ -26,6 +28,7 @@ export enum MsgType {
FrameStats = 3,
OpenURL = 4,
Keepalive = 5,
CurrentURL = 6, // <-- Current URL packet
}

export enum Encoding {
Expand Down Expand Up @@ -75,9 +78,21 @@ export const TILE_HEADER_BYTES = 2 + 2 + 2 + 2 + 4; // 12
export const TOUCH_BYTES = 1 + 1 + 1 + 1 + 2 + 2; // 8
export const FRAME_STATS_BYTES = 1 + 1 + 4 + 4; // 10
export const OPENURL_HEADER_BYTES = 1 + 1 + 2 + 4; // 8
export const CURRENTURL_HEADER_BYTES = 1 + 1 + 4; // 6 bytes: [type u8][ver u8][len u32] - Current URL header

const clampU16 = (v: number) => (v < 0 ? 0 : v > 0xffff ? 0xffff : v|0);

// Current URL packet
export function buildCurrentURLPacket(url: string): Buffer {
const urlBuf = Buffer.from(url, "utf8");
const buf = Buffer.alloc(CURRENTURL_HEADER_BYTES + urlBuf.length);
buf.writeUInt8(MsgType.CurrentURL, 0);
buf.writeUInt8(PROTOCOL_VERSION, 1);
buf.writeUInt32LE(urlBuf.length, 2);
urlBuf.copy(buf, CURRENTURL_HEADER_BYTES);
return buf;
}

export function buildTouchPacket(kind: TouchKind, x: number, y: number, pointerId = 0): Buffer {
const buf = Buffer.alloc(TOUCH_BYTES);
buf.writeUInt8(MsgType.Touch, 0);
Expand Down