feat: implement WebRTC signaling and UDP-based video streaming and control service

This commit is contained in:
2026-07-16 14:55:14 +04:00
parent 53288ea670
commit 41eaf49bb2
11 changed files with 614 additions and 108 deletions
+100 -1
View File
@@ -35,6 +35,14 @@
tPos: $('tPos'),
tTsm: $('tTsm'),
tCars: $('tCars'),
lblGear: $('lblGear'),
lblDrs: $('lblDrs'),
lblPit: $('lblPit'),
btnDrs: $('btnDrs'),
btnPit: $('btnPit'),
btnGearUp: $('btnGearUp'),
btnGearDown: $('btnGearDown'),
ctrlJson: $('ctrlJson'),
};
// -------- State --------
@@ -53,7 +61,7 @@
snapshotCount: 0,
snapshotWindow: [],
keys: new Set(),
input: { throttle: 0, brake: 0, steer: 0, handbrake: false, buttons: 0 },
input: { throttle: 0, brake: 0, steer: 0, handbrake: false, buttons: 0, gear: 0, drs: false, pitLimiter: false },
pingTimer: null,
inputTimer: null,
slotHint: 0, // сервер выдаёт слот; 0 по умолчанию
@@ -113,6 +121,12 @@
state.input.steer = steer;
state.input.handbrake = handbrake;
// Calculate buttons bitmask: bit 0 (DRS), bit 1 (Pit Limiter)
let buttons = 0;
if (state.input.drs) buttons |= 1;
if (state.input.pitLimiter) buttons |= 2;
state.input.buttons = buttons;
// Update UI bars
ui.lblThrottle.textContent = throttle.toFixed(2);
ui.lblBrake.textContent = brake.toFixed(2);
@@ -129,6 +143,48 @@
ui.barSteer.style.width = '2%';
ui.wheelDisc.style.transform = `rotate(${steer * 90}deg)`;
// Update Gear, DRS, Pit Limiter UI text & class if elements exist
if (ui.lblGear) {
let gName = 'N';
if (state.input.gear === -1) gName = 'R';
if (state.input.gear === 1) gName = 'D';
ui.lblGear.textContent = gName;
}
if (ui.lblDrs) {
ui.lblDrs.textContent = state.input.drs ? 'ON' : 'OFF';
ui.lblDrs.className = state.input.drs ? 'val active' : 'val';
}
if (ui.lblPit) {
ui.lblPit.textContent = state.input.pitLimiter ? 'ON' : 'OFF';
ui.lblPit.className = state.input.pitLimiter ? 'val active' : 'val';
}
// Format control packet as JSON and show in UI
if (ui.ctrlJson) {
const steerByte = Math.round((steer + 1.0) * 100);
const throttleByte = Math.round(throttle * 100);
const brakeByte = Math.round(brake * 100);
const gearByte = state.input.gear === -1 ? 0 : (state.input.gear === 1 ? 2 : 1);
const jsonStr = JSON.stringify({
sent_json: {
steering: steer,
throttle: throttle,
brake: brake,
gear: state.input.gear,
buttons: buttons
},
binary_packet_bytes: {
byte0_steer: `0x${steerByte.toString(16).toUpperCase().padStart(2, '0')} (${steerByte})`,
byte1_throttle: `0x${throttleByte.toString(16).toUpperCase().padStart(2, '0')} (${throttleByte}%)`,
byte2_brake: `0x${brakeByte.toString(16).toUpperCase().padStart(2, '0')} (${brakeByte}%)`,
byte3_gear: `0x${gearByte.toString(16).toUpperCase().padStart(2, '0')} (${gearByte === 0 ? 'R' : (gearByte === 2 ? 'D' : 'N')})`,
byte4_flags: `0x${buttons.toString(16).toUpperCase().padStart(2, '0')} (DRS=${state.input.drs ? 1 : 0}, Pit=${state.input.pitLimiter ? 1 : 0})`
}
}, null, 2);
ui.ctrlJson.textContent = jsonStr;
}
}
// -------- Send helpers --------
@@ -450,6 +506,24 @@
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
if (['arrowup','arrowdown','arrowleft','arrowright',' '].includes(k)) e.preventDefault();
state.keys.add(k);
// Toggle DRS, Pit Limiter, and shift gears
if (k === 'q') {
state.input.drs = !state.input.drs;
pollInput();
}
if (k === 'e') {
state.input.pitLimiter = !state.input.pitLimiter;
pollInput();
}
if (k === 'z') {
state.input.gear = Math.max(-1, state.input.gear - 1);
pollInput();
}
if (k === 'x') {
state.input.gear = Math.min(1, state.input.gear + 1);
pollInput();
}
});
window.addEventListener('keyup', (e) => {
state.keys.delete(keyName(e));
@@ -462,6 +536,31 @@
ui.btnJoin.addEventListener('click', joinRace);
ui.btnLeave.addEventListener('click', leaveRace);
if (ui.btnDrs) {
ui.btnDrs.addEventListener('click', () => {
state.input.drs = !state.input.drs;
pollInput();
});
}
if (ui.btnPit) {
ui.btnPit.addEventListener('click', () => {
state.input.pitLimiter = !state.input.pitLimiter;
pollInput();
});
}
if (ui.btnGearUp) {
ui.btnGearUp.addEventListener('click', () => {
state.input.gear = Math.min(1, state.input.gear + 1);
pollInput();
});
}
if (ui.btnGearDown) {
ui.btnGearDown.addEventListener('click', () => {
state.input.gear = Math.max(-1, state.input.gear - 1);
pollInput();
});
}
// -------- Boot --------
setConn('disconnected', '');
log('x0gp PoC client ready. Press Connect.', 'meta');