1 Commits
Author SHA1 Message Date
ayka-667andClaude Opus 5.5 9252569a82 v1.1: English UI, low-latency video pipeline
- 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>
2026-10-26 13:33:10 +01:00
16 changed files with 668 additions and 317 deletions

No files matched your search

+6 -3
View File
@@ -4,7 +4,7 @@
# Variables: OO_PS4_TOOLCHAIN (OpenOrbis toolchain), FFMPEG (PS4 FFmpeg build, see toolchain/build_ffmpeg.sh)
TITLE := SysDVR PS4
VERSION := 1.00
VERSION := 1.10
TITLE_ID := SDVR00001
CONTENT_ID := IV0000-SDVR00001_00-SYSDVRPS4CLIENT0
@@ -68,9 +68,12 @@ $(BUILD):
TEST_H264 ?= $(BUILD)/test.h264
test:
mkdir -p $(BUILD)
gcc -O1 -g -Wall -Wno-unused-function -Itests/stubs -o $(BUILD)/harness tests/harness.c src/video.c src/audio.c src/sysdvr_proto.c -lavcodec -lswscale -lavutil -lpthread
[ -f $(TEST_H264) ] || ffmpeg -loglevel error -y -f lavfi -i testsrc2=size=1280x720:rate=30 -t 8 -c:v libx264 -profile:v high -bf 0 -g 60 -b:v 5M -f h264 $(TEST_H264)
gcc -O1 -g -Wall -Wno-unused-function -DSYSDVR_TEST -Itests/stubs -o $(BUILD)/harness tests/harness.c src/video.c src/audio.c src/sysdvr_proto.c -lavcodec -lswscale -lavutil -lpthread
[ -f $(TEST_H264) ] || ffmpeg -loglevel error -y -f lavfi -i testsrc2=size=1280x720:rate=30 -t 8 -c:v libx264 -profile:v high -bf 0 -g 30 -b:v 5M -f h264 $(TEST_H264)
@echo "==> Real-time decoder"
python3 tests/fake_switch.py $(TEST_H264) & PID=$$!; sleep 1; cd $(BUILD) && ./harness; RC=$$?; kill $$PID; exit $$RC
@echo "==> Decoder too slow for the stream (must skip frames, not build up latency)"
python3 tests/fake_switch.py $(TEST_H264) & PID=$$!; sleep 1; cd $(BUILD) && SYSDVR_TEST_DECODE_DELAY_MS=45 ./harness; RC=$$?; kill $$PID; exit $$RC
clean:
rm -rf $(BUILD) eboot.bin pkg.gp4 sce_sys/param.sfo $(CONTENT_ID).pkg
+44 -21
View File
@@ -20,16 +20,15 @@ SysDVR-PS4 is a PS4 homebrew port of the [SysDVR](https://github.com/exelix11/Sy
- 📡 **Automatic discovery:** finds the Switch on your network (SysDVR's UDP broadcast), no IP needed
- 🎮 **Manual IP entry** with the controller if your network blocks broadcasts
- 🎞️ **720p ~30 fps H.264**, hardware-agnostic software decoding (FFmpeg, multithreaded, SSE/AVX optimised for the PS4's Jaguar CPU)
- 🎞️ **720p ~30 fps H.264**, software decoding (FFmpeg, multithreaded, SSE/AVX optimised for the PS4's Jaguar CPU)
- ⚡ **Low latency:** the socket is always drained on its own thread, and if decoding falls behind the app skips to the next keyframe instead of piling up delay
- 🖥️ **Fullscreen 1080p** (scaled) or **1:1 720p**
- 🔊 **Audio**, 48 kHz stereo PCM with a low-latency jitter buffer
- 🔁 **Auto-reconnects** if the Wi-Fi drops or the Switch goes to sleep
- 💡 Option to **turn off the Switch's screen** while streaming (SysDVR 6.1+)
- 📊 On-screen stats (fps, bitrate)
- 📊 On-screen stats (fps, bitrate, decoder load, delay, skipped frames, audio buffer)
- ⚙️ Settings saved to `/data/sysdvr-ps4/config.ini`
The UI is in French (the author's language). Pull requests for translations are welcome.
## Requirements
### Switch
@@ -61,7 +60,7 @@ The UI is in French (the author's language). Pull requests for translations are
| Button | Action |
|---|---|
| **OPTIONS** | Open / close the settings menu |
| **△ Triangle** | Show / hide stats (fps, Mbit/s) |
| **△ Triangle** | Show / hide stats (see [Stats overlay](#stats-overlay)) |
| **□ Square** | Toggle fullscreen (1080p) / 1:1 (720p) |
| **✕ Cross** | Reconnect (when not connected) |
| **PS button** | Return to the PS4 home screen |
@@ -77,13 +76,29 @@ The file is created on first launch. Edit it over FTP or change the values from
| Key | Default | Description |
|---|---|---|
| `ip` | `auto` | Switch IP address, or `auto` for discovery |
| `threads` | `3` | H.264 decoder threads (1–6). More threads are smoother but add about 1 frame of latency each. `1` gives the lowest latency. |
| `fast` | `0` | `1` skips the deblocking filter. Much faster decoding, slightly blockier picture. |
| `threads` | `2` | H.264 decoder threads (1–6). Each extra thread decodes faster but adds about 33 ms (one frame) of latency. `1` gives the lowest latency if your PS4 keeps up. |
| `fast` | `0` | `1` skips the deblocking filter. Much faster decoding, slightly blockier picture. It is also switched on automatically while the decoder is catching up. |
| `smooth` | `1` | Bilinear scaling. `0` uses the cheaper fast-bilinear scaler. |
| `max_delay_ms` | `150` | When decoded video falls this far behind, queued frames are dropped and decoding restarts at the next keyframe. |
| `audio` | `1` | Enable sound |
| `fullscreen` | `1` | Scale to 1080p (`0` = native 720p, centred) |
| `screen_off` | `0` | Turn off the Switch screen while streaming (SysDVR 6.1+) |
| `audio_batch` | `2` | SysDVR audio batching (0–3) |
| `audio_buffer_ms` | `80` | Target audio buffer. Raise it if the sound crackles, lower it for less delay. |
| `audio_batch` | `0` | SysDVR audio batching (0–3). Each step adds ~21 ms of audio per packet, so `0` is the lowest latency. |
| `audio_buffer_ms` | `60` | Target audio buffer. Raise it if the sound crackles, lower it for less delay. |
Config files from 1.0 are upgraded automatically: `threads`, `audio_batch` and `audio_buffer_ms` are reset to the new low-latency defaults once.
### Stats overlay
Press **△** while streaming:
| Field | Meaning |
|---|---|
| `fps` / `Mbit/s` | Frames decoded per second and stream bitrate |
| `decode` | Average time the decoder thread spends per frame. Close to 33 ms means the PS4 is at its limit: try `fast` or more `threads`. |
| `delay` | Extra delay of the shown frame compared to the fastest frame of this session (network + queue + decoding) |
| `queue` / `skipped` | Frames waiting to be decoded / frames dropped to catch up. `skipped` going up all the time means the decoder is too slow. |
| `audio` / `underruns` | Audio buffered right now / times the buffer ran dry (crackles). Raise `audio_buffer_ms` if `underruns` keeps going up. |
A log is written to `/data/sysdvr-ps4/log.txt`.
@@ -91,13 +106,14 @@ A log is written to `/data/sysdvr-ps4/log.txt`.
| Problem | Fix |
|---|---|
| Stuck on *"Recherche de la Switch…"* (searching) | Broadcasts may be blocked (guest Wi-Fi, AP isolation, mesh). Enter the Switch IP by hand (System Settings → Internet on the Switch). |
| *"Reponse inattendue… TCP Bridge ?"* | SysDVR is in USB or RTSP mode. Switch it to **TCP Bridge**. |
| *"Version de protocole SysDVR non supportee"* | Update SysDVR on the Switch to 5.5 or later. |
| Stuck on *"Searching for the Switch on the network..."* | Broadcasts may be blocked (guest Wi-Fi, AP isolation, mesh). Enter the Switch IP by hand (System Settings → Internet on the Switch). |
| *"Unexpected reply: is SysDVR in TCP Bridge mode?"* | SysDVR is in USB or RTSP mode. Switch it to **TCP Bridge**. |
| *"Unsupported SysDVR protocol version"* | Update SysDVR on the Switch to 5.5 or later. |
| Connected but black screen | Launch a game. The home menu isn't captured. |
| Choppy video | Use Ethernet on the PS4, keep the Switch close to the router (5 GHz), try `fast = 1`, or set `threads` to 4. |
| Too much latency | `threads = 1` or `2`, lower `audio_buffer_ms`. |
| Crackling sound | Raise `audio_buffer_ms` to 120–200. |
| Video freezes for a moment now and then | The decoder fell behind and skipped to the next keyframe (`skipped` goes up in the stats). Turn on *Fast decoding*, set `threads` to 3, or turn off *Smooth scaling*. |
| Choppy video, `skipped` stays at 0 | Network problem: use Ethernet on the PS4 and keep the Switch close to the router (5 GHz). |
| Too much latency | `threads = 1` (if `decode` stays well under 33 ms), lower `max_delay_ms` and `audio_buffer_ms`. |
| Crackling sound | Raise `audio_buffer_ms` to 100–200. |
## How it works
@@ -112,9 +128,9 @@ On the PS4 side:
| Module | Role |
|---|---|
| `src/sysdvr_proto.c` | Discovery, connect with timeout, handshake (proto 02/03), packet reading and resync |
| `src/video.c` | Network thread → FFmpeg `libavcodec` H.264 decoder (frame threads) → `libswscale` YUV→BGRA scaling (sliced, multithreaded) written **directly** into the framebuffer |
| `src/audio.c` | Network thread → ring buffer with drift/latency control → `sceAudioOut` (256-sample grains) |
| `src/gfx.c` | `sceVideoOut` double-buffered 1920×1080 framebuffer, vsync flip, text rendering with `stb_easy_font` |
| `src/video.c` | Network thread (always drains the socket) → packet queue with catch-up (drops to the next keyframe when it gets older than `max_delay_ms`) → decoder thread, FFmpeg `libavcodec` H.264 (frame threads, deblocking skipped while behind) → `libswscale` YUV→BGRA scaling (sliced, multithreaded) written **directly** into the framebuffer |
| `src/audio.c` | Network thread → ring buffer with latency control → `sceAudioOut` (256-sample grains) |
| `src/gfx.c` | `sceVideoOut` triple-buffered 1920×1080 framebuffer (a new frame never waits for the previous vsync), text rendering with `stb_easy_font` |
| `src/main.c` | UI, DualShock 4 input, settings menu, IP editor, auto-discovery thread |
| `src/util.c` | Logging, config file |
@@ -159,13 +175,20 @@ Output: `IV0000-SDVR00001_00-SYSDVRPS4CLIENT0.pkg`
### Testing
`make test` builds the **real** `video.c`, `audio.c` and `sysdvr_proto.c` natively (PS4 APIs replaced by stubs in `tests/stubs`). It runs them against `tests/fake_switch.py`, a fake Switch that implements the SysDVR sysmodule side (UDP discovery, handshake 02/03, H.264 + PCM streaming):
`make test` builds the **real** `video.c`, `audio.c` and `sysdvr_proto.c` natively (PS4 APIs replaced by stubs in `tests/stubs`). It runs them against `tests/fake_switch.py`, a fake Switch that implements the SysDVR sysmodule side (UDP discovery, handshake 02/03, H.264 + PCM streaming). It runs two scenarios:
1. a normal decoder: nothing may be skipped, and the delay must stay low;
2. a decoder made artificially too slow for the stream (`SYSDVR_TEST_DECODE_DELAY_MS=45`): frames must be skipped, and the delay must stay bounded instead of growing.
```sh
sudo apt install ffmpeg libavcodec-dev libswscale-dev python3
make test
# ... Decouverte OK : 172.x.x.x serie=FAKE-SWITCH-0001
# ... Images decodees=194 rendues=178 ... RESULTAT : OK
# ==> Real-time decoder
# ... frames decoded=255 rendered=239 skipped=0 ... delay avg=34 max=44 ms
# RESULT: OK
# ==> Decoder too slow for the stream (must skip frames, not build up latency)
# ... skipped=124 ... delay avg=123 max=200 ms
# RESULT: OK
```
You can also run `python3 tests/fake_switch.py video.h264` on a PC and point the PS4 app at that PC's IP to test on a real PS4 without a Switch.
+27 -22
View File
@@ -9,17 +9,18 @@
#include <orbis/AudioOut.h>
#include <orbis/UserService.h>
// La Switch envoie du PCM 16 bits stereo 48 kHz
// The Switch sends 16-bit stereo PCM at 48 kHz
#define SAMPLE_RATE 48000
#define BYTES_PER_FRAME 4
#define OUT_GRAIN 256
#define OUT_CHUNK (OUT_GRAIN * BYTES_PER_FRAME)
#define RING_SIZE (SAMPLE_RATE * BYTES_PER_FRAME) // 1 seconde
#define RING_SIZE (SAMPLE_RATE * BYTES_PER_FRAME) // 1 second
static pthread_t g_net_thread, g_out_thread;
static pthread_mutex_t g_ring_lock = PTHREAD_MUTEX_INITIALIZER;
static uint8_t g_ring[RING_SIZE];
static size_t g_ring_read, g_ring_fill;
static bool g_primed;
static volatile int g_sock = -1;
void audio_kick(void)
@@ -34,7 +35,8 @@ static size_t ms_to_bytes(int ms)
return (size_t)SAMPLE_RATE * ms / 1000 * BYTES_PER_FRAME;
}
static void ring_push(const uint8_t *data, size_t len, size_t max_fill)
// target = buffer level to aim for; anything above target + headroom is dropped
static void ring_push(const uint8_t *data, size_t len, size_t target, size_t headroom)
{
pthread_mutex_lock(&g_ring_lock);
if (len > RING_SIZE) {
@@ -53,23 +55,23 @@ static void ring_push(const uint8_t *data, size_t len, size_t max_fill)
g_ring_fill = RING_SIZE;
}
}
// trop de retard accumule : on jette le plus ancien pour garder une latence faible
if (g_ring_fill > max_fill) {
size_t drop = (g_ring_fill - max_fill / 2) & ~(size_t)(BYTES_PER_FRAME - 1);
// too much delay piled up (network burst): drop the oldest samples to get back to the target
if (g_primed && g_ring_fill > target + headroom) {
size_t drop = (g_ring_fill - target) & ~(size_t)(BYTES_PER_FRAME - 1);
g_ring_read = (g_ring_read + drop) % RING_SIZE;
g_ring_fill -= drop;
}
g_app.audio_fill_ms = (uint32_t)(g_ring_fill * 1000 / (SAMPLE_RATE * BYTES_PER_FRAME));
pthread_mutex_unlock(&g_ring_lock);
}
static bool ring_pop(uint8_t *out, size_t len, size_t prime)
{
static bool primed = false;
bool ok = false;
pthread_mutex_lock(&g_ring_lock);
if (!primed && g_ring_fill >= prime)
primed = true;
if (primed && g_ring_fill >= len) {
if (!g_primed && g_ring_fill >= prime)
g_primed = true;
if (g_primed && g_ring_fill >= len) {
for (size_t i = 0; i < len;) {
size_t n = RING_SIZE - g_ring_read;
if (n > len - i) n = len - i;
@@ -79,8 +81,10 @@ static bool ring_pop(uint8_t *out, size_t len, size_t prime)
}
g_ring_fill -= len;
ok = true;
} else if (primed) {
primed = false; // sous-alimentation : on attend de remplir a nouveau
} else if (g_primed) {
// underrun: wait until the buffer is back at the target
g_primed = false;
g_app.audio_underruns++;
}
pthread_mutex_unlock(&g_ring_lock);
return ok;
@@ -90,6 +94,7 @@ static void ring_reset(void)
{
pthread_mutex_lock(&g_ring_lock);
g_ring_read = g_ring_fill = 0;
g_primed = false;
pthread_mutex_unlock(&g_ring_lock);
}
@@ -98,16 +103,15 @@ static void *audio_out_thread(void *arg)
(void)arg;
int handle = sceAudioOutOpen(ORBIS_USER_SERVICE_USER_ID_SYSTEM, ORBIS_AUDIO_OUT_PORT_TYPE_MAIN, 0, OUT_GRAIN, SAMPLE_RATE, ORBIS_AUDIO_OUT_PARAM_FORMAT_S16_STEREO);
if (handle <= 0) {
log_printf("sceAudioOutOpen a echoue : %x", handle);
log_printf("sceAudioOutOpen failed: %x", handle);
return NULL;
}
static uint8_t chunk[OUT_CHUNK] __attribute__((aligned(64)));
while (!g_app.quit) {
int prime_ms = g_app.cfg.audio_buffer_ms / 2;
if (!ring_pop(chunk, OUT_CHUNK, ms_to_bytes(prime_ms)))
if (!ring_pop(chunk, OUT_CHUNK, ms_to_bytes(g_app.cfg.audio_buffer_ms)))
memset(chunk, 0, OUT_CHUNK);
// bloque jusqu'a ce que le materiel soit pret : cadence la lecture
// blocks until the hardware is ready: this paces playback
sceAudioOutOutput(handle, chunk);
}
sceAudioOutOutput(handle, NULL);
@@ -150,7 +154,7 @@ static void *audio_net_thread(void *arg)
fd = -1;
}
if (fd < 0) {
log_printf("Audio : %s", err);
log_printf("Audio: %s", err);
set_state(LINK_ERROR);
for (int i = 0; i < 20 && !g_app.quit && g_app.generation == gen; i++)
sleep_ms(100);
@@ -158,23 +162,24 @@ static void *audio_net_thread(void *arg)
}
g_sock = fd;
log_printf("Audio connecte a %s", ip);
log_printf("Audio connected to %s", ip);
set_state(LINK_STREAMING);
ring_reset();
size_t max_fill = ms_to_bytes(cfg.audio_buffer_ms * 2);
// one Switch packet is 1024 * (1 + batch) samples; allow two of them on top of the target before dropping
size_t packet = ms_to_bytes(1000 * 1024 * (1 + cfg.audio_batch) / SAMPLE_RATE);
volatile bool stop = false;
SdvrPacketHeader hdr;
while (!g_app.quit && g_app.generation == gen && g_app.cfg.audio) {
int rc = sdvr_read_packet(fd, &hdr, buf, &stop);
if (rc < 0) {
log_printf("Audio : connexion perdue");
log_printf("Audio: connection lost");
break;
}
if (rc == 0)
continue;
if ((hdr.MetaData & SDVR_META_AUDIO) && !(hdr.MetaData & SDVR_META_ERROR) && hdr.DataSize > 0)
ring_push(buf, hdr.DataSize & ~(uint32_t)(BYTES_PER_FRAME - 1), max_fill);
ring_push(buf, hdr.DataSize & ~(uint32_t)(BYTES_PER_FRAME - 1), ms_to_bytes(g_app.cfg.audio_buffer_ms), 2 * packet + ms_to_bytes(20));
}
g_sock = -1;
@@ -188,7 +193,7 @@ static void *audio_net_thread(void *arg)
bool audio_start(void)
{
if (sceAudioOutInit() != 0) {
log_printf("sceAudioOutInit a echoue");
log_printf("sceAudioOutInit failed");
return false;
}
if (pthread_create(&g_out_thread, NULL, audio_out_thread, NULL) != 0)
+21 -10
View File
@@ -4,20 +4,25 @@
#include <stddef.h>
#include <pthread.h>
#define APP_VERSION "1.0"
#define APP_VERSION "1.1"
#define DATA_DIR "/data/sysdvr-ps4"
#define CONFIG_PATH DATA_DIR "/config.ini"
#define LOG_PATH DATA_DIR "/log.txt"
// Bumped when the defaults change: older config files get the latency-related keys reset
#define CONFIG_VERSION 2
typedef struct {
char ip[64]; // vide = detection automatique
int threads; // threads du decodeur H.264 (1-6)
bool fast; // saute le deblocking (plus rapide, un peu moins joli)
bool audio; // active le son
bool fullscreen; // 720p etire en 1080p, sinon 1:1 centre
bool screen_off; // eteint l'ecran de la Switch pendant le stream (SysDVR >= 6.1)
char ip[64]; // empty = automatic discovery
int threads; // H.264 decoder threads (1-6)
bool fast; // skip the deblocking filter (faster, slightly blockier)
bool audio; // enable sound
bool fullscreen; // stretch 720p to 1080p, otherwise 1:1 centred
bool screen_off; // turn off the Switch screen while streaming (SysDVR >= 6.1)
bool smooth; // bilinear scaling (otherwise the faster fast-bilinear)
int audio_batch; // 0-3
int audio_buffer_ms;
int max_delay_ms; // video backlog above which frames are dropped to catch up
} Config;
typedef enum {
@@ -32,10 +37,10 @@ typedef struct {
Config cfg;
// IP de la Switch actuellement ciblee (manuelle ou detectee)
// Switch IP currently targeted (manual or discovered)
char target_ip[64];
char serial[64];
// incremente pour forcer les threads reseau a se reconnecter
// incremented to force the network threads to reconnect
volatile int generation;
volatile bool quit;
@@ -43,9 +48,15 @@ typedef struct {
LinkState audio_state;
char message[256];
// statistiques
// statistics
volatile uint64_t video_bytes;
volatile uint32_t video_frames;
volatile uint32_t video_dropped; // packets skipped to catch up
volatile uint32_t video_queue; // packets waiting to be decoded
volatile uint32_t decode_us; // average decode time per frame
volatile int32_t video_delay_ms; // extra delay of the last shown frame over the best seen
volatile uint32_t audio_fill_ms;
volatile uint32_t audio_underruns;
volatile uint64_t last_packet_us;
volatile uint64_t last_frame_us;
} AppShared;
+11 -10
View File
@@ -8,7 +8,8 @@
#include <stb/stb_easy_font.h>
#define NUM_BUFFERS 2
// triple buffering: drawing never waits for the frame currently being flipped
#define NUM_BUFFERS 3
#define FB_SIZE (SCREEN_W * SCREEN_H * 4)
static int g_video = -1;
@@ -21,12 +22,12 @@ bool gfx_init(void)
{
g_video = sceVideoOutOpen(ORBIS_VIDEO_USER_MAIN, ORBIS_VIDEO_OUT_BUS_MAIN, 0, 0);
if (g_video < 0) {
log_printf("sceVideoOutOpen a echoue : %x", g_video);
log_printf("sceVideoOutOpen failed: %x", g_video);
return false;
}
if (sceKernelCreateEqueue(&g_flip_queue, "sysdvr flip queue") < 0) {
log_printf("sceKernelCreateEqueue a echoue");
log_printf("sceKernelCreateEqueue failed");
return false;
}
sceVideoOutAddFlipEvent(g_flip_queue, g_video, 0);
@@ -36,14 +37,14 @@ bool gfx_init(void)
off_t phys = 0;
int rc = sceKernelAllocateDirectMemory(0, sceKernelGetDirectMemorySize(), total, align, 3, &phys);
if (rc < 0) {
log_printf("sceKernelAllocateDirectMemory a echoue : %x", rc);
log_printf("sceKernelAllocateDirectMemory failed: %x", rc);
return false;
}
void *mem = NULL;
rc = sceKernelMapDirectMemory(&mem, total, 0x33, 0, phys, align);
if (rc < 0) {
log_printf("sceKernelMapDirectMemory a echoue : %x", rc);
log_printf("sceKernelMapDirectMemory failed: %x", rc);
return false;
}
@@ -53,11 +54,11 @@ bool gfx_init(void)
}
OrbisVideoOutBufferAttribute attr;
// 0x80000000 = A8R8G8B8 sRGB, mode lineaire
// 0x80000000 = A8R8G8B8 sRGB, linear tiling
sceVideoOutSetBufferAttribute(&attr, 0x80000000, 1, 0, SCREEN_W, SCREEN_H, SCREEN_W);
rc = sceVideoOutRegisterBuffers(g_video, 0, (void **)g_buffers, NUM_BUFFERS, &attr);
if (rc != 0) {
log_printf("sceVideoOutRegisterBuffers a echoue : %x", rc);
log_printf("sceVideoOutRegisterBuffers failed: %x", rc);
return false;
}
@@ -76,11 +77,11 @@ void gfx_flip(void)
g_frame_id++;
sceVideoOutSubmitFlip(g_video, g_current, ORBIS_VIDEO_OUT_FLIP_VSYNC, g_frame_id);
// attendre que ce buffer soit affiche : l'autre devient libre
// the next buffer was last shown NUM_BUFFERS - 1 flips ago: it is free once the previous flip has happened
for (;;) {
OrbisVideoOutFlipStatus st;
sceVideoOutGetFlipStatus(g_video, &st);
if (st.flipArg >= g_frame_id)
if (st.flipArg >= g_frame_id - (NUM_BUFFERS - 2))
break;
OrbisKernelEvent evt;
int count;
@@ -120,7 +121,7 @@ typedef struct {
void gfx_text(uint32_t *fb, int x, int y, int scale, uint32_t color, const char *text)
{
static EasyFontVertex verts[4 * 2000];
// stb_easy_font n'est pas reentrant avec ce buffer statique : uniquement depuis le thread principal
// static vertex buffer: main thread only
int quads = stb_easy_font_print(0, 0, (char *)text, NULL, verts, sizeof(verts));
for (int q = 0; q < quads; q++) {
EasyFontVertex *v = &verts[q * 4];
+4 -4
View File
@@ -5,19 +5,19 @@
#define SCREEN_W 1920
#define SCREEN_H 1080
// Couleurs au format du framebuffer (B8G8R8A8 en memoire, donc 0xAARRGGBB)
// Colours in framebuffer format (B8G8R8A8 in memory, i.e. 0xAARRGGBB)
#define RGB(r, g, b) (0xFF000000u | ((uint32_t)(r) << 16) | ((uint32_t)(g) << 8) | (uint32_t)(b))
bool gfx_init(void);
// Buffer sur lequel dessiner la prochaine image
// Buffer to draw the next frame into
uint32_t *gfx_backbuffer(void);
// Affiche le backbuffer au prochain vsync puis attend que ce soit fait
// Queues the backbuffer for the next vsync and waits until the next buffer is free
void gfx_flip(void);
void gfx_clear(uint32_t *fb, uint32_t color);
void gfx_rect(uint32_t *fb, int x, int y, int w, int h, uint32_t color);
// Dessine du texte (police vectorielle stb_easy_font), scale = taille du "pixel"
// Draws text (stb_easy_font vector font), scale = size of one font "pixel"
void gfx_text(uint32_t *fb, int x, int y, int scale, uint32_t color, const char *text);
int gfx_text_width(const char *text, int scale);
void gfx_text_centered(uint32_t *fb, int y, int scale, uint32_t color, const char *text);
+80 -62
View File
@@ -1,4 +1,4 @@
// SysDVR-PS4 : affiche sur la PS4 l'image et le son d'une Switch qui fait tourner SysDVR (mode TCP Bridge).
// SysDVR-PS4: shows on the PS4 the picture and sound of a Switch running SysDVR (TCP Bridge mode).
#include "common.h"
#include "gfx.h"
#include "video.h"
@@ -22,7 +22,7 @@
#define COL_OK RGB(80, 200, 120)
#define COL_WARN RGB(240, 180, 60)
// ---------------------------------------------------------------- manette
// ---------------------------------------------------------------- controller
static int g_pad = -1;
static uint32_t g_buttons, g_pressed;
@@ -36,14 +36,14 @@ static void pad_init(void)
sceUserServiceInitialize(&param);
if (scePadInit() != 0) {
log_printf("scePadInit a echoue");
log_printf("scePadInit failed");
return;
}
int user = -1;
sceUserServiceGetInitialUser(&user);
g_pad = scePadOpen(user, 0, 0, NULL);
if (g_pad < 0)
log_printf("scePadOpen a echoue : %x", g_pad);
log_printf("scePadOpen failed: %x", g_pad);
}
static void pad_update(void)
@@ -57,7 +57,7 @@ static void pad_update(void)
g_pressed = now & ~g_buttons;
g_buttons = now;
// repetition automatique pour les fleches et L1/R1
// auto-repeat for the D-pad and L1/R1
const uint32_t rep = ORBIS_PAD_BUTTON_UP | ORBIS_PAD_BUTTON_DOWN | ORBIS_PAD_BUTTON_LEFT | ORBIS_PAD_BUTTON_RIGHT |
ORBIS_PAD_BUTTON_L1 | ORBIS_PAD_BUTTON_R1;
uint64_t t = now_us();
@@ -76,7 +76,7 @@ static void pad_update(void)
static bool pressed(uint32_t b) { return (g_pressed & b) != 0; }
// ---------------------------------------------------------------- etat de l'application
// ---------------------------------------------------------------- application state
typedef enum { UI_MAIN, UI_MENU, UI_IPEDIT } UiMode;
@@ -102,12 +102,12 @@ static void apply_target_from_config(void)
if (g_app.cfg.ip[0])
strcpy(g_app.target_ip, g_app.cfg.ip);
else
g_app.target_ip[0] = 0; // la detection automatique va remplir
g_app.target_ip[0] = 0; // filled in by discovery
pthread_mutex_unlock(&g_app.lock);
reconnect();
}
// Detection automatique : la Switch annonce sa presence en UDP sur le port 19999 toutes les 2 s
// Automatic discovery: the Switch announces itself over UDP on port 19999 every 2 s
static void *discovery_thread(void *arg)
{
(void)arg;
@@ -134,7 +134,7 @@ static void *discovery_thread(void *arg)
}
pthread_mutex_unlock(&g_app.lock);
if (changed)
log_printf("Switch detectee : %s (serie %s)", ip, serial);
log_printf("Switch found: %s (serial %s)", ip, serial);
} else {
sleep_ms(100);
}
@@ -145,21 +145,23 @@ static void *discovery_thread(void *arg)
// ---------------------------------------------------------------- menu
enum {
MI_IP, MI_FULLSCREEN, MI_FAST, MI_THREADS, MI_AUDIO, MI_AUDIO_BUF, MI_SCREEN_OFF, MI_RECONNECT, MI_CLOSE, MI_COUNT
MI_IP, MI_FULLSCREEN, MI_SMOOTH, MI_FAST, MI_THREADS, MI_MAX_DELAY, MI_AUDIO, MI_AUDIO_BUF, MI_SCREEN_OFF, MI_RECONNECT, MI_CLOSE, MI_COUNT
};
static void menu_item_text(int i, const Config *c, char *out, size_t len)
{
switch (i) {
case MI_IP: snprintf(out, len, "IP de la Switch : %s", c->ip[0] ? c->ip : "automatique"); break;
case MI_FULLSCREEN: snprintf(out, len, "Plein ecran : %s", c->fullscreen ? "oui" : "non (taille reelle 720p)"); break;
case MI_FAST: snprintf(out, len, "Mode rapide (moins de qualite) : %s", c->fast ? "oui" : "non"); break;
case MI_THREADS: snprintf(out, len, "Threads de decodage : %d", c->threads); break;
case MI_AUDIO: snprintf(out, len, "Son : %s", c->audio ? "oui" : "non"); break;
case MI_AUDIO_BUF: snprintf(out, len, "Tampon audio : %d ms", c->audio_buffer_ms); break;
case MI_SCREEN_OFF: snprintf(out, len, "Eteindre l'ecran de la Switch : %s", c->screen_off ? "oui" : "non"); break;
case MI_RECONNECT: snprintf(out, len, "Reconnecter"); break;
case MI_CLOSE: snprintf(out, len, "Fermer le menu"); break;
case MI_IP: snprintf(out, len, "Switch IP: %s", c->ip[0] ? c->ip : "automatic"); break;
case MI_FULLSCREEN: snprintf(out, len, "Fullscreen: %s", c->fullscreen ? "on" : "off (native 720p)"); break;
case MI_SMOOTH: snprintf(out, len, "Smooth scaling: %s", c->smooth ? "on" : "off (faster)"); break;
case MI_FAST: snprintf(out, len, "Fast decoding (lower quality): %s", c->fast ? "on" : "off"); break;
case MI_THREADS: snprintf(out, len, "Decoder threads: %d%s", c->threads, c->threads == 1 ? " (lowest latency)" : ""); break;
case MI_MAX_DELAY: snprintf(out, len, "Max video delay: %d ms", c->max_delay_ms); break;
case MI_AUDIO: snprintf(out, len, "Sound: %s", c->audio ? "on" : "off"); break;
case MI_AUDIO_BUF: snprintf(out, len, "Audio buffer: %d ms", c->audio_buffer_ms); break;
case MI_SCREEN_OFF: snprintf(out, len, "Turn off Switch screen: %s", c->screen_off ? "on" : "off"); break;
case MI_RECONNECT: snprintf(out, len, "Reconnect"); break;
case MI_CLOSE: snprintf(out, len, "Close menu"); break;
}
}
@@ -217,7 +219,7 @@ static void ui_update(void)
g_ui = UI_MENU;
}
if (pressed(ORBIS_PAD_BUTTON_SQUARE)) {
// retour a la detection automatique
// back to automatic discovery
pthread_mutex_lock(&g_app.lock);
g_app.cfg.ip[0] = 0;
pthread_mutex_unlock(&g_app.lock);
@@ -258,6 +260,12 @@ static void ui_update(void)
}
break;
case MI_FULLSCREEN: c->fullscreen = !c->fullscreen; break;
case MI_SMOOTH: c->smooth = !c->smooth; break;
case MI_MAX_DELAY:
c->max_delay_ms += dir * 50;
if (c->max_delay_ms < 50) c->max_delay_ms = 50;
if (c->max_delay_ms > 1000) c->max_delay_ms = 1000;
break;
case MI_FAST: c->fast = !c->fast; need_reconnect = true; break;
case MI_THREADS:
c->threads += dir;
@@ -287,15 +295,15 @@ static void ui_update(void)
reconnect();
}
// ---------------------------------------------------------------- dessin
// ---------------------------------------------------------------- drawing
static const char *state_name(LinkState s)
{
switch (s) {
case LINK_IDLE: return "inactif";
case LINK_CONNECTING: return "connexion...";
case LINK_STREAMING: return "connecte";
case LINK_ERROR: return "erreur";
case LINK_IDLE: return "idle";
case LINK_CONNECTING: return "connecting...";
case LINK_STREAMING: return "connected";
case LINK_ERROR: return "error";
}
return "?";
}
@@ -311,7 +319,7 @@ static void draw_menu(uint32_t *fb)
int x = (SCREEN_W - w) / 2, y = (SCREEN_H - h) / 2;
gfx_rect(fb, x - 4, y - 4, w + 8, h + 8, COL_ACCENT);
gfx_rect(fb, x, y, w, h, COL_PANEL);
gfx_text(fb, x + 40, y + 30, 5, COL_TEXT, "Parametres");
gfx_text(fb, x + 40, y + 30, 5, COL_TEXT, "Settings");
for (int i = 0; i < MI_COUNT; i++) {
char line[128];
@@ -321,7 +329,7 @@ static void draw_menu(uint32_t *fb)
gfx_rect(fb, x + 20, ly - 10, w - 40, 52, COL_ACCENT);
gfx_text(fb, x + 40, ly, 4, COL_TEXT, line);
}
gfx_text(fb, x + 40, y + h - 55, 3, COL_DIM, "Haut/Bas : choisir Gauche/Droite/X : modifier O : fermer");
gfx_text(fb, x + 40, y + h - 55, 3, COL_DIM, "Up/Down: select Left/Right/X: change O: close");
}
static void draw_ip_editor(uint32_t *fb)
@@ -330,7 +338,7 @@ static void draw_ip_editor(uint32_t *fb)
int x = (SCREEN_W - w) / 2, y = (SCREEN_H - h) / 2;
gfx_rect(fb, x - 4, y - 4, w + 8, h + 8, COL_ACCENT);
gfx_rect(fb, x, y, w, h, COL_PANEL);
gfx_text(fb, x + 40, y + 30, 5, COL_TEXT, "Adresse IP de la Switch");
gfx_text(fb, x + 40, y + 30, 5, COL_TEXT, "Switch IP address");
int bx = x + 90;
for (int i = 0; i < 4; i++) {
@@ -343,8 +351,8 @@ static void draw_ip_editor(uint32_t *fb)
if (i < 3)
gfx_text(fb, ox + 165, y + 150, 10, COL_TEXT, ".");
}
gfx_text(fb, x + 40, y + 290, 3, COL_DIM, "Gauche/Droite : champ Haut/Bas : +-1 L1/R1 : +-10");
gfx_text(fb, x + 40, y + 340, 3, COL_DIM, "X : valider CARRE : detection auto O : annuler");
gfx_text(fb, x + 40, y + 290, 3, COL_DIM, "Left/Right: field Up/Down: +-1 L1/R1: +-10");
gfx_text(fb, x + 40, y + 340, 3, COL_DIM, "X: save SQUARE: automatic discovery O: cancel");
}
static void draw_status_screen(uint32_t *fb)
@@ -364,38 +372,38 @@ static void draw_status_screen(uint32_t *fb)
gfx_clear(fb, COL_BG);
gfx_rect(fb, 0, 0, SCREEN_W, 12, COL_ACCENT);
gfx_text_centered(fb, 110, 9, COL_TEXT, "SysDVR pour PS4");
gfx_text_centered(fb, 230, 3, COL_DIM, "Affiche ta Nintendo Switch sur la tele via ta PS4 - v" APP_VERSION);
gfx_text_centered(fb, 110, 9, COL_TEXT, "SysDVR for PS4");
gfx_text_centered(fb, 230, 3, COL_DIM, "Play your Nintendo Switch on the TV through your PS4 - v" APP_VERSION);
char line[256];
int y = 340;
if (!ip[0]) {
gfx_text_centered(fb, y, 5, COL_WARN, "Recherche de la Switch sur le reseau...");
gfx_text_centered(fb, y, 5, COL_WARN, "Searching for the Switch on the network...");
} else if (vs == LINK_STREAMING) {
snprintf(line, sizeof(line), "Connecte a %s", ip);
snprintf(line, sizeof(line), "Connected to %s", ip);
gfx_text_centered(fb, y, 5, COL_OK, line);
gfx_text_centered(fb, y + 80, 4, COL_WARN, "En attente de l'image : lance un jeu sur la Switch");
gfx_text_centered(fb, y + 80, 4, COL_WARN, "Waiting for video: start a game on the Switch");
} else {
snprintf(line, sizeof(line), "Connexion a %s ...", ip);
snprintf(line, sizeof(line), "Connecting to %s ...", ip);
gfx_text_centered(fb, y, 5, vs == LINK_ERROR ? COL_WARN : COL_TEXT, line);
}
if (msg[0])
gfx_text_centered(fb, y + 150, 3, COL_DIM, msg);
snprintf(line, sizeof(line), "Mode : %s | Video : %s | Son : %s%s%s",
manual ? "IP manuelle" : "detection auto", state_name(vs), audio ? state_name(as) : "desactive",
serial[0] ? " | Switch : " : "", serial);
snprintf(line, sizeof(line), "Mode: %s | Video: %s | Sound: %s%s%s",
manual ? "manual IP" : "auto discovery", state_name(vs), audio ? state_name(as) : "off",
serial[0] ? " | Switch: " : "", serial);
gfx_text_centered(fb, y + 200, 3, COL_DIM, line);
int bx = 300, by = 650;
gfx_rect(fb, bx - 30, by - 30, SCREEN_W - 2 * (bx - 30), 300, COL_PANEL);
gfx_text(fb, bx, by, 4, COL_TEXT, "1. Sur la Switch : ouvre SysDVR Settings et choisis le mode TCP Bridge");
gfx_text(fb, bx, by + 60, 4, COL_TEXT, "2. Connecte la Switch et la PS4 au meme reseau (meme box)");
gfx_text(fb, bx, by + 120, 4, COL_TEXT, "3. Lance un jeu sur la Switch : l'image apparait ici");
gfx_text(fb, bx, by + 180, 3, COL_DIM, "Si la Switch n'est pas trouvee, entre son IP dans OPTIONS > IP de la Switch");
gfx_text(fb, bx, by, 4, COL_TEXT, "1. On the Switch: open SysDVR Settings and pick TCP Bridge mode");
gfx_text(fb, bx, by + 60, 4, COL_TEXT, "2. Connect the Switch and the PS4 to the same network (same router)");
gfx_text(fb, bx, by + 120, 4, COL_TEXT, "3. Start a game on the Switch: the picture shows up here");
gfx_text(fb, bx, by + 180, 3, COL_DIM, "If the Switch is not found, enter its IP in OPTIONS > Switch IP");
gfx_text_centered(fb, 1010, 3, COL_DIM, "OPTIONS : parametres X : reconnecter TRIANGLE : statistiques CARRE : plein ecran");
gfx_text_centered(fb, 1010, 3, COL_DIM, "OPTIONS: settings X: reconnect TRIANGLE: stats SQUARE: fullscreen");
}
static void draw_stats(uint32_t *fb)
@@ -403,20 +411,26 @@ static void draw_stats(uint32_t *fb)
static uint64_t last_t;
static uint32_t last_frames;
static uint64_t last_bytes;
static char text[128] = "";
static char line1[128] = "", line2[128] = "";
uint64_t t = now_us();
if (t - last_t >= 1000000) {
if (t - last_t >= 500000) {
double dt = (t - last_t) / 1e6;
uint32_t f = g_app.video_frames;
uint64_t b = g_app.video_bytes;
snprintf(text, sizeof(text), "%.1f ips %.2f Mbit/s", (f - last_frames) / dt, (b - last_bytes) * 8 / dt / 1e6);
snprintf(line1, sizeof(line1), "%.1f fps %.2f Mbit/s decode %.1f ms delay +%d ms",
(f - last_frames) / dt, (b - last_bytes) * 8 / dt / 1e6, g_app.decode_us / 1000.0,
g_app.video_delay_ms > 0 ? (int)g_app.video_delay_ms : 0);
snprintf(line2, sizeof(line2), "queue %u skipped %u audio %u ms underruns %u",
g_app.video_queue, g_app.video_dropped, g_app.audio_fill_ms, g_app.audio_underruns);
last_t = t;
last_frames = f;
last_bytes = b;
}
gfx_rect(fb, 20, 20, gfx_text_width(text, 3) + 30, 50, RGB(0, 0, 0));
gfx_text(fb, 35, 32, 3, COL_OK, text);
int w1 = gfx_text_width(line1, 3), w2 = gfx_text_width(line2, 3);
gfx_rect(fb, 20, 20, (w1 > w2 ? w1 : w2) + 30, 90, RGB(0, 0, 0));
gfx_text(fb, 35, 32, 3, COL_OK, line1);
gfx_text(fb, 35, 72, 3, COL_DIM, line2);
}
static void draw_overlays(uint32_t *fb)
@@ -424,7 +438,7 @@ static void draw_overlays(uint32_t *fb)
if (g_show_stats)
draw_stats(fb);
if (now_us() < g_hint_until) {
const char *hint = "OPTIONS : menu TRIANGLE : stats CARRE : plein ecran";
const char *hint = "OPTIONS: menu TRIANGLE: stats SQUARE: fullscreen";
int w = gfx_text_width(hint, 3) + 40;
gfx_rect(fb, (SCREEN_W - w) / 2, SCREEN_H - 90, w, 50, RGB(0, 0, 0));
gfx_text_centered(fb, SCREEN_H - 78, 3, COL_TEXT, hint);
@@ -451,7 +465,7 @@ int main(void)
sceSystemServiceHideSplashScreen();
if (!gfx_init()) {
log_printf("Initialisation graphique impossible");
log_printf("Graphics initialisation failed");
for (;;)
sleep_ms(1000);
}
@@ -464,20 +478,21 @@ int main(void)
pthread_create(&disc, NULL, discovery_thread, NULL);
if (!video_start())
app_set_message("Impossible de demarrer le thread video");
app_set_message("Could not start the video threads");
if (!audio_start())
log_printf("Audio indisponible");
log_printf("Audio unavailable");
uint64_t last_tick = 0, last_ui_draw = 0;
uint64_t last_tick = 0, last_flip = 0;
bool was_showing_video = false;
for (;;) {
pad_update();
bool input = g_pressed != 0;
ui_update();
uint64_t t = now_us();
if (t - last_tick > 5000000) {
// empeche la PS4 de passer en veille / d'assombrir l'ecran
// keeps the PS4 from dimming the screen / going to sleep
sceSystemServicePowerTick();
last_tick = t;
}
@@ -485,7 +500,7 @@ int main(void)
pthread_mutex_lock(&g_app.lock);
bool streaming = g_app.video_state == LINK_STREAMING;
bool fullscreen = g_app.cfg.fullscreen;
bool fast = g_app.cfg.fast;
bool smooth = g_app.cfg.smooth;
pthread_mutex_unlock(&g_app.lock);
bool showing_video = streaming && g_app.last_frame_us && (t - g_app.last_frame_us) < 3000000;
@@ -495,19 +510,22 @@ int main(void)
uint32_t *fb = gfx_backbuffer();
if (showing_video) {
if (video_render(fb, fullscreen, fast)) {
// show every new frame as soon as it is decoded; redraw the last one when the menu changes
bool force = input || t - last_flip > 100000;
if ((force || video_has_new_frame()) && video_render(fb, fullscreen, smooth, force)) {
draw_overlays(fb);
gfx_flip();
last_flip = t;
} else {
sleep_ms(2);
sleep_ms(1);
}
} else if (t - last_ui_draw > 50000) {
// video_render consomme aussi les images residuelles
video_render(fb, fullscreen, fast);
} else if (input || t - last_flip > 50000) {
// also drains leftover frames
video_render(fb, fullscreen, smooth, false);
draw_status_screen(fb);
draw_overlays(fb);
gfx_flip();
last_ui_draw = t;
last_flip = t;
} else {
sleep_ms(5);
}
+39 -29
View File
@@ -39,13 +39,13 @@ int sdvr_connect(const char *ip, int port, int timeout_ms, char *err, size_t err
addr.sin_family = AF_INET;
addr.sin_port = htons((uint16_t)port);
if (inet_pton(AF_INET, ip, &addr.sin_addr) != 1) {
snprintf(err, errlen, "Adresse IP invalide : %s", ip);
snprintf(err, errlen, "Invalid IP address: %s", ip);
return -1;
}
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
snprintf(err, errlen, "socket() a echoue : %s", strerror(errno));
snprintf(err, errlen, "socket() failed: %s", strerror(errno));
return -1;
}
@@ -54,7 +54,7 @@ int sdvr_connect(const char *ip, int port, int timeout_ms, char *err, size_t err
int rcvbuf = 2 * 1024 * 1024;
setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
// connect non bloquant pour pouvoir appliquer un timeout
// non-blocking connect so a timeout can be applied
int flags = fcntl(fd, F_GETFL, 0);
bool nonblock = flags >= 0 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
@@ -66,7 +66,7 @@ int sdvr_connect(const char *ip, int port, int timeout_ms, char *err, size_t err
struct timeval tv = { timeout_ms / 1000, (timeout_ms % 1000) * 1000 };
rc = select(fd + 1, NULL, &wfds, NULL, &tv);
if (rc <= 0) {
snprintf(err, errlen, "Pas de reponse de %s:%d", ip, port);
snprintf(err, errlen, "No answer from %s:%d", ip, port);
close(fd);
return -1;
}
@@ -74,7 +74,7 @@ int sdvr_connect(const char *ip, int port, int timeout_ms, char *err, size_t err
socklen_t len = sizeof(soerr);
getsockopt(fd, SOL_SOCKET, SO_ERROR, &soerr, &len);
if (soerr != 0) {
snprintf(err, errlen, "Connexion a %s:%d refusee (%s)", ip, port, strerror(soerr));
snprintf(err, errlen, "Connection to %s:%d refused (%s)", ip, port, strerror(soerr));
close(fd);
return -1;
}
@@ -82,7 +82,7 @@ int sdvr_connect(const char *ip, int port, int timeout_ms, char *err, size_t err
}
if (rc != 0) {
snprintf(err, errlen, "Connexion a %s:%d impossible (%s)", ip, port, strerror(errno));
snprintf(err, errlen, "Cannot connect to %s:%d (%s)", ip, port, strerror(errno));
close(fd);
return -1;
}
@@ -95,7 +95,7 @@ int sdvr_connect(const char *ip, int port, int timeout_ms, char *err, size_t err
return fd;
}
// Lit exactement len octets. Retourne 1 ok, 0 timeout sans aucun octet lu (si allow_idle), -1 erreur.
// Reads exactly len bytes. Returns 1 on success, 0 on timeout with nothing read (if allow_idle), -1 on error.
static int read_full(int fd, void *dst, size_t len, volatile bool *stop, bool allow_idle, int max_idle_s)
{
uint8_t *p = (uint8_t *)dst;
@@ -113,7 +113,7 @@ static int read_full(int fd, void *dst, size_t len, volatile bool *stop, bool al
continue;
}
if (n == 0)
return -1; // connexion fermee
return -1; // connection closed
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR || errno == ETIMEDOUT) {
if (got == 0 && allow_idle)
@@ -131,12 +131,12 @@ int sdvr_handshake(int fd, const SdvrHandshakeOptions *opt, char *err, size_t er
{
char hello[10];
if (read_full(fd, hello, sizeof(hello), NULL, false, 5) != 1) {
snprintf(err, errlen, "La Switch n'a pas envoye le message d'accueil SysDVR");
snprintf(err, errlen, "The Switch did not send the SysDVR hello");
return -1;
}
if (memcmp(hello, "SysDVR|", 7) != 0 || hello[9] != 0) {
snprintf(err, errlen, "Reponse inattendue : ce n'est pas SysDVR en mode TCP Bridge ?");
snprintf(err, errlen, "Unexpected reply: is SysDVR in TCP Bridge mode?");
return -1;
}
@@ -144,7 +144,7 @@ int sdvr_handshake(int fd, const SdvrHandshakeOptions *opt, char *err, size_t er
bool v2 = v0 == '0' && v1 == '2';
bool v3 = v0 == '0' && v1 == '3';
if (!v2 && !v3) {
snprintf(err, errlen, "Version de protocole SysDVR non supportee (%c%c). Mets a jour SysDVR.", v0, v1);
snprintf(err, errlen, "Unsupported SysDVR protocol version (%c%c). Update SysDVR.", v0, v1);
return -1;
}
@@ -153,20 +153,20 @@ int sdvr_handshake(int fd, const SdvrHandshakeOptions *opt, char *err, size_t er
req.Magic = SDVR_REQUEST_MAGIC;
req.ProtoVer = (uint16_t)((uint8_t)v0 | ((uint8_t)v1 << 8));
req.MetaFlags = opt->video ? 1 : 2;
req.VideoFlags = 2; // InjectPPSSPS, pas de replay de NAL
req.VideoFlags = 2; // InjectPPSSPS, no NAL replay
req.AudioBatching = (uint8_t)opt->audio_batching;
if (v3 && opt->turn_off_screen)
req.FeatureFlags |= 1;
if (send(fd, &req, sizeof(req), 0) != (ssize_t)sizeof(req)) {
snprintf(err, errlen, "Envoi du handshake impossible");
snprintf(err, errlen, "Could not send the handshake");
return -1;
}
uint8_t resp[72];
size_t resp_len = v3 ? 72 : 4;
if (read_full(fd, resp, resp_len, NULL, false, 5) != 1) {
snprintf(err, errlen, "Pas de reponse au handshake");
snprintf(err, errlen, "No handshake response");
return -1;
}
@@ -174,21 +174,21 @@ int sdvr_handshake(int fd, const SdvrHandshakeOptions *opt, char *err, size_t er
memcpy(&code, resp, 4);
if (code != SDVR_HANDSHAKE_OK) {
if (code == 1)
snprintf(err, errlen, "Version refusee par la Switch : mets a jour SysDVR");
snprintf(err, errlen, "Version rejected by the Switch: update SysDVR");
else
snprintf(err, errlen, "Handshake refuse par la Switch (code %u)", code);
snprintf(err, errlen, "Handshake rejected by the Switch (code %u)", code);
return -1;
}
return 0;
}
int sdvr_read_packet(int fd, SdvrPacketHeader *hdr, uint8_t *buf, volatile bool *stop)
int sdvr_read_header(int fd, SdvrPacketHeader *hdr, volatile bool *stop)
{
int rc = read_full(fd, hdr, sizeof(*hdr), stop, true, 10);
if (rc != 1)
return rc;
// resynchronisation si le header est corrompu : chercher CC CC CC CC
// resync if the header is corrupted: look for CC CC CC CC
int resync_budget = 4 * 1024 * 1024;
while (hdr->Magic != SDVR_PACKET_MAGIC || hdr->DataSize > SDVR_MAX_PAYLOAD) {
if (--resync_budget <= 0)
@@ -198,12 +198,22 @@ int sdvr_read_packet(int fd, SdvrPacketHeader *hdr, uint8_t *buf, volatile bool
if (rc != 1)
return -1;
}
return 1;
}
if (hdr->DataSize == 0)
int sdvr_read_payload(int fd, uint8_t *buf, uint32_t size, volatile bool *stop)
{
if (size == 0)
return 1;
return read_full(fd, buf, size, stop, false, 10) == 1 ? 1 : -1;
}
rc = read_full(fd, buf, hdr->DataSize, stop, false, 10);
return rc == 1 ? 1 : -1;
int sdvr_read_packet(int fd, SdvrPacketHeader *hdr, uint8_t *buf, volatile bool *stop)
{
int rc = sdvr_read_header(fd, hdr, stop);
if (rc != 1)
return rc;
return sdvr_read_payload(fd, buf, hdr->DataSize, stop);
}
int sdvr_discover(int timeout_ms, char *ip, size_t iplen, char *serial, size_t seriallen)
@@ -237,8 +247,8 @@ int sdvr_discover(int timeout_ms, char *ip, size_t iplen, char *serial, size_t s
socklen_t fromlen = sizeof(from);
ssize_t n = recvfrom(fd, pkt, sizeof(pkt) - 1, 0, (struct sockaddr *)&from, &fromlen);
if (n > 0) {
pkt[n] = 0; // la chaine s'arrete au premier NUL
// format : SysDVR|<version>|<proto>|<serial>
pkt[n] = 0; // the string ends at the first NUL
// format: SysDVR|<version>|<proto>|<serial>
if (strncmp(pkt, "SysDVR|", 7) == 0) {
char *ver = pkt + 7;
char *proto = strchr(ver, '|');
@@ -261,7 +271,7 @@ int sdvr_discover(int timeout_ms, char *ip, size_t iplen, char *serial, size_t s
void sdvr_format_error(const uint8_t *data, uint32_t size, char *out, size_t outlen)
{
if (size < 8) {
snprintf(out, outlen, "Erreur inconnue de la console");
snprintf(out, outlen, "Unknown console error");
return;
}
uint32_t type, code;
@@ -272,12 +282,12 @@ void sdvr_format_error(const uint8_t *data, uint32_t size, char *out, size_t out
memcpy(&ctx1, data + 8, 8);
switch (type) {
case 1: snprintf(out, outlen, "Capture video echouee (0x%x)", code); break;
case 1: snprintf(out, outlen, "Video capture failed (0x%x)", code); break;
case 2:
case 3: snprintf(out, outlen, "Capture audio echouee (0x%x)", code); break;
case 4: snprintf(out, outlen, "grc:d (video) a echoue (0x%x)", code); break;
case 5: snprintf(out, outlen, "grc:d (audio) a echoue (0x%x)", code); break;
default: snprintf(out, outlen, "Erreur console 0x%x / 0x%x", type, code); break;
case 3: snprintf(out, outlen, "Audio capture failed (0x%x)", code); break;
case 4: snprintf(out, outlen, "grc:d (video) failed (0x%x)", code); break;
case 5: snprintf(out, outlen, "grc:d (audio) failed (0x%x)", code); break;
default: snprintf(out, outlen, "Console error 0x%x / 0x%x", type, code); break;
}
(void)ctx1;
}
+19 -13
View File
@@ -1,5 +1,5 @@
#pragma once
// Implementation client du protocole SysDVR "TCP Bridge" (protocole 02 / 03).
// Client implementation of the SysDVR "TCP Bridge" protocol (protocol 02 / 03).
// Reference : https://github.com/exelix11/SysDVR/blob/master/protocol.md
#include <stdint.h>
#include <stdbool.h>
@@ -13,7 +13,7 @@
#define SDVR_REQUEST_MAGIC 0xAAAAAAAAu
#define SDVR_HANDSHAKE_OK 6
// Taille max du contenu d'un paquet : max(VbufSz, AbufSz * (1 + MaxABatching))
// Max packet payload: max(VbufSz, AbufSz * (1 + MaxABatching))
#define SDVR_VBUF_SIZE 0x54000
#define SDVR_ABUF_SIZE 0x1000
#define SDVR_MAX_PAYLOAD SDVR_VBUF_SIZE
@@ -31,18 +31,18 @@ enum {
typedef struct {
uint32_t Magic;
uint32_t DataSize;
uint64_t Timestamp; // microsecondes
uint64_t Timestamp; // microseconds
uint8_t MetaData;
uint8_t ReplaySlot;
} SdvrPacketHeader;
typedef struct {
uint32_t Magic;
uint16_t ProtoVer; // deux caracteres ASCII, ex: '0','2'
uint16_t ProtoVer; // two ASCII characters, e.g. '0','2'
uint8_t MetaFlags; // 1 = video, 2 = audio
uint8_t VideoFlags; // 1 = NalHash, 2 = InjectPPSSPS, 4 = NalHashOnlyIDR
uint8_t AudioBatching;
uint8_t FeatureFlags; // 1 = eteindre l'ecran (proto 03)
uint8_t FeatureFlags; // 1 = turn off the screen (proto 03)
uint8_t Reserved[6];
} SdvrHandshakeRequest;
#pragma pack(pop)
@@ -56,25 +56,31 @@ typedef struct {
bool turn_off_screen;
} SdvrHandshakeOptions;
// SPS/PPS par defaut du flux de la Switch (copie du client officiel)
// Default SPS/PPS of the Switch stream (same as the official client)
extern const uint8_t SDVR_SPS[20];
extern const uint8_t SDVR_PPS[8];
// Connexion TCP avec timeout. Retourne le fd ou -1.
// TCP connect with timeout. Returns the fd or -1.
int sdvr_connect(const char *ip, int port, int timeout_ms, char *err, size_t errlen);
// Effectue le handshake. Retourne 0 si OK.
// Performs the handshake. Returns 0 on success.
int sdvr_handshake(int fd, const SdvrHandshakeOptions *opt, char *err, size_t errlen);
// Lit le prochain paquet. buf doit faire au moins SDVR_MAX_PAYLOAD octets.
// Retourne 1 si un paquet a ete lu, 0 si timeout (aucune donnee), -1 en cas d'erreur/deconnexion.
// *stop est verifie a chaque timeout pour permettre d'interrompre.
// Reads the next packet header (resyncs on garbage).
// Returns 1 if a header was read, 0 on timeout (no data), -1 on error/disconnect.
// *stop is checked on every timeout so the read can be interrupted.
int sdvr_read_header(int fd, SdvrPacketHeader *hdr, volatile bool *stop);
// Reads exactly size payload bytes. Returns 1 on success, -1 on error.
int sdvr_read_payload(int fd, uint8_t *buf, uint32_t size, volatile bool *stop);
// Header + payload. buf must hold at least SDVR_MAX_PAYLOAD bytes. Same return values as sdvr_read_header.
int sdvr_read_packet(int fd, SdvrPacketHeader *hdr, uint8_t *buf, volatile bool *stop);
// Ecoute les annonces UDP de la Switch. Retourne 1 si trouvee (ip/serial remplis), 0 sinon.
// Listens for the Switch's UDP announcements. Returns 1 if found (ip/serial filled in), 0 otherwise.
int sdvr_discover(int timeout_ms, char *ip, size_t iplen, char *serial, size_t seriallen);
// Formate un paquet d'erreur envoye par la console
// Formats an error packet sent by the console
void sdvr_format_error(const uint8_t *data, uint32_t size, char *out, size_t outlen);
void sdvr_close(int fd);
+36 -12
View File
@@ -30,7 +30,7 @@ void log_init(void)
{
mkdir(DATA_DIR, 0777);
g_log = fopen(LOG_PATH, "w");
log_printf("SysDVR-PS4 " APP_VERSION " demarre");
log_printf("SysDVR-PS4 " APP_VERSION " started");
}
void log_printf(const char *fmt, ...)
@@ -72,18 +72,20 @@ void app_set_message(const char *fmt, ...)
void config_defaults(Config *c)
{
memset(c, 0, sizeof(*c));
c->threads = 3;
c->threads = 2;
c->fast = false;
c->audio = true;
c->fullscreen = true;
c->screen_off = false;
c->audio_batch = 2;
c->audio_buffer_ms = 80;
c->smooth = true;
c->audio_batch = 0;
c->audio_buffer_ms = 60;
c->max_delay_ms = 150;
}
static bool parse_bool(const char *v)
{
return v[0] == '1' || v[0] == 'y' || v[0] == 'Y' || v[0] == 't' || v[0] == 'T' || v[0] == 'o' || v[0] == 'O';
return v[0] == '1' || v[0] == 'y' || v[0] == 'Y' || v[0] == 't' || v[0] == 'T';
}
static int clampi(int v, int lo, int hi)
@@ -101,6 +103,7 @@ void config_load(Config *c)
return;
}
int version = 1;
char line[256];
while (fgets(line, sizeof(line), f)) {
char *p = line;
@@ -115,7 +118,7 @@ void config_load(Config *c)
char *key = p;
char *val = eq + 1;
// retire espaces et fins de ligne
// trim spaces and line endings
for (char *e = key + strlen(key); e > key && (e[-1] == ' ' || e[-1] == '\t'); e--) e[-1] = 0;
while (*val == ' ' || *val == '\t') val++;
for (char *e = val + strlen(val); e > val && (e[-1] == '\n' || e[-1] == '\r' || e[-1] == ' '); e--) e[-1] = 0;
@@ -124,15 +127,29 @@ void config_load(Config *c)
strncpy(c->ip, val, sizeof(c->ip) - 1);
if (!strcmp(c->ip, "auto")) c->ip[0] = 0;
}
else if (!strcmp(key, "config_version")) version = atoi(val);
else if (!strcmp(key, "threads")) c->threads = clampi(atoi(val), 1, 6);
else if (!strcmp(key, "fast")) c->fast = parse_bool(val);
else if (!strcmp(key, "audio")) c->audio = parse_bool(val);
else if (!strcmp(key, "fullscreen")) c->fullscreen = parse_bool(val);
else if (!strcmp(key, "smooth")) c->smooth = parse_bool(val);
else if (!strcmp(key, "screen_off")) c->screen_off = parse_bool(val);
else if (!strcmp(key, "audio_batch")) c->audio_batch = clampi(atoi(val), 0, 3);
else if (!strcmp(key, "audio_buffer_ms")) c->audio_buffer_ms = clampi(atoi(val), 20, 500);
else if (!strcmp(key, "max_delay_ms")) c->max_delay_ms = clampi(atoi(val), 50, 1000);
}
fclose(f);
if (version < CONFIG_VERSION) {
// the 1.0 defaults added latency: move them to the new low-latency values
Config d;
config_defaults(&d);
c->threads = d.threads;
c->audio_batch = d.audio_batch;
c->audio_buffer_ms = d.audio_buffer_ms;
log_printf("Config upgraded from version %d", version);
config_save(c);
}
}
void config_save(const Config *c)
@@ -140,21 +157,28 @@ void config_save(const Config *c)
mkdir(DATA_DIR, 0777);
FILE *f = fopen(CONFIG_PATH, "w");
if (!f) {
log_printf("Impossible d'ecrire %s", CONFIG_PATH);
log_printf("Cannot write %s", CONFIG_PATH);
return;
}
fprintf(f, "# Configuration SysDVR-PS4\n");
fprintf(f, "# ip = adresse IP de la Switch, ou auto pour la detection automatique\n");
fprintf(f, "# SysDVR-PS4 configuration\n");
fprintf(f, "config_version = %d\n", CONFIG_VERSION);
fprintf(f, "# ip = Switch IP address, or auto for automatic discovery\n");
fprintf(f, "ip = %s\n", c->ip[0] ? c->ip : "auto");
fprintf(f, "# threads du decodeur video (1-6). Plus = plus fluide mais un peu plus de latence\n");
fprintf(f, "# video decoder threads (1-6). Each extra thread decodes faster but adds ~33 ms of latency\n");
fprintf(f, "threads = %d\n", c->threads);
fprintf(f, "# fast = 1 desactive le filtre de deblocage (plus rapide)\n");
fprintf(f, "# fast = 1 disables the deblocking filter (faster decoding, slightly blockier)\n");
fprintf(f, "fast = %d\n", c->fast);
fprintf(f, "audio = %d\n", c->audio);
fprintf(f, "fullscreen = %d\n", c->fullscreen);
fprintf(f, "# eteint l'ecran de la Switch pendant le stream (necessite SysDVR 6.1+)\n");
fprintf(f, "# smooth = 0 uses a cheaper scaler\n");
fprintf(f, "smooth = %d\n", c->smooth);
fprintf(f, "# turn off the Switch screen while streaming (needs SysDVR 6.1+)\n");
fprintf(f, "screen_off = %d\n", c->screen_off);
fprintf(f, "# audio packets batched by the Switch (0-3). 0 = lowest latency\n");
fprintf(f, "audio_batch = %d\n", c->audio_batch);
fprintf(f, "# target audio buffer. Raise it if the sound crackles\n");
fprintf(f, "audio_buffer_ms = %d\n", c->audio_buffer_ms);
fprintf(f, "# when the video falls this far behind, frames are skipped to catch up\n");
fprintf(f, "max_delay_ms = %d\n", c->max_delay_ms);
fclose(f);
}
+310 -95
View File
@@ -1,3 +1,6 @@
// Video pipeline: network thread -> packet queue -> decoder thread -> latest frame -> main thread (scale + display).
// The network thread always drains the socket so a backlog never builds up inside TCP: when the queue gets too old
// it is thrown away and decoding resumes at the next keyframe, which keeps the latency bounded.
#include "video.h"
#include "common.h"
#include "gfx.h"
@@ -12,13 +15,42 @@
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
static pthread_t g_thread;
static pthread_mutex_t g_frame_lock = PTHREAD_MUTEX_INITIALIZER;
static AVFrame *g_shared_frame; // derniere image decodee, protegee par g_frame_lock
static bool g_has_new_frame;
#define QUEUE_CAP 64
#define KEYFRAME_WAIT_US 2000000 // give up waiting for a keyframe after this long
#ifdef SYSDVR_TEST
extern int g_test_decode_delay_ms;
#endif
typedef struct {
AVPacket *pkt;
uint64_t recv_us;
int session;
bool flush; // decoder must be flushed before this packet (frames were dropped)
} QueueItem;
static pthread_t g_net_thread, g_dec_thread;
static volatile int g_sock = -1;
// packet queue, protected by g_queue_lock
static pthread_mutex_t g_queue_lock = PTHREAD_MUTEX_INITIALIZER;
static QueueItem g_queue[QUEUE_CAP];
static int g_queue_head, g_queue_count;
static int g_session;
static bool g_wait_key;
static uint64_t g_wait_key_since;
// best (smallest) local arrival time minus Switch timestamp seen this session
static volatile int64_t g_clock_offset;
// latest decoded frame, protected by g_frame_lock
static pthread_mutex_t g_frame_lock = PTHREAD_MUTEX_INITIALIZER;
static AVFrame *g_shared_frame;
static bool g_has_new_frame;
// main thread only
static AVFrame *g_render_frame;
static bool g_render_valid;
static struct SwsContext *g_sws;
static int g_sws_src_w, g_sws_src_h, g_sws_src_fmt, g_sws_dst_w, g_sws_dst_h, g_sws_flags;
@@ -29,63 +61,106 @@ void video_kick(void)
shutdown(fd, SHUT_RDWR);
}
static AVCodecContext *open_decoder(const Config *cfg)
// ---------------------------------------------------------------- queue
static void queue_clear_locked(void)
{
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
log_printf("Decodeur H.264 introuvable");
return NULL;
while (g_queue_count > 0) {
av_packet_free(&g_queue[g_queue_head].pkt);
g_queue_head = (g_queue_head + 1) % QUEUE_CAP;
g_queue_count--;
}
g_app.video_queue = 0;
}
static void queue_new_session(void)
{
pthread_mutex_lock(&g_queue_lock);
queue_clear_locked();
g_session++;
g_wait_key = false;
g_clock_offset = INT64_MAX;
pthread_mutex_unlock(&g_queue_lock);
}
// Takes ownership of pkt
static void queue_push(AVPacket *pkt, bool key, int max_delay_ms)
{
uint64_t t = now_us();
bool flush = false;
pthread_mutex_lock(&g_queue_lock);
// too far behind: drop everything queued and restart at the next keyframe
if (g_queue_count > 0) {
uint64_t age = t - g_queue[g_queue_head].recv_us;
if (g_queue_count >= QUEUE_CAP || age > (uint64_t)max_delay_ms * 1000) {
g_app.video_dropped += g_queue_count;
log_printf("Video %d ms behind (%d packets queued): skipping to the next keyframe", (int)(age / 1000), g_queue_count);
queue_clear_locked();
if (!g_wait_key) {
g_wait_key = true;
g_wait_key_since = t;
}
}
}
AVCodecContext *ctx = avcodec_alloc_context3(codec);
if (!ctx)
return NULL;
// SPS/PPS de la Switch fournis d'avance comme le client officiel
ctx->extradata_size = sizeof(SDVR_SPS) + sizeof(SDVR_PPS);
ctx->extradata = av_mallocz(ctx->extradata_size + AV_INPUT_BUFFER_PADDING_SIZE);
memcpy(ctx->extradata, SDVR_SPS, sizeof(SDVR_SPS));
memcpy(ctx->extradata + sizeof(SDVR_SPS), SDVR_PPS, sizeof(SDVR_PPS));
ctx->thread_count = cfg->threads;
ctx->thread_type = FF_THREAD_FRAME | FF_THREAD_SLICE;
if (cfg->threads <= 1)
ctx->flags |= AV_CODEC_FLAG_LOW_DELAY;
ctx->flags2 |= AV_CODEC_FLAG2_FAST;
if (cfg->fast)
ctx->skip_loop_filter = AVDISCARD_ALL;
int rc = avcodec_open2(ctx, codec, NULL);
if (rc < 0) {
log_printf("avcodec_open2 a echoue : %d", rc);
avcodec_free_context(&ctx);
return NULL;
if (g_wait_key) {
if (!key && t - g_wait_key_since < KEYFRAME_WAIT_US) {
g_app.video_dropped++;
pthread_mutex_unlock(&g_queue_lock);
av_packet_free(&pkt);
return;
}
// resume on a keyframe with a clean decoder; after a timeout keep the old references instead
flush = key;
g_wait_key = false;
}
log_printf("Decodeur H.264 ouvert (%d threads, fast=%d)", cfg->threads, cfg->fast);
return ctx;
QueueItem *it = &g_queue[(g_queue_head + g_queue_count) % QUEUE_CAP];
it->pkt = pkt;
it->recv_us = t;
it->session = g_session;
it->flush = flush;
g_queue_count++;
g_app.video_queue = g_queue_count;
pthread_mutex_unlock(&g_queue_lock);
}
static void publish_frame(AVFrame *frame)
static bool queue_pop(QueueItem *out, int *remaining)
{
pthread_mutex_lock(&g_frame_lock);
av_frame_unref(g_shared_frame);
av_frame_move_ref(g_shared_frame, frame);
g_has_new_frame = true;
pthread_mutex_unlock(&g_frame_lock);
g_app.video_frames++;
g_app.last_frame_us = now_us();
bool ok = false;
pthread_mutex_lock(&g_queue_lock);
if (g_queue_count > 0) {
*out = g_queue[g_queue_head];
g_queue_head = (g_queue_head + 1) % QUEUE_CAP;
g_queue_count--;
*remaining = g_queue_count;
g_app.video_queue = g_queue_count;
ok = true;
}
pthread_mutex_unlock(&g_queue_lock);
return ok;
}
static void decode_packet(AVCodecContext *ctx, AVPacket *pkt, AVFrame *frame)
// IDR slice (NAL type 5) before any non-IDR slice
static bool is_keyframe(const uint8_t *d, size_t n)
{
int rc = avcodec_send_packet(ctx, pkt);
if (rc < 0 && rc != AVERROR(EAGAIN))
return;
while (avcodec_receive_frame(ctx, frame) == 0)
publish_frame(frame);
for (size_t i = 0; i + 3 < n; i++) {
if (d[i] == 0 && d[i + 1] == 0 && d[i + 2] == 1) {
int type = d[i + 3] & 0x1F;
if (type == 5)
return true;
if (type == 1)
return false;
i += 2;
}
}
return false;
}
// ---------------------------------------------------------------- network thread
static void set_state(LinkState s)
{
pthread_mutex_lock(&g_app.lock);
@@ -93,13 +168,10 @@ static void set_state(LinkState s)
pthread_mutex_unlock(&g_app.lock);
}
static void *video_thread(void *arg)
static void *video_net_thread(void *arg)
{
(void)arg;
uint8_t *buf = av_malloc(SDVR_MAX_PAYLOAD + AV_INPUT_BUFFER_PADDING_SIZE);
AVPacket *pkt = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
AVCodecContext *ctx = NULL;
uint8_t *scratch = malloc(SDVR_MAX_PAYLOAD);
char err[256];
while (!g_app.quit) {
@@ -124,7 +196,7 @@ static void *video_thread(void *arg)
fd = -1;
}
if (fd < 0) {
app_set_message("Video : %s", err);
app_set_message("Video: %s", err);
set_state(LINK_ERROR);
for (int i = 0; i < 20 && !g_app.quit && g_app.generation == gen; i++)
sleep_ms(100);
@@ -132,52 +204,69 @@ static void *video_thread(void *arg)
}
g_sock = fd;
log_printf("Video connectee a %s", ip);
app_set_message("Connecte a la Switch (%s)", ip);
queue_new_session();
log_printf("Video connected to %s", ip);
app_set_message("Connected to the Switch (%s)", ip);
set_state(LINK_STREAMING);
// on recree le decodeur a chaque connexion pour repartir d'un etat propre
if (ctx)
avcodec_free_context(&ctx);
ctx = open_decoder(&cfg);
if (!ctx) {
app_set_message("Impossible d'initialiser le decodeur video");
set_state(LINK_ERROR);
sdvr_close(fd);
g_sock = -1;
sleep_ms(2000);
continue;
}
volatile bool stop = false;
uint64_t last_key_us = 0;
int keys_logged = 0;
SdvrPacketHeader hdr;
while (!g_app.quit && g_app.generation == gen) {
int rc = sdvr_read_packet(fd, &hdr, buf, &stop);
int rc = sdvr_read_header(fd, &hdr, &stop);
if (rc < 0) {
app_set_message("Video : connexion perdue, reconnexion...");
app_set_message("Video: connection lost, reconnecting...");
break;
}
if (rc == 0)
continue; // pas de donnees (aucun jeu lance ?)
continue; // no data (no game running?)
g_app.last_packet_us = now_us();
bool is_video = (hdr.MetaData & SDVR_META_VIDEO) && !(hdr.MetaData & (SDVR_META_REPLAY | SDVR_META_ERROR)) && hdr.DataSize > 0;
AVPacket *pkt = NULL;
uint8_t *dst = scratch;
if (is_video) {
pkt = av_packet_alloc();
if (!pkt || av_new_packet(pkt, (int)hdr.DataSize) < 0) {
av_packet_free(&pkt);
is_video = false;
} else {
dst = pkt->data;
}
}
if (sdvr_read_payload(fd, dst, hdr.DataSize, &stop) != 1) {
av_packet_free(&pkt);
app_set_message("Video: connection lost, reconnecting...");
break;
}
uint64_t t = now_us();
g_app.last_packet_us = t;
g_app.video_bytes += hdr.DataSize + sizeof(hdr);
if (hdr.MetaData & SDVR_META_ERROR) {
char msg[200];
sdvr_format_error(buf, hdr.DataSize, msg, sizeof(msg));
app_set_message("Switch : %s", msg);
sdvr_format_error(scratch, hdr.DataSize, msg, sizeof(msg));
app_set_message("Switch: %s", msg);
continue;
}
if (!(hdr.MetaData & SDVR_META_VIDEO) || (hdr.MetaData & SDVR_META_REPLAY) || hdr.DataSize == 0)
if (!is_video)
continue;
memset(buf + hdr.DataSize, 0, AV_INPUT_BUFFER_PADDING_SIZE);
pkt->data = buf;
pkt->size = (int)hdr.DataSize;
pkt->pts = (int64_t)hdr.Timestamp;
pkt->dts = pkt->pts;
decode_packet(ctx, pkt, frame);
int64_t off = (int64_t)t - (int64_t)hdr.Timestamp;
if (off < g_clock_offset)
g_clock_offset = off;
pkt->pts = pkt->dts = (int64_t)hdr.Timestamp;
bool key = is_keyframe(pkt->data, (size_t)pkt->size);
if (key) {
if (last_key_us && keys_logged < 5) {
log_printf("Keyframe interval: %d ms", (int)((t - last_key_us) / 1000));
keys_logged++;
}
last_key_us = t;
}
queue_push(pkt, key, g_app.cfg.max_delay_ms);
}
g_sock = -1;
@@ -185,11 +274,120 @@ static void *video_thread(void *arg)
set_state(LINK_CONNECTING);
}
free(scratch);
return NULL;
}
// ---------------------------------------------------------------- decoder thread
static AVCodecContext *open_decoder(const Config *cfg)
{
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
log_printf("H.264 decoder not found");
return NULL;
}
AVCodecContext *ctx = avcodec_alloc_context3(codec);
if (!ctx)
return NULL;
// the Switch's SPS/PPS given up front, like the official client
ctx->extradata_size = sizeof(SDVR_SPS) + sizeof(SDVR_PPS);
ctx->extradata = av_mallocz(ctx->extradata_size + AV_INPUT_BUFFER_PADDING_SIZE);
memcpy(ctx->extradata, SDVR_SPS, sizeof(SDVR_SPS));
memcpy(ctx->extradata + sizeof(SDVR_SPS), SDVR_PPS, sizeof(SDVR_PPS));
// frame threads add one frame of delay each; LOW_DELAY would disable them, so only use it with 1 thread
ctx->thread_count = cfg->threads;
ctx->thread_type = FF_THREAD_FRAME | FF_THREAD_SLICE;
if (cfg->threads <= 1)
ctx->flags |= AV_CODEC_FLAG_LOW_DELAY;
ctx->flags2 |= AV_CODEC_FLAG2_FAST;
if (cfg->fast)
ctx->skip_loop_filter = AVDISCARD_ALL;
int rc = avcodec_open2(ctx, codec, NULL);
if (rc < 0) {
log_printf("avcodec_open2 failed: %d", rc);
avcodec_free_context(&ctx);
return NULL;
}
log_printf("H.264 decoder opened (%d threads, fast=%d)", cfg->threads, cfg->fast);
return ctx;
}
static void publish_frame(AVFrame *frame)
{
pthread_mutex_lock(&g_frame_lock);
av_frame_unref(g_shared_frame);
av_frame_move_ref(g_shared_frame, frame);
g_has_new_frame = true;
pthread_mutex_unlock(&g_frame_lock);
g_app.video_frames++;
g_app.last_frame_us = now_us();
}
static void *video_dec_thread(void *arg)
{
(void)arg;
AVFrame *frame = av_frame_alloc();
AVCodecContext *ctx = NULL;
int session = -1;
bool fast = false;
uint32_t avg_us = 0;
while (!g_app.quit) {
QueueItem it;
int remaining;
if (!queue_pop(&it, &remaining)) {
sleep_ms(1);
continue;
}
if (it.session != session) {
// fresh decoder for every connection, with the settings of that connection
pthread_mutex_lock(&g_app.lock);
Config cfg = g_app.cfg;
pthread_mutex_unlock(&g_app.lock);
if (ctx)
avcodec_free_context(&ctx);
ctx = open_decoder(&cfg);
if (!ctx)
app_set_message("Could not initialise the video decoder");
session = it.session;
fast = cfg.fast;
avg_us = 0;
}
if (!ctx) {
av_packet_free(&it.pkt);
continue;
}
if (it.flush)
avcodec_flush_buffers(ctx);
// falling behind: skip the deblocking filter until the queue is drained
ctx->skip_loop_filter = (fast || remaining >= 2) ? AVDISCARD_ALL : AVDISCARD_DEFAULT;
uint64_t t0 = now_us();
int rc = avcodec_send_packet(ctx, it.pkt);
av_packet_free(&it.pkt);
if (rc >= 0 || rc == AVERROR(EAGAIN)) {
while (avcodec_receive_frame(ctx, frame) == 0)
publish_frame(frame);
}
#ifdef SYSDVR_TEST
if (g_test_decode_delay_ms)
sleep_ms(g_test_decode_delay_ms);
#endif
uint32_t dt = (uint32_t)(now_us() - t0);
avg_us = avg_us ? (avg_us * 15 + dt) / 16 : dt;
g_app.decode_us = avg_us;
}
if (ctx)
avcodec_free_context(&ctx);
av_frame_free(&frame);
av_packet_free(&pkt);
av_free(buf);
return NULL;
}
@@ -199,7 +397,20 @@ bool video_start(void)
g_render_frame = av_frame_alloc();
if (!g_shared_frame || !g_render_frame)
return false;
return pthread_create(&g_thread, NULL, video_thread, NULL) == 0;
g_clock_offset = INT64_MAX;
if (pthread_create(&g_dec_thread, NULL, video_dec_thread, NULL) != 0)
return false;
return pthread_create(&g_net_thread, NULL, video_net_thread, NULL) == 0;
}
// ---------------------------------------------------------------- rendering (main thread)
bool video_has_new_frame(void)
{
pthread_mutex_lock(&g_frame_lock);
bool has = g_has_new_frame;
pthread_mutex_unlock(&g_frame_lock);
return has;
}
static void noop_free(void *opaque, uint8_t *data)
@@ -208,7 +419,7 @@ static void noop_free(void *opaque, uint8_t *data)
(void)data;
}
bool video_render(uint32_t *fb, bool fullscreen, bool fast)
bool video_render(uint32_t *fb, bool fullscreen, bool smooth, bool force)
{
pthread_mutex_lock(&g_frame_lock);
bool has = g_has_new_frame;
@@ -216,16 +427,20 @@ bool video_render(uint32_t *fb, bool fullscreen, bool fast)
av_frame_unref(g_render_frame);
av_frame_move_ref(g_render_frame, g_shared_frame);
g_has_new_frame = false;
g_render_valid = true;
}
pthread_mutex_unlock(&g_frame_lock);
if (!has)
if (!has && !(force && g_render_valid))
return false;
AVFrame *src = g_render_frame;
if (has && g_clock_offset != INT64_MAX && src->pts != AV_NOPTS_VALUE)
g_app.video_delay_ms = (int32_t)(((int64_t)now_us() - src->pts - g_clock_offset) / 1000);
int dst_w, dst_h;
if (fullscreen) {
// garde le ratio de l'image
// keep the aspect ratio
dst_w = SCREEN_W;
dst_h = (int)((int64_t)SCREEN_W * src->height / src->width);
if (dst_h > SCREEN_H) {
@@ -239,7 +454,7 @@ bool video_render(uint32_t *fb, bool fullscreen, bool fast)
dst_w &= ~1;
dst_h &= ~1;
int flags = fast ? SWS_FAST_BILINEAR : SWS_BILINEAR;
int flags = smooth ? SWS_BILINEAR : SWS_FAST_BILINEAR;
if (!g_sws || g_sws_src_w != src->width || g_sws_src_h != src->height || g_sws_src_fmt != src->format ||
g_sws_dst_w != dst_w || g_sws_dst_h != dst_h || g_sws_flags != flags) {
if (g_sws)
@@ -254,7 +469,7 @@ bool video_render(uint32_t *fb, bool fullscreen, bool fast)
av_opt_set_int(g_sws, "sws_flags", flags, 0);
av_opt_set_int(g_sws, "threads", 3, 0);
if (sws_init_context(g_sws, NULL, NULL) < 0) {
log_printf("sws_init_context a echoue");
log_printf("sws_init_context failed");
sws_freeContext(g_sws);
g_sws = NULL;
return false;
@@ -265,10 +480,10 @@ bool video_render(uint32_t *fb, bool fullscreen, bool fast)
g_sws_dst_w = dst_w;
g_sws_dst_h = dst_h;
g_sws_flags = flags;
log_printf("Conversion %dx%d -> %dx%d", src->width, src->height, dst_w, dst_h);
log_printf("Scaling %dx%d -> %dx%d", src->width, src->height, dst_w, dst_h);
}
// bandes noires autour de l'image
// black bars around the picture
int ox = (SCREEN_W - dst_w) / 2;
int oy = (SCREEN_H - dst_h) / 2;
if (oy > 0) {
@@ -280,7 +495,7 @@ bool video_render(uint32_t *fb, bool fullscreen, bool fast)
gfx_rect(fb, ox + dst_w, oy, SCREEN_W - ox - dst_w, dst_h, RGB(0, 0, 0));
}
// l'image de sortie pointe directement dans le framebuffer
// the output frame points straight into the framebuffer
AVFrame *dst = av_frame_alloc();
dst->format = AV_PIX_FMT_BGRA;
dst->width = dst_w;
@@ -292,7 +507,7 @@ bool video_render(uint32_t *fb, bool fullscreen, bool fast)
int rc = sws_scale_frame(g_sws, dst, src);
av_frame_free(&dst);
if (rc < 0) {
log_printf("sws_scale_frame a echoue : %d", rc);
log_printf("sws_scale_frame failed: %d", rc);
return false;
}
return true;
+7 -3
View File
@@ -4,8 +4,12 @@
bool video_start(void);
// Si une nouvelle image est disponible, la convertit dans fb (1920x1080 BGRA) et retourne true.
bool video_render(uint32_t *fb, bool fullscreen, bool fast);
// True if a frame was decoded since the last video_render()
bool video_has_new_frame(void);
// Force la reconnexion (fermeture du socket en cours)
// Converts the newest decoded frame into fb (1920x1080 BGRA) and returns true.
// With force, redraws the previous frame when there is no new one.
bool video_render(uint32_t *fb, bool fullscreen, bool smooth, bool force);
// Forces a reconnect (closes the current socket)
void video_kick(void);
+11 -11
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
"""Fausse Switch qui imite le sysmodule SysDVR en mode TCP Bridge (protocole 02/03).
"""Fake Switch that mimics the SysDVR sysmodule in TCP Bridge mode (protocol 02/03).
Sert a tester le client PS4 (ou le harness natif) sans console.
Usage : python3 fake_switch.py fichier.h264 [--proto 02|03] [--no-broadcast]
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
@@ -16,7 +16,7 @@ REQ = struct.Struct("<IHBBBB6s")
def split_access_units(data):
"""Decoupe un flux H.264 Annex B en unites d'acces (une image par paquet, comme grc:d)."""
"""Splits an Annex B H.264 stream into access units (one frame per packet, like grc:d)."""
starts = []
i = 0
while True:
@@ -50,11 +50,11 @@ def handshake(conn, proto, expect_meta):
while len(req) < 16:
chunk = conn.recv(16 - len(req))
if not chunk:
raise ConnectionError("client parti pendant le handshake")
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 'REFUS'}")
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)
@@ -70,7 +70,7 @@ def serve_video(port, aus, proto):
srv.listen(1)
while True:
conn, addr = srv.accept()
print(f"[fake] video : client {addr}")
print(f"[fake] video: client {addr}")
try:
ok, _ = handshake(conn, proto, 1)
if not ok:
@@ -86,7 +86,7 @@ def serve_video(port, aus, proto):
if delay > 0:
time.sleep(delay)
except (ConnectionError, OSError) as e:
print(f"[fake] video : deconnecte ({e})")
print(f"[fake] video: disconnected ({e})")
finally:
conn.close()
@@ -98,7 +98,7 @@ def serve_audio(port, proto):
srv.listen(1)
while True:
conn, addr = srv.accept()
print(f"[fake] audio : client {addr}")
print(f"[fake] audio: client {addr}")
try:
ok, batch = handshake(conn, proto, 2)
if not ok:
@@ -120,7 +120,7 @@ def serve_audio(port, proto):
if delay > 0:
time.sleep(delay)
except (ConnectionError, OSError) as e:
print(f"[fake] audio : deconnecte ({e})")
print(f"[fake] audio: disconnected ({e})")
finally:
conn.close()
@@ -147,7 +147,7 @@ def main():
with open(args.h264, "rb") as f:
aus = split_access_units(f.read())
print(f"[fake] {len(aus)} images chargees, protocole {args.proto}")
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()
+50 -19
View File
@@ -1,5 +1,6 @@
// Harness natif (Linux) : fait tourner les vrais video.c / audio.c / sysdvr_proto.c contre fake_switch.py.
// Les APIs PS4 (log, temps, framebuffer, audio) sont remplacees par des stubs.
// Native (Linux) harness: runs the real video.c / audio.c / sysdvr_proto.c against fake_switch.py.
// PS4 APIs (log, time, framebuffer, audio) are replaced by stubs.
// SYSDVR_TEST_DECODE_DELAY_MS=N makes every decode N ms slower, to simulate a decoder that cannot keep up.
#include "../src/common.h"
#include "../src/gfx.h"
#include "../src/video.h"
@@ -15,6 +16,7 @@
AppShared g_app;
volatile uint64_t g_stub_audio_nonzero_chunks;
int g_test_decode_delay_ms;
uint64_t now_us(void)
{
@@ -49,38 +51,54 @@ void gfx_rect(uint32_t *fb, int x, int y, int w, int h, uint32_t color)
int main(void)
{
const char *d = getenv("SYSDVR_TEST_DECODE_DELAY_MS");
g_test_decode_delay_ms = d ? atoi(d) : 0;
bool slow = g_test_decode_delay_ms > 0;
pthread_mutex_init(&g_app.lock, NULL);
// memes valeurs que config_defaults() (util.c depend de libkernel)
g_app.cfg.threads = 3;
// same values as config_defaults() (util.c depends on libkernel)
g_app.cfg.threads = 2;
g_app.cfg.audio = true;
g_app.cfg.fullscreen = true;
g_app.cfg.audio_batch = 2;
g_app.cfg.audio_buffer_ms = 80;
g_app.cfg.smooth = true;
g_app.cfg.audio_batch = 0;
g_app.cfg.audio_buffer_ms = 60;
g_app.cfg.max_delay_ms = 150;
char ip[64] = { 0 }, serial[64] = { 0 };
if (!sdvr_discover(5000, ip, sizeof(ip), serial, sizeof(serial))) {
printf("ECHEC : decouverte UDP\n");
printf("FAIL: UDP discovery\n");
return 1;
}
printf("Decouverte OK : %s serie=%s\n", ip, serial);
printf("Discovery OK: %s serial=%s\n", ip, serial);
strcpy(g_app.target_ip, ip);
if (!video_start() || !audio_start()) {
printf("ECHEC : demarrage des threads\n");
printf("FAIL: could not start the threads\n");
return 1;
}
uint32_t *fb = malloc((size_t)SCREEN_W * SCREEN_H * 4);
int rendered = 0;
int32_t max_delay = 0;
uint64_t delay_sum = 0, delay_n = 0;
uint64_t t0 = now_us();
while (now_us() - t0 < 6000000) {
if (video_render(fb, true, false))
while (now_us() - t0 < 8000000) {
if (video_has_new_frame() && video_render(fb, true, true, false)) {
rendered++;
else
usleep(2000);
// ignore the first second (connection, first keyframe)
if (now_us() - t0 > 1000000) {
if (g_app.video_delay_ms > max_delay)
max_delay = g_app.video_delay_ms;
delay_sum += g_app.video_delay_ms > 0 ? g_app.video_delay_ms : 0;
delay_n++;
}
} else {
usleep(1000);
}
}
// une image du milieu en PPM pour verification visuelle
// one frame in PPM for a visual check
FILE *f = fopen("frame.ppm", "wb");
fprintf(f, "P6 %d %d 255\n", SCREEN_W, SCREEN_H);
for (size_t i = 0; i < (size_t)SCREEN_W * SCREEN_H; i++) {
@@ -90,11 +108,24 @@ int main(void)
}
fclose(f);
printf("Images decodees=%u rendues=%d octets video=%llu chunks audio non vides=%llu etat video=%d audio=%d\n",
g_app.video_frames, rendered, (unsigned long long)g_app.video_bytes,
(unsigned long long)g_stub_audio_nonzero_chunks, g_app.video_state, g_app.audio_state);
bool ok = g_app.video_frames > 120 && rendered > 100 && g_stub_audio_nonzero_chunks > 500;
printf(ok ? "RESULTAT : OK\n" : "RESULTAT : ECHEC\n");
int avg_delay = delay_n ? (int)(delay_sum / delay_n) : -1;
printf("Decode delay=%d ms frames decoded=%u rendered=%d skipped=%u video bytes=%llu delay avg=%d max=%d ms\n",
g_test_decode_delay_ms, g_app.video_frames, rendered, g_app.video_dropped, (unsigned long long)g_app.video_bytes,
avg_delay, max_delay);
printf("Decode %.1f ms/frame audio chunks=%llu fill=%u ms underruns=%u state video=%d audio=%d\n",
g_app.decode_us / 1000.0, (unsigned long long)g_stub_audio_nonzero_chunks, g_app.audio_fill_ms,
g_app.audio_underruns, g_app.video_state, g_app.audio_state);
bool ok;
if (!slow) {
// real time: nothing dropped, low delay
ok = g_app.video_frames > 200 && rendered > 180 && g_app.video_dropped == 0 && max_delay < 150 &&
g_stub_audio_nonzero_chunks > 1000;
} else {
// decoder slower than the stream: must skip frames and keep the delay bounded instead of growing
ok = g_app.video_dropped > 0 && rendered > 30 && max_delay < g_app.cfg.max_delay_ms + 250;
}
printf(ok ? "RESULT: OK\n" : "RESULT: FAIL\n");
g_app.quit = true;
return ok ? 0 : 1;
}
+1 -1
View File
@@ -1,4 +1,4 @@
// Stub natif de sceAudioOut pour tester audio.c sur PC
// Native sceAudioOut stub to test audio.c on a PC
#pragma once
#include <stdint.h>
#include <unistd.h>
+2 -2
View File
@@ -1,4 +1,4 @@
# Genere sce_sys/icon0.png (512x512) et sce_sys/pic1.png (1920x1080) pour le paquet PS4.
# Generates sce_sys/icon0.png (512x512) and sce_sys/pic1.png (1920x1080) for the PS4 package.
import os
import sys
from PIL import Image, ImageDraw, ImageFont
@@ -16,7 +16,7 @@ def font(size):
def switch_logo(d, cx, cy, s):
# deux Joy-Con stylises (rouge neon / bleu neon) autour d'un ecran
# two stylised Joy-Cons (neon red / neon blue) around a screen
w, h = int(150 * s), int(250 * s)
d.rounded_rectangle([cx - w - 40 * s, cy - h // 2, cx - 40 * s, cy + h // 2], radius=int(60 * s), fill=(0, 190, 240))
d.rounded_rectangle([cx + 40 * s, cy - h // 2, cx + w + 40 * s, cy + h // 2], radius=int(60 * s), fill=(255, 60, 40))