mirror of
https://github.com/ayka-667/SysDVRClient-PS4.git
synced 2026-10-29 04:00:25 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9252569a82 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
@@ -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(¶m);
|
||||
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,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
@@ -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))
|
||||
|
||||
Reference in new issue
Block a user