Skip to content

Commit

Permalink
Refactor WS subprotocol handling
Browse files Browse the repository at this point in the history
  • Loading branch information
ku1ik committed Jan 22, 2025
1 parent 0506708 commit 12087c3
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 138 deletions.
55 changes: 24 additions & 31 deletions src/driver/websocket.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ function websocket(
let successfulConnectionTimeout;
let stop = false;
let wasOnline = false;
let initTimeout;

function connect() {
socket = new WebSocket(url, ["v1.alis", "v2.asciicast", "raw"]);
Expand All @@ -33,11 +34,11 @@ function websocket(
logger.info(`activating ${proto} protocol handler`);

if (proto === "v1.alis") {
socket.onmessage = onMessage(alisHandler);
socket.onmessage = onMessage(alisHandler(logger));
} else if (proto === "v2.asciicast") {
socket.onmessage = onMessage(jsonHandler);
socket.onmessage = onMessage(jsonHandler());
} else if (proto === "raw") {
socket.onmessage = onMessage(rawHandler);
socket.onmessage = onMessage(rawHandler());
}

successfulConnectionTimeout = setTimeout(() => {
Expand All @@ -46,6 +47,7 @@ function websocket(
};

socket.onclose = (event) => {
clearTimeout(initTimeout);
stopBuffer();

if (stop || event.code === 1000 || event.code === 1005) {
Expand All @@ -67,7 +69,7 @@ function websocket(
}

function onMessage(handler) {
const initialHandler = handler;
initTimeout = setTimeout(enterOfflineMode, 5000);

return function (event) {
const result = handler(event.data);
Expand All @@ -78,31 +80,34 @@ function websocket(
} else if (typeof result === "string") {
buf.pushText(result);
} else if (result === false) {
stopBuffer();
handleOfflineMessage();
handler = initialHandler;
// EOT
enterOfflineMode();
} else if (result !== undefined) {
throw `unexpected value from protocol handler: ${result}`;
}
} else {
if (typeof result === "object" && !Array.isArray(result)) {
const { cols, rows, time, init, theme } = result.meta;
initStream(cols, rows, time, init, theme);
handler = result.handler;
} else if (result === false) {
handleOfflineMessage();
handler = initialHandler;
} else if (result !== undefined) {
const { time, term } = result;
const { size, init, theme } = term;
const { cols, rows } = size;
enterOnlineMode(cols, rows, time, init, theme);
clearTimeout(initTimeout);
} else if (result === undefined) {
clearTimeout(initTimeout);
initTimeout = setTimeout(enterOfflineMode, 1000);
} else {
clearTimeout(initTimeout);
throw `unexpected value from protocol handler: ${result}`;
}
}
};
}

function initStream(cols, rows, time, init, theme) {
function enterOnlineMode(cols, rows, time, init, theme) {
logger.debug(`stream init (${cols}x${rows} @${time})`);
setState("playing");
initBuffer(time);
stopBuffer();
buf = getBuffer(bufferTime, feed, resize, (t) => clock.setTime(t), time, minFrameTime, logger);
reset(cols, rows, init, theme);
clock = new Clock();
wasOnline = true;
Expand All @@ -112,26 +117,14 @@ function websocket(
}
}

function initBuffer(baseStreamTime) {
function enterOfflineMode() {
stopBuffer();

buf = getBuffer(
bufferTime,
feed,
resize,
(t) => clock.setTime(t),
baseStreamTime,
minFrameTime,
logger,
);
}

function handleOfflineMessage() {
logger.info("stream offline");

if (wasOnline) {
logger.info("stream ended");
setState("offline", { message: "Stream ended" });
} else {
logger.info("stream offline");
setState("offline", { message: "Stream offline" });
}

Expand Down
185 changes: 103 additions & 82 deletions src/driver/websocket/alis.js
Original file line number Diff line number Diff line change
@@ -1,94 +1,115 @@
function alisHandler(buffer) {
function alisHandler(logger) {
const outputDecoder = new TextDecoder();
const inputDecoder = new TextDecoder();
const arr = new Uint8Array(buffer);
let handler = parseMagicString;

if (!(arr[0] == 0x41 && arr[1] == 0x4c && arr[2] == 0x69 && arr[3] == 0x53 && arr[4] === 1)) {
// not 'ALiS\x01'
throw "not an ALiS v1 live stream";
}
function parseMagicString(buffer) {
const text = (new TextDecoder()).decode(buffer);

const view = new DataView(buffer);
let offset = 5;
const cols = view.getUint16(offset, true);
offset += 2;
const rows = view.getUint16(offset, true);
offset += 2;
const time = view.getFloat32(offset, true);
offset += 4;
const themeFormat = view.getUint8(offset);
offset += 1;
let theme;

if (themeFormat === 8) {
const len = (2 + 8) * 3;
theme = parseTheme(new Uint8Array(buffer, offset, len));
offset += len;
} else if (themeFormat === 16) {
const len = (2 + 16) * 3;
theme = parseTheme(new Uint8Array(buffer, offset, len));
offset += len;
} else if (themeFormat !== 0) {
logger.warn(`unsupported theme format (${themeFormat})`);
socket.close();
return;
if (text === "ALiS\x01") {
handler = parseInitFrame;
} else {
throw "not an ALiS v1 live stream";
}
}

const initLen = view.getUint32(offset, true);
offset += 4;

let init;
function parseInitFrame(buffer) {
const view = new DataView(buffer);
const type = view.getUint8(0);

if (type !== 0x01) throw `expected init (0x01) frame, got ${type}`;

let offset = 1;
const cols = view.getUint16(offset, true);
offset += 2;
const rows = view.getUint16(offset, true);
offset += 2;
const time = view.getFloat32(offset, true);
offset += 4;
const themeFormat = view.getUint8(offset);
offset += 1;
let theme;

if (themeFormat === 8) {
const len = (2 + 8) * 3;
theme = parseTheme(new Uint8Array(buffer, offset, len));
offset += len;
} else if (themeFormat === 16) {
const len = (2 + 16) * 3;
theme = parseTheme(new Uint8Array(buffer, offset, len));
offset += len;
} else if (themeFormat !== 0) {
logger.warn(`alis: unsupported theme format (${themeFormat})`);
socket.close();
return;
}

const initLen = view.getUint32(offset, true);
offset += 4;

let init;

if (initLen > 0) {
init = outputDecoder.decode(new Uint8Array(buffer, offset, initLen));
offset += initLen;
}

handler = parseEventFrame;

return {
time,
term: {
size: { cols, rows },
theme,
init
}
}
}

if (initLen > 0) {
init = outputDecoder.decode(new Uint8Array(buffer, offset, initLen));
offset += initLen;
function parseEventFrame(buffer) {
const view = new DataView(buffer);
const type = view.getUint8(0);

if (type === 0x6f) {
// 'o' - output
const time = view.getFloat32(1, true);
const len = view.getUint32(5, true);
const text = outputDecoder.decode(new Uint8Array(buffer, 9, len));

return [time, "o", text];
} else if (type === 0x69) {
// 'i' - input
const time = view.getFloat32(1, true);
const len = view.getUint32(5, true);
const text = inputDecoder.decode(new Uint8Array(buffer, 9, len));

return [time, "i", text];
} else if (type === 0x72) {
// 'r' - resize
const time = view.getFloat32(1, true);
const cols = view.getUint16(5, true);
const rows = view.getUint16(7, true);

return [time, "r", { cols, rows }];
} else if (type === 0x6d) {
// 'm' - marker
const time = view.getFloat32(1, true);
const len = view.getUint32(5, true);
const decoder = new TextDecoder();
const text = decoder.decode(new Uint8Array(buffer, 9, len));

return [time, "m", text];
} else if (type === 0x04) {
// EOT
handler = parseInitFrame;
return false;
} else {
logger.debug(`alis: unknown frame type: ${type}`);
}
}

const meta = { cols, rows, time, init, theme };

return {
meta,

handler: function(buffer) {
const view = new DataView(buffer);
const type = view.getUint8(0);

if (type === 0x6f) {
// 'o' - output
const time = view.getFloat32(1, true);
const len = view.getUint32(5, true);
const text = outputDecoder.decode(new Uint8Array(buffer, 9, len));

return [time, "o", text];
} else if (type === 0x69) {
// 'i' - input
const time = view.getFloat32(1, true);
const len = view.getUint32(5, true);
const text = inputDecoder.decode(new Uint8Array(buffer, 9, len));

return [time, "i", text];
} else if (type === 0x72) {
// 'r' - resize
const time = view.getFloat32(1, true);
const cols = view.getUint16(5, true);
const rows = view.getUint16(7, true);

return [time, "r", { cols, rows }];
} else if (type === 0x6d) {
// 'm' - marker
const time = view.getFloat32(1, true);
const len = view.getUint32(5, true);
const decoder = new TextDecoder();
const text = decoder.decode(new Uint8Array(buffer, 9, len));

return [time, "m", text];
} else if (type === 0x04) {
// offline (EOT)
return false; // go offline
} else {
logger.debug(`unknown event type: ${type}`);
}
},
return function(buffer) {
return handler(buffer);
};
}

Expand Down
46 changes: 30 additions & 16 deletions src/driver/websocket/json.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,40 @@
function jsonHandler(buffer) {
const header = JSON.parse(buffer);
function jsonHandler() {
let parse = parseHeader;

if (header.version !== 2) {
throw "not an asciicast v2 stream";
}

const meta = { cols: header.width, rows: header.height, time: 0.0 };
function parseHeader(buffer) {
const header = JSON.parse(buffer);

return {
meta,
if (header.version !== 2) {
throw "not an asciicast v2 stream";
}

handler: function(buffer) {
const event = JSON.parse(buffer);
parse = parseEvent;

if (event[1] === "r") {
const [cols, rows] = event[2].split("x");
return [event[0], "r", { cols, rows }];
} else {
return event;
return {
time: 0.0,
term: {
size: {
cols: header.width,
rows: header.height
}
}
};
}

function parseEvent(buffer) {
const event = JSON.parse(buffer);

if (event[1] === "r") {
const [cols, rows] = event[2].split("x");
return [event[0], "r", { cols, rows }];
} else {
return event;
}
}

return function(buffer) {
return parse(buffer);
};
}

export { jsonHandler };
Loading

0 comments on commit 12087c3

Please sign in to comment.