clean up test slop

This commit is contained in:
zefie
2026-04-21 09:01:36 -04:00
parent a346fd7f69
commit f8ad40cb41
3 changed files with 60 additions and 178 deletions

View File

@@ -2,6 +2,7 @@ const net = require('net');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const dgram = require('dgram');
const { WTVShared } = require('./WTVShared.js');
class WTVPNM {
@@ -67,12 +68,6 @@ class WTVPNM {
mediaPath: null,
notFoundSent: false,
pnaFields: null,
tcpTimer: null,
tcpSeq: 0,
ipId: 0,
tcpStartTimer: null,
mediaData: null,
mediaOffset: 0,
bytesRx: 0,
bytesTx: 0,
// Control-stream command accumulator. TCP is byte-oriented so
@@ -179,9 +174,12 @@ class WTVPNM {
}
if (session.requestedMedia && !session.mediaPath) {
console.log(' * PNM RealServer Warning: requested media not found', session.requestedMedia);
this.sendNotFound(socket, session.requestedMedia);
session.notFoundSent = true;
return;
} else {
console.log(' * PNM RealServer Request from', session.id, 'for media', session.mediaPath);
}
}
@@ -210,7 +208,7 @@ class WTVPNM {
} else {
this.debugLog('client hash MISMATCH', session.id, 'expected', expectedResp);
}
if (session.clientUdpPort && this.service_config.auto_stream !== false) {
if (session.clientUdpPort) {
this.startUdpStream(socket, session);
}
} else {
@@ -501,24 +499,6 @@ class WTVPNM {
}
}
scheduleDescriptorSequence(socket, session) {
this.clearDescriptorTimer(session);
let delayMs = null;
let reason = 'webtv-sequence';
if (this.service_config.descriptor_on_first_pna === true) {
delayMs = (typeof this.service_config.descriptor_after_hello_ms === 'number')
? this.service_config.descriptor_after_hello_ms
: (this.service_config.descriptor_fallback_ms || 20);
}
if (delayMs === null) return;
this.debugLog('descriptor scheduled', session.id, reason, `${delayMs}ms`);
session.descriptorTimer = setTimeout(() => {
this.sendDescriptorAndStartStream(socket, session, reason);
}, delayMs);
}
sendDescriptorAndStartStream(socket, session, reason) {
if (!socket || !session || session.descriptorSent) return;
this.clearDescriptorTimer(session);
@@ -631,14 +611,22 @@ class WTVPNM {
const startDelayMs = 72;
const redundantSeqs = [0, 1];
// Pre-start burst: send the first N ms of audio at double rate to
// pre-fill the client buffer before settling into normal pacing.
const burstPrestartMs = typeof this.service_config.burst_prestart_ms === 'number'
? this.service_config.burst_prestart_ms
: 3000;
const burstFrameCount = burstPrestartMs > 0 ? Math.ceil(burstPrestartMs / intervalMs) : 0;
session.burstFramesSent = 0;
this.debugLog('udp stream start', session.id,
`frames=${session.mediaFrames?.length || 0}`,
`avgBitRate=${session.avgBitRate || 'unknown'}bps`,
`bodyLen=${bodyLen}`,
`interval=${intervalMs.toFixed(2)}ms`,
`burstFrames=${burstFrameCount}`,
`target=${socket.remoteAddress}:${session.clientUdpPort}`);
const dgram = require('dgram');
session.udpSocket = dgram.createSocket('udp4');
session.udpSocket.on('error', (err) => {
this.debugLog('udp socket error', session.id, err.message);
@@ -678,20 +666,18 @@ class WTVPNM {
session._startDataInterval = () => {
if (session.udpTimer) return;
session.udpTimer = setInterval(() => {
const tick = () => {
session.udpTimer = null;
if (socket.destroyed || !session.udpSocket) {
this.stopUdpStream(session);
return;
}
// Don't re-arm while paused; resumeUdpStream calls _startDataInterval.
if (session.paused) return;
const frames = session.mediaFrames;
if (!frames || session.mediaFrameIdx >= frames.length) {
// End of media: stop sending once all RA frames are out.
this.debugLog('udp stream complete', session.id, `sent=${seq}`);
if (session.udpTimer) {
clearInterval(session.udpTimer);
session.udpTimer = null;
}
// Signal end-of-stream to the client on TCP. wtv2.pcap
// shows the native RealServer sending a single 0x45 byte
// ~0.5s after the last UDP packet; the client then FINs.
@@ -716,7 +702,14 @@ class WTVPNM {
sendPacket(seq, frame);
seq++;
session.mediaFrameIdx++;
}, intervalMs);
session.burstFramesSent++;
// Use half the interval during the pre-start burst window, then
// drop to normal pacing once burstFrameCount frames have been sent.
const delay = session.burstFramesSent < burstFrameCount ? intervalMs / 2 : intervalMs;
session.udpTimer = setTimeout(tick, delay);
};
const initialDelay = session.burstFramesSent < burstFrameCount ? intervalMs / 2 : intervalMs;
session.udpTimer = setTimeout(tick, initialDelay);
};
session.udpStartTimer = setTimeout(() => {
@@ -760,18 +753,12 @@ class WTVPNM {
return out;
}
buildTunnelFrame(session) {
// Send raw PNA media data directly on TCP (no PPP/IP/UDP wrapping).
return this.buildMediaPayload(session);
}
buildMediaPayload(session, pSeq, pFrame) {
const seq = pSeq !== undefined ? pSeq : (session ? session.udpSeq || 0 : 0);
if (session && pSeq === undefined) session.udpSeq = seq + 1;
// Pick the frame: caller can pass one explicitly (interval / burst /
// seek path) or, for the legacy tunnel path, we fall back to indexing
// by seq against mediaFrames as before.
// seek path) or fall back to indexing by seq against mediaFrames.
let frame = pFrame;
if (frame === undefined) {
frame = session?.mediaFrames?.[seq];
@@ -826,83 +813,6 @@ class WTVPNM {
return out;
}
buildIPv4UdpPacket(session, udpPayload) {
const srcIp = this.parseIPv4(this.service_config.stream_src_ip || '10.0.0.2');
const dstIp = this.parseIPv4(this.service_config.stream_dst_ip || '10.0.0.3');
const srcPort = this.service_config.stream_udp_src_port || 0xb385;
const dstPort = this.service_config.stream_udp_dst_port || 6970;
const ipHeader = Buffer.alloc(20);
const udpHeader = Buffer.alloc(8);
const totalLen = 20 + 8 + udpPayload.length;
const ipId = (session.ipId++ & 0xffff);
ipHeader[0] = 0x45;
ipHeader[1] = 0x00;
ipHeader.writeUInt16BE(totalLen, 2);
ipHeader.writeUInt16BE(ipId, 4);
ipHeader.writeUInt16BE(0x4000, 6);
ipHeader[8] = 0x40;
ipHeader[9] = 0x11;
srcIp.copy(ipHeader, 12);
dstIp.copy(ipHeader, 16);
ipHeader.writeUInt16BE(this.ipv4HeaderChecksum(ipHeader), 10);
udpHeader.writeUInt16BE(srcPort & 0xffff, 0);
udpHeader.writeUInt16BE(dstPort & 0xffff, 2);
udpHeader.writeUInt16BE(8 + udpPayload.length, 4);
udpHeader.writeUInt16BE(0, 6);
return Buffer.concat([ipHeader, udpHeader, udpPayload]);
}
buildPppIpFrame(ipPacket) {
const protocol = Buffer.from([0x21]);
const raw = Buffer.concat([protocol, ipPacket]);
return this.pppEscape(raw);
}
pppEscape(buffer) {
const escaped = [];
for (let i = 0; i < buffer.length; i++) {
const b = buffer[i];
if (b === 0x7d || b === 0x7e || b < 0x20) {
escaped.push(0x7d, b ^ 0x20);
} else {
escaped.push(b);
}
}
return Buffer.from(escaped);
}
parseIPv4(ipStr) {
const parts = String(ipStr).split('.').map((v) => parseInt(v, 10));
if (parts.length !== 4 || parts.some((v) => Number.isNaN(v) || v < 0 || v > 255)) {
return Buffer.from([10, 0, 0, 2]);
}
return Buffer.from(parts);
}
ipv4HeaderChecksum(header) {
let sum = 0;
for (let i = 0; i < 20; i += 2) {
if (i === 10) continue;
sum += header.readUInt16BE(i);
while (sum > 0xffff) sum = (sum & 0xffff) + (sum >>> 16);
}
return (~sum) & 0xffff;
}
// The server-challenge wire format has two observed variants:
// - 16-bit: small values like 0x03f1, 0x047d, 0x011e..0x0138. Seen in
// every WebTV capture (wtv.pcap, wtv2.pcap, wtv_multi.pcap). WebTV's
// PNM client REFUSES to send its hash response when the upper 16 bits
// are non-zero.
// - 32-bit Unix timestamp: seen in multi_auth.pcap (a newer RealServer
// build talking to modern RealPlayer).
// Detection is by User-Agent (set in handleData from the raw request data)
// since WebTV clients advertise the same cook/sipr caps as modern RP.
buildPnaHello(session = null) {
// The client advertises its local `time()` value in tag 0 of the
// PNA request, XORed with 0x67E32B93. The hello-parser in
@@ -960,7 +870,6 @@ class WTVPNM {
}
buildDescriptorPacket(session = null) {
const fs = require('fs');
const outChunks = [];
// 4F headers: Rule Tags / Properties (based on capture to appease client parser)
@@ -1120,20 +1029,6 @@ class WTVPNM {
}
}
// Optional codec adaptation
if (tag === 'MDPR' && this.service_config.adapt_codec_from_caps === true) {
const caps = (session && Array.isArray(session.capabilities)) ? session.capabilities : [];
if (caps.includes('cook')) {
// Replace 'slae' codec with 'cook' if requested
const at = chunkData.indexOf(Buffer.from('slae', 'ascii'));
if (at >= 0) {
const newChunk = Buffer.from(chunkData);
Buffer.from('cook', 'ascii').copy(newChunk, at);
chunkData = newChunk;
}
}
}
// Wrap in [0x72] [size_16]
const wrap = Buffer.alloc(3);
wrap[0] = 0x72;
@@ -1156,9 +1051,7 @@ class WTVPNM {
}
// Include the session token as tag 0x23 [size_16 = 64]
const token = (this.service_config.dynamic_session_token === true)
? this.buildSessionToken(session)
: '8e475de1df1ddc5c58c5ecef20e64d26073fe6f98fc0077dd0eb4429e0d8c375';
const token = this.buildSessionToken(session);
const tokenBuf = Buffer.alloc(3 + 64);
tokenBuf[0] = 0x23;

View File

@@ -399,20 +399,9 @@
"flags": "0x00000001",
"allow_double_slash": true,
"protocol_handler": "pnm",
"send_keepalive": false,
"keepalive_zero_ack": true,
"hello_split_ms": 8,
"descriptor_on_first_pna": true,
"descriptor_after_hello_ms": 85,
"descriptor_fallback_ms": 85,
"descriptor_idle_fallback_ms": 85,
"tcp_start_delay_ms": 72,
"tcp_initial_burst": 1,
"tcp_interval_ms": 232,
"dynamic_session_token": true,
"adapt_codec_from_caps": false,
"auto_stream": true,
"debug": true
"burst_prestart_ms": 5000,
"debug": false
}
},
"favorites": {

View File

@@ -44,9 +44,9 @@
}
},
"node_modules/@emnapi/runtime": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
"integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"license": "MIT",
"optional": true,
"dependencies": {
@@ -952,9 +952,9 @@
}
},
"node_modules/adm-zip": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
"integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
"version": "0.5.17",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz",
"integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==",
"license": "MIT",
"engines": {
"node": ">=12.0"
@@ -1051,9 +1051,9 @@
"license": "MIT"
},
"node_modules/basic-ftp": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz",
"integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==",
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.0.tgz",
"integrity": "sha512-5K9eNNn7ywHPsYnFwjKgYH8Hf8B5emh7JKcPaVjjrMJFQQwGpwowEnZNEtHs7DfR7hCZsmaK3VA4HUK0YarT+w==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
@@ -1111,9 +1111,9 @@
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1949,9 +1949,9 @@
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"funding": [
{
"type": "individual",
@@ -2158,9 +2158,9 @@
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
"integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -2602,9 +2602,9 @@
}
},
"node_modules/netmask": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
"integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz",
"integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
@@ -2880,9 +2880,9 @@
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.5.8",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
"funding": [
{
"type": "opencollective",
@@ -3144,9 +3144,9 @@
"license": "MIT"
},
"node_modules/sanitize-html": {
"version": "2.17.2",
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.2.tgz",
"integrity": "sha512-EnffJUl46VE9uvZ0XeWzObHLurClLlT12gsOk1cHyP2Ol1P0BnBnsXmShlBmWVJM+dKieQI68R0tsPY5m/B+Jg==",
"version": "2.17.3",
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.3.tgz",
"integrity": "sha512-Kn4srCAo2+wZyvCNKCSyB2g8RQ8IkX/gQs2uqoSRNu5t9I2qvUyAVvRDiFUVAiX3N3PNuwStY0eNr+ooBHVWEg==",
"license": "MIT",
"dependencies": {
"deepmerge": "^4.2.2",
@@ -3374,13 +3374,13 @@
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
"object-inspect": "^1.13.4"
},
"engines": {
"node": ">= 0.4"