mirror of
https://github.com/ayka-667/SysDVRClient-PS4.git
synced 2026-10-29 04:00:25 +01:00
- Translate the whole app (UI, logs, config comments, tests) to English - Split video into a network thread and a decoder thread joined by a queue; when the queue gets older than max_delay_ms, drop to the next keyframe instead of letting latency build up in the TCP buffer - Skip the deblocking filter automatically while the decoder catches up - Triple buffering so a new frame never waits for the previous vsync - Lower-latency defaults (2 decoder threads, audio batch 0, 60 ms audio buffer) with automatic upgrade of 1.0 config files - Audio: buffer target/headroom based on packet size, underrun counter - New settings: smooth scaling, max video delay; detailed stats overlay - Native test now also checks a decoder too slow for the stream Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
162 lines
5.4 KiB
Python
Executable File
162 lines
5.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Fake Switch that mimics the SysDVR sysmodule in TCP Bridge mode (protocol 02/03).
|
|
|
|
Used to test the PS4 client (or the native harness) without a console.
|
|
Usage: python3 fake_switch.py file.h264 [--proto 02|03] [--no-broadcast]
|
|
"""
|
|
import argparse
|
|
import math
|
|
import socket
|
|
import struct
|
|
import threading
|
|
import time
|
|
|
|
HEADER = struct.Struct("<IIQBB") # Magic, DataSize, Timestamp, MetaData, ReplaySlot
|
|
REQ = struct.Struct("<IHBBBB6s")
|
|
|
|
|
|
def split_access_units(data):
|
|
"""Splits an Annex B H.264 stream into access units (one frame per packet, like grc:d)."""
|
|
starts = []
|
|
i = 0
|
|
while True:
|
|
i = data.find(b"\x00\x00\x01", i)
|
|
if i < 0:
|
|
break
|
|
s = i - 1 if i > 0 and data[i - 1] == 0 else i
|
|
starts.append((s, data[i + 3] & 0x1F))
|
|
i += 3
|
|
aus, cur_start, have_slice = [], None, False
|
|
for idx, (pos, nal) in enumerate(starts):
|
|
if cur_start is None:
|
|
cur_start = pos
|
|
is_slice = nal in (1, 5)
|
|
if is_slice:
|
|
first_mb_zero = data[pos + (4 if data[pos + 2] == 0 else 3) + 1] & 0x80
|
|
if have_slice and first_mb_zero:
|
|
aus.append(data[cur_start:pos])
|
|
cur_start = pos
|
|
have_slice = True
|
|
elif nal in (7, 8, 6, 9) and have_slice:
|
|
aus.append(data[cur_start:pos])
|
|
cur_start, have_slice = pos, False
|
|
aus.append(data[cur_start:])
|
|
return [a for a in aus if a]
|
|
|
|
|
|
def handshake(conn, proto, expect_meta):
|
|
conn.sendall(b"SysDVR|" + proto.encode() + b"\x00")
|
|
req = b""
|
|
while len(req) < 16:
|
|
chunk = conn.recv(16 - len(req))
|
|
if not chunk:
|
|
raise ConnectionError("client left during the handshake")
|
|
req += chunk
|
|
magic, ver, meta, vflags, abatch, feat, _ = REQ.unpack(req)
|
|
ok = magic == 0xAAAAAAAA and ver == (ord(proto[0]) | ord(proto[1]) << 8) and meta == expect_meta
|
|
print(f"[fake] handshake meta={meta} vflags={vflags} abatch={abatch} feat={feat} -> {'OK' if ok else 'REJECTED'}")
|
|
code = 6 if ok else 4
|
|
if proto == "03":
|
|
conn.sendall(struct.pack("<I", code) + struct.pack("<I", 0xFFFFFFFF) + b"\x00" * 64)
|
|
else:
|
|
conn.sendall(struct.pack("<I", code))
|
|
return ok, abatch
|
|
|
|
|
|
def serve_video(port, aus, proto):
|
|
srv = socket.socket()
|
|
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
srv.bind(("0.0.0.0", port))
|
|
srv.listen(1)
|
|
while True:
|
|
conn, addr = srv.accept()
|
|
print(f"[fake] video: client {addr}")
|
|
try:
|
|
ok, _ = handshake(conn, proto, 1)
|
|
if not ok:
|
|
conn.close()
|
|
continue
|
|
t0 = time.time()
|
|
n = 0
|
|
while True:
|
|
au = aus[n % len(aus)]
|
|
conn.sendall(HEADER.pack(0xCCCCCCCC, len(au), int((time.time() - t0) * 1e6), 0x01 | 0x04, 0xFF) + au)
|
|
n += 1
|
|
delay = t0 + n / 30 - time.time()
|
|
if delay > 0:
|
|
time.sleep(delay)
|
|
except (ConnectionError, OSError) as e:
|
|
print(f"[fake] video: disconnected ({e})")
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def serve_audio(port, proto):
|
|
srv = socket.socket()
|
|
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
srv.bind(("0.0.0.0", port))
|
|
srv.listen(1)
|
|
while True:
|
|
conn, addr = srv.accept()
|
|
print(f"[fake] audio: client {addr}")
|
|
try:
|
|
ok, batch = handshake(conn, proto, 2)
|
|
if not ok:
|
|
conn.close()
|
|
continue
|
|
frames = 1024 * (batch + 1)
|
|
t0 = time.time()
|
|
n = 0
|
|
phase = 0
|
|
while True:
|
|
pcm = bytearray()
|
|
for _ in range(frames):
|
|
v = int(8000 * math.sin(phase))
|
|
phase += 2 * math.pi * 440 / 48000
|
|
pcm += struct.pack("<hh", v, v)
|
|
conn.sendall(HEADER.pack(0xCCCCCCCC, len(pcm), int((time.time() - t0) * 1e6), 0x02 | 0x04, 0xFF) + pcm)
|
|
n += frames
|
|
delay = t0 + n / 48000 - time.time()
|
|
if delay > 0:
|
|
time.sleep(delay)
|
|
except (ConnectionError, OSError) as e:
|
|
print(f"[fake] audio: disconnected ({e})")
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def broadcast(proto):
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
|
msg = f"SysDVR|6.3|{proto}|FAKE-SWITCH-0001".encode() + b"\x00" * 8
|
|
while True:
|
|
for dst in ("255.255.255.255", "127.0.0.1"):
|
|
try:
|
|
s.sendto(msg, (dst, 19999))
|
|
except OSError:
|
|
pass
|
|
time.sleep(2)
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("h264")
|
|
ap.add_argument("--proto", default="03", choices=["02", "03"])
|
|
ap.add_argument("--no-broadcast", action="store_true")
|
|
args = ap.parse_args()
|
|
|
|
with open(args.h264, "rb") as f:
|
|
aus = split_access_units(f.read())
|
|
print(f"[fake] {len(aus)} frames loaded, protocol {args.proto}")
|
|
|
|
threading.Thread(target=serve_video, args=(9911, aus, args.proto), daemon=True).start()
|
|
threading.Thread(target=serve_audio, args=(9922, args.proto), daemon=True).start()
|
|
if not args.no_broadcast:
|
|
threading.Thread(target=broadcast, args=(args.proto,), daemon=True).start()
|
|
while True:
|
|
time.sleep(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|