Compare commits

...
10 Commits
Author SHA1 Message Date
fstat13 d292d0fbf7 fix: harden FTP/send paths and driver registration (audit round)
- send_all(): full send loop for the elfldr payload push and the FTP STOR
  (a single send() can legally transfer less than requested)
- PASV: check command result and zero the buffer before parsing (no more
  parsing of uninitialized stack on FTP failure)
- DllRegisterServer: verify the per-user key was actually written and
  return a real HRESULT instead of unconditional S_OK
- README: 'Verify the download' section with release checksums
2026-10-15 15:54:16 +02:00
fstat13 edf3c4c6f2 docs: new social preview image, used as og:image on the site 2026-10-15 15:34:27 +02:00
fstat13 876f8ef82b docs: redesign the guide site, fix inaccurate docs
- new modern layout for index.html / index.it.html with shared
  style.css + main.js: app-window mock, pipeline diagram, searchable
  troubleshooting, copy buttons on build commands, live star count
- stream port is 5599 (not 9xxx); real log line is
  "ERRORE caricamento dll"; button label is STOP
- troubleshooting covers more real log lines (UAC refused, pldmgr
  missing, elfldr unreachable, stream dropped)
- links to .md guides point to GitHub, download goes to /releases
  (v0.1 is a pre-release, /latest would miss it)
2026-10-15 15:28:12 +02:00
fstat13 07fe4968da docs: social preview image + README badges 2026-10-15 15:19:40 +02:00
fstat13 472ea0cab0 docs: link the published guide 2026-10-15 15:16:55 +02:00
fstat13 c0ea0921c9 repo: drop accidental log file, fix ignore pattern 2026-10-15 15:15:17 +02:00
fstat13 a3692b1e25 docs: italian editions + front-page links
- docs/GUIDE.it.md and docs/BUILD.it.md: full italian translations
- docs/index.it.html: italian HTML guide with language switcher
- README: guide/build links right under the badges (EN + IT)
- GUIDE.md / BUILD.md: link to their italian editions
2026-10-15 15:14:21 +02:00
fstat13 0daa328a3c docs: fool-proof guide + complete build walkthrough (md and html)
- docs/GUIDE.md: step-by-step usage (what you see at every step), tuning,
  symptom-by-symptom troubleshooting, uninstall
- docs/BUILD.md: complete build chain (payload via docker, PC toolchain
  install, exact commands, verification, gotchas)
- docs/index.html: rebuilt with quick-start detail, log-line table,
  troubleshooting, build section, anchors
- README: detailed quick start, corrected build commands (static dll,
  luxtheme, embed order) and links
2026-10-15 15:12:13 +02:00
fstat13 99f895cafb app: modern dark UI, honest driver installer, waiting status
- custom-chrome dark window: painted title bar (minimize/close), rounded
  region, owner-drawn buttons, status pill with state dot, dark console pane
- installer: verified writes (write -> check -> report), in-process
  LoadLibrary + DllRegisterServer with real HRESULT reporting; no more
  cryptic rundll32 'module not found'
- driver check verifies the registered dll still exists on disk
- feeder reports 'IN ATTESA DELLA PS5' when the console is unreachable
- log file next to the exe with %TEMP% fallback
- docs: installer + engineering log (B6, B7, lesson 8)
2026-10-15 15:09:35 +02:00
fstat13 9676e67432 app: driver embedded in the executable (single-file installer)
- pscapture.dll now ships as RCDATA resource n.3: the installer extracts it
  to Program Files and registers it; on-disk copy kept as fallback
- docs: installer walkthrough and build order updated
2026-10-15 14:52:45 +02:00
16 changed files with 2738 additions and 316 deletions

No files matched your search

+5
View File
@@ -10,3 +10,8 @@ pscapture_trace.txt
# ambienti
_sequenza_esiti/
_BOZZE/
# runtime
pscapture-app.log
pscapture.ini
dist/SHA256SUMS.txt
+82 -26
View File
@@ -15,6 +15,30 @@ No capture card. No Remote Play. No cloud. Just your console and your network.<b
<b>by fstat13</b>
</p>
<p align="center">
📖 <b><a href="docs/GUIDE.md">How to use it — step-by-step guide</a></b>
&nbsp;·&nbsp;
🇮🇹 <b><a href="docs/GUIDE.it.md">Guida passo-passo in italiano</a></b>
&nbsp;·&nbsp;
🔧 <b><a href="docs/BUILD.md">Build from source</a></b>
&nbsp;·&nbsp;
🇮🇹 <b><a href="docs/BUILD.it.md">Build dai sorgenti</a></b>
</p>
<p align="center">
🌐 Illustrated guide:
<b><a href="https://fstat13.github.io/PSCapture/">English</a></b>
&nbsp;·&nbsp;
<b><a href="https://fstat13.github.io/PSCapture/index.it.html">Italiano</a></b>
</p>
<p align="center">
<a href="https://github.com/fstat13/PSCapture/releases"><img src="https://img.shields.io/github/v/release/fstat13/PSCapture?style=flat-square&color=0070d1&label=release" alt="Release"></a>
<a href="https://github.com/fstat13/PSCapture/stargazers"><img src="https://img.shields.io/github/stars/fstat13/PSCapture?style=flat-square&color=0070d1" alt="Stars"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-source--available-35a3ff?style=flat-square" alt="License"></a>
<img src="https://img.shields.io/badge/platform-Windows%2010%2F11-0070d1?style=flat-square" alt="Platform">
</p>
---
## ✨ What you get
@@ -31,50 +55,82 @@ No capture card. No Remote Play. No cloud. Just your console and your network.<b
## 🚀 Quick start
1. **Jailbreak your PS5** (payload loader on `:9021`, anonymous FTP on `:2121`,
`pldmgr` autoloader — all standard in today's setups).
2. **Run `PSCapture.exe`** (admin is requested automatically; set your PS5 IP
if it isn't the default).
3. **Press Start.** The app does the rest. The console pops a notification:
`PSCapture is streaming on:PORT`
4. In Discord/OBS pick the camera **PS5 Capture**.
1. **Jailbreak your PS5** — you need the standard setup: a payload loader on
TCP `9021` (`elfldr`-style), anonymous FTP on TCP `2121`, and optionally
`pldmgr` so the payload starts on every boot.
2. **Double-click `PSCapture.exe`** (one file — the console payload and the
Windows driver travel inside it). Accept the UAC prompt. If your PS5 isn't
at `192.168.0.36`, type its IP in the *PS5 IP* field and press *Apply*.
3. **Watch the log pane** — the app installs the capture driver (first run
only), sends the payload to the console and connects by itself. The status
pill turns green: `STREAMING`. The console shows the system toast
**PSCapture is streaming on:PORT**.
4. In Discord (*Settings → Voice & Video → Camera*) or OBS pick the camera
**PS5 Capture**.
📖 The illustrated guide lives in [`docs/index.html`](docs/index.html)
(open it in a browser, or enable GitHub Pages on `/docs`).
🔧 Curious about what happens under the hood when you press Start? See
[`docs/INSTALLER.md`](docs/INSTALLER.md) — the exact four steps the app runs,
down to registry keys and console paths.
Stuck on *“IN ATTESA DELLA PS5”*? Console off, not jailbroken yet, or wrong
IP — the app retries by itself every 2 seconds.
📖 Step-by-step with every log line explained: [`docs/GUIDE.md`](docs/GUIDE.md)
— or the [illustrated HTML guide](docs/index.html).
🔧 What the app does under the hood when you press Start:
[`docs/INSTALLER.md`](docs/INSTALLER.md).
## 🔒 Verify the download
The release binaries are built from this repository's sources. Every release
carries a `SHA256SUMS.txt` — check it before running the exe:
```
certutil -hashfile pscapture.exe SHA256 (Windows)
sha256sum -c SHA256SUMS.txt (Linux/macOS)
```
And you never have to trust any binary: the whole toolchain is documented in
[`docs/BUILD.md`](docs/BUILD.md) — rebuild `pscapture.exe` from the sources
yourself and compare.
## 📦 Repository layout
```
payload/ PS5 side: display-engine reader + NV12 streamer (ps5capture.elf)
pc/ PSCapture.exe — GUI + installer + payload deployer + feeder
payload/ PS5 side: display-engine reader + NV12 streamer (pscapture.elf)
pc/ PSCapture.exe — GUI + installer + payload deployer + feeder
pc/ps5cam.c the virtual capture-card driver (DirectShow source filter)
pc/shm.h shared-memory contract between feeder and driver
pc/*.dll the virtual capture-card driver (DirectShow source filter)
docs/ HTML guide (dark theme, ready for GitHub Pages)
docs/ guides: HTML, step-by-step, build, installer, RE, log
```
## 🔧 Build
**Console payload** (needs the PS5 payload SDK; any Linux/docker with it):
Full walk-through (toolchain install, verification, gotchas):
**[`docs/BUILD.md`](docs/BUILD.md)**. The short version, in the **mandatory
order** (the `.res` embeds the driver and the payload as they are on disk):
**Console payload** → `payload/pscapture.elf` (needs Docker + the PS5 payload
SDK image; any tag works, here `ps5sdk:18`):
```bash
docker run --rm -v "$PWD/payload:/payload" -w /payload <ps5-payload-sdk-image> make
docker run --rm -v "$PWD/payload:/payload" -w /payload ps5sdk:18 make
```
**PC app** (mingw-w64, no dependencies):
**PC side** → `pscapture.exe` + `pscapture.dll` (needs MinGW-w64 with `gcc` +
`windres`):
```bash
windres pscapture.rc -O coff -o pscapture.res # icon + embedded payload
gcc -O2 -mwindows -o pscapture.exe pscapture.c pscapture.res -lws2_32 -lshell32 -ladvapi32
cd pc
cp ../payload/pscapture.elf . # gets embedded as-is
# 1) the capture-card driver (-static is mandatory, see BUILD.md)
gcc -O2 -shared -static -static-libgcc -o pscapture.dll ps5cam.c \
-lole32 -loleaut32 -luser32 -ladvapi32 -lws2_32
# 2) resources = icon + payload + driver
windres pscapture.rc -O coff -o pscapture.res
# 3) the app (one self-contained executable)
gcc -O2 -mwindows -o pscapture.exe pscapture.c pscapture.res \
-lws2_32 -lshell32 -ladvapi32 -luxtheme
```
**Driver** (mingw-w64):
```bash
gcc -O2 -shared -o pscapture.dll ps5cam.c -lole32 -loleaut32 -luser32 -ladvapi32 -lws2_32
```
Rebuild rule: **DLL → windres → exe, every time** — rebuilding only the exe
ships the old driver inside it.
## ❓ FAQ
+162
View File
@@ -0,0 +1,162 @@
# PSCapture — build dai sorgenti, passo per passo
Tutto il necessario per passare da questi sorgenti a un `PSCapture.exe`
funzionante e a un `pscapture.elf` per la console. Ogni comando è scritto
esattamente come va eseguito, in ordine. Qui dentro non c'è magia: l'ordine
conta e ogni passo è spiegato.
Ci sono due metà, che si compilano in posti diversi:
| Metà | Output | Dove si compila | Toolchain |
|---|---|---|---|
| Payload console | `payload/pscapture.elf` | Docker (container Linux) | PS5 payload SDK (prospero-clang) |
| Lato PC | `pc/pscapture.exe` + `pc/pscapture.dll` | Windows | MinGW-w64 (`gcc` + `windres`) |
🇬🇧 English version: [BUILD.md](BUILD.md)
---
## Parte 1 — Il payload per la console
### 1.1 Cosa serve
- **Docker** (Docker Desktop su Windows, o Docker normale su Linux/macOS).
- **L'immagine del PS5 payload SDK.** È il payload SDK della community per
PS5 (`prospero-clang`, `make`, `PS5_PAYLOAD_SDK` nell'ambiente) impacchettato
in una immagine. Sulla macchina di sviluppo l'immagine si chiama `ps5sdk:18`
— qualsiasi nome va bene, basta adattare i comandi.
Controlla che l'immagine ci sia:
```bash
docker images | grep ps5sdk
```
### 1.2 Build
Dalla radice della repo:
```bash
docker run --rm -v "$PWD/payload:/payload" -w /payload ps5sdk:18 make
```
Su Windows (PowerShell/cmd) metti il percorso completo al posto di
`"$PWD/payload"`, es.: `-v "C:\Users\tuonome\PSCapture\payload:/payload"`.
Risultato: **`payload/pscapture.elf`** (~175 KB).
Il `Makefile` fa l'unica cosa che conta: compila `main.c` con il clang
Prospero dell'SDK e linka l'ELF finale. Nessun altro input.
### 1.3 Opzionale — mandarlo alla console
Con la console jailbroken (payload loader sulla porta 9021):
```bash
docker run --rm -v "$PWD/payload:/payload" -w /payload ps5sdk:18 \
make deploy PS5_HOST=192.168.0.36
```
(oppure carica `pscapture.elf` con il loader che usi di solito).
Come si presenta un avvio sano sulla console:
- appare `/data/pscapture.log` con il log di partenza,
- appare `/data/pscapture.port` con la porta TCP dello stream,
- la console mostra la notifica *PSCapture is streaming on:PORT*.
---
## Parte 2 — Il lato PC
### 2.1 Cosa serve (Windows)
**MinGW-w64 64-bit** con `gcc` e `windres`. Sulla macchina di sviluppo:
```powershell
winget install BrechtSanders.WinLibs.POSIX.UCRT
```
(oppure qualsiasi MSYS2 con `mingw-w64-x86_64-gcc` — `pacman -S mingw-w64-x86_64-gcc`).
Controlla che entrambi siano nel PATH:
```bash
gcc --version
windres --version
```
### 2.2 Build — l'ordine è obbligatorio
`pscapture.res` è un bundle che **incastra i file così come sono su disco**:
l'icona, il payload della console e il driver della capture card. Quindi la
DLL va compilata *prima* del passo risorse, e il payload deve già trovarsi in
`pc/` quando gira `windres`. L'EXE si compila per ultimo.
```bash
cd pc
# 0) porta il payload appena compilato accanto al .rc (viene incastrato così com'è)
cp ../payload/pscapture.elf .
# 1) il driver della capture card (filtro sorgente DirectShow)
gcc -O2 -shared -static -static-libgcc -o pscapture.dll ps5cam.c \
-lole32 -loleaut32 -luser32 -ladvapi32 -lws2_32
# 2) bundle: icona + payload + driver in un unico oggetto risorse
# (pscapture.rc => 1 ICON pscapture.ico / 2 RCDATA pscapture.elf
# / 3 RCDATA pscapture.dll)
windres pscapture.rc -O coff -o pscapture.res
# 3) l'app — un unico eseguibile autocontenuto
gcc -O2 -mwindows -o pscapture.exe pscapture.c pscapture.res \
-lws2_32 -lshell32 -ladvapi32 -luxtheme
```
### 2.3 Verifica della build
```bash
# il driver deve dipendere SOLO da DLL di sistema (niente libgcc/libwinpthread):
objdump -p pscapture.dll | grep "DLL Name"
# le dimensioni giuste: exe ~500 KB, dll ~220 KB
ls -la pscapture.exe pscapture.dll
# payload e driver sono davvero dentro l'eseguibile:
strings pscapture.exe | grep -c PSCapture
```
Avvia `pscapture.exe`: si apre la finestra (prompt UAC → Sì) e il pannello log
mostra la sequenza controllo driver → estrazione payload → tentativo di
deploy. È la stessa sequenza descritta in `docs/INSTALLER.md`.
### 2.4 Trappole (imparate sul campo)
- **`-static -static-libgcc` sulla DLL non è cosmetico.** Senza, il driver
può ritrovarsi a dipendere dalle DLL runtime di mingw che *non* sono nel
PATH di sistema quando Windows (o un'altra app) lo carica — il classico
*"impossibile trovare il modulo specificato"*.
- **Ordine di build.** `windres` legge `pscapture.dll` e `pscapture.elf` da
`pc/` al momento della compilazione. Ricompili la DLL → rifai `windres` →
ricompili l'EXE, ogni volta. Dimenticare il passo centrale spedisce il
driver vecchio dentro l'exe nuovo.
- **Solo ASCII** nelle stringhe passate alle API Win32 `*A` (titoli, bottoni,
log). Byte UTF-8 in una stringa ANSI si vedono come caratteri rotti.
- Il **manifest** (`pc/pscapture.manifest`) è tenuto solo per riferimento;
la build attuale **non** lo incastra — l'auto-elevazione è fatta nel codice
(`ShellExecuteEx … "runas"`), cosa che evita il `.rsrc merge failure` di
mingw con più manifest.
### 2.5 Preparare una release
```bash
mkdir -p dist
cp pc/pscapture.exe pc/pscapture.dll dist/
```
Per gli utenti basta `pscapture.exe` da solo (la DLL e il payload viaggiano
dentro). La `pscapture.dll` sciolta si allega per le installazioni manuali:
```bash
rundll32 "pscapture.dll",DllRegisterServer
```
+162
View File
@@ -0,0 +1,162 @@
# PSCapture — build from source, step by step
🇮🇹 Versione italiana: [BUILD.it.md](BUILD.it.md)
Everything needed to go from these sources to a working `PSCapture.exe` and a
`pscapture.elf` for the console. Every command is written exactly as it is
run, in order. Nothing here is optional magic — the order matters and each
step is explained.
There are two halves, built in different places:
| Half | Output | Where it builds | Toolchain |
|---|---|---|---|
| Console payload | `payload/pscapture.elf` | Docker (Linux container) | PS5 payload SDK (prospero-clang) |
| PC side | `pc/pscapture.exe` + `pc/pscapture.dll` | Windows | MinGW-w64 (`gcc` + `windres`) |
---
## Part 1 — The console payload
### 1.1 What you need
- **Docker** (Docker Desktop on Windows, or plain Docker on Linux/macOS).
- **The PS5 payload SDK container image.** This is the community PS5 payload
SDK (`prospero-clang`, `make`, `PS5_PAYLOAD_SDK` set in the environment)
baked into an image. On the development machine the image is tagged
`ps5sdk:18` — any name works, adjust the commands below to match yours.
Check the image is there:
```bash
docker images | grep ps5sdk
```
### 1.2 Build
From the repository root:
```bash
docker run --rm -v "$PWD/payload:/payload" -w /payload ps5sdk:18 make
```
On Windows PowerShell/cmd, give the full path instead of `"$PWD/payload"`,
e.g. `-v "C:\Users\you\PSCapture\payload:/payload"`.
Result: **`payload/pscapture.elf`** (~175 KB).
The `Makefile` does the only thing that matters: compile `main.c` with the
Prospero clang from the SDK and link the final ELF. No other inputs.
### 1.3 Optional — send it to the console
With the console jailbroken (payload loader on port 9021):
```bash
docker run --rm -v "$PWD/payload:/payload" -w /payload ps5sdk:18 \
make deploy PS5_HOST=192.168.0.36
```
(or just push `pscapture.elf` with whatever loader you normally use).
What a healthy run looks like on the console:
- `/data/pscapture.log` appears and logs the startup,
- `/data/pscapture.port` appears containing the TCP port of the stream,
- the console pops the notification *PSCapture is streaming on:PORT*.
---
## Part 2 — The PC side
### 2.1 What you need (Windows)
**MinGW-w64 64-bit** with `gcc` and `windres`. On the development machine:
```powershell
winget install BrechtSanders.WinLibs.POSIX.UCRT
```
(or any MSYS2 `mingw-w64-x86_64-gcc` install — `pacman -S mingw-w64-x86_64-gcc`).
Check both tools are on PATH:
```bash
gcc --version
windres --version
```
### 2.2 Build — the order is mandatory
`pscapture.res` is a bundle that **embeds files as they are on disk**: the
icon, the console payload and the capture-card driver. So the DLL must be
built *before* the resource step, and the payload must be sitting in `pc/`
when `windres` runs. Build the EXE last.
```bash
cd pc
# 0) bring the freshly built payload next to the .rc (it gets embedded as-is)
cp ../payload/pscapture.elf .
# 1) the capture-card driver (DirectShow source filter)
gcc -O2 -shared -static -static-libgcc -o pscapture.dll ps5cam.c \
-lole32 -loleaut32 -luser32 -ladvapi32 -lws2_32
# 2) bundle icon + payload + driver into one resource object
# (pscapture.rc => 1 ICON pscapture.ico / 2 RCDATA pscapture.elf
# / 3 RCDATA pscapture.dll)
windres pscapture.rc -O coff -o pscapture.res
# 3) the app — one self-contained executable
gcc -O2 -mwindows -o pscapture.exe pscapture.c pscapture.res \
-lws2_32 -lshell32 -ladvapi32 -luxtheme
```
### 2.3 Verify the build
```bash
# the driver must depend on system DLLs ONLY (no libgcc/libwinpthread):
objdump -p pscapture.dll | grep "DLL Name"
# sizes look right: exe ~500 KB, dll ~220 KB
ls -la pscapture.exe pscapture.dll
# both the payload and the driver are really inside the exe:
strings pscapture.exe | grep -c PSCapture
```
Run `pscapture.exe`: the window opens (UAC prompt → Yes), the log pane walks
through driver check → payload extraction → deploy attempt. That is the same
sequence described in `docs/INSTALLER.md`.
### 2.4 Gotchas (learned the hard way)
- **`-static -static-libgcc` on the DLL is not cosmetic.** Without it the
driver can end up needing mingw runtime DLLs that are *not* on the system
PATH when Windows (or another app) loads it — the classic
*"the specified module could not be found"*.
- **Build order.** `windres` reads `pscapture.dll` and `pscapture.elf` from
`pc/` at compile time. Rebuild the DLL → re-run `windres` → rebuild the
EXE, every time. Forgetting the middle step ships an old driver inside the
new exe.
- **ASCII only** in strings passed to the `*A` Win32 APIs (window titles,
buttons, logs). UTF-8 bytes in an ANSI string render as mojibake.
- The **manifest** (`pc/pscapture.manifest`) is kept for reference only;
the current build does **not** embed it — self-elevation is done in code
(`ShellExecuteEx … "runas"`), which avoids mingw's `.rsrc merge failure`
with multiple manifests.
### 2.5 Package a release
```bash
mkdir -p dist
cp pc/pscapture.exe pc/pscapture.dll dist/
```
`pscapture.exe` alone is enough for users (the DLL and the payload travel
inside it). The bare `pscapture.dll` is attached for manual installs:
```bash
rundll32 "pscapture.dll",DllRegisterServer
```
+233
View File
@@ -0,0 +1,233 @@
# PSCapture — Engineering log: problems, diagnoses, work-arounds
This is the long story: every wall we hit while building PSCapture, how it
was diagnosed, and what finally fixed (or bypassed) it. Dead ends are kept on
purpose — they cost real time and shouldn't cost yours.
*(Testbed: retail PS5, firmware 13.60, jailbroken; PC on the same LAN.
All work done on personally owned hardware.)*
---
## Part A — Getting pixels out of the console
### A1. The official capture stack is a dead end
**Symptoms / approach.** First attempts used the console services that look
like capture APIs: `libSceAvcap2` / `libSceVideoRecording` etc.
**What we found.** `libSceVideoRecording` opens a session backed by a **128 MB
file** and talks to its client over internal IPC; calls from an unprivileged
payload fail with the service unavailable. The IPMI (inter-process messaging)
client table is limited, and every live instance of our payload holds a
session — with ~26 stale instances, a new payload waits **forever**
(`connect -> 0x8002000d`).
**Resolution.** Abandoned the service path entirely. The work-around that
later became permanent: (1) never rely on the recording service; (2) payloads
kill their own stale instances at boot ("v17 hygiene"), fingerprinting them by
*their* fixed cacheable mapping in the page tables (PML4 entry #128 — no
system process uses it).
### A2. The direct path: the display engine's scan-out ring
**Diagnosis.** The AV-control daemon (`SceSysAvControl.elf`) exposes the
scan-out geometry: ring at `0x200200000`, data at `+0x4200000`, four surfaces
`0x600000` apart, rewritten at 60 Hz, stored as P010 (10-bit).
**First bug — P010 decoding.** Extracting the *even* bytes gave noise: the
usable byte of a 10-bit sample is the **odd** one. Lesson recorded in
`REVERSE_ENGINEERING.md` so nobody re-makes it.
### A3. The rotating image (the pan)
**Symptom.** The picture "rotated" and slid horizontally forever; reading the
four surfaces in alternation made it jump every frame.
**Diagnosis.** The scan-out base drifts within the ring: the frame does not
start at a fixed offset. The surface content is also quasi-periodic, which
made the first metric find a **twin minimum** 1920 bytes away and oscillate
between the two.
**Dead ends (documented so you skip them):**
- a "junction" continuity metric that needed a `+960` fudge — produced
oscillation between twins;
- chasing the drift with a per-frame tracker — the pursuit itself caused
visible horizontal wiggling.
**Final metric** ("exact cut"): strip `[row r, 0..maxj]` must match
`[row r-1, c..]`, minimizing `Σ |Y[r][j*2+1] − Y[r-1][(c+j)*2+1]|` over
sampled rows; conversion `pan = ((1920 − c) % 1920) * 2`.
**Resolution.** Field-verified fixed `pan = 2230` (the drift is ~2 B/s,
below visibility), and later a **user-tunable fine shift** on the PC side
(even pixels only, see D2).
### A4. The color saga (or: "v10 colors were right")
**Symptom.** Backgrounds looked red/amber instead of blue; users kept saying
"too red", "now too gray", "the colors of v10 were right".
**Diagnosis path.** The chroma plane does **not** start after the luma:
it begins at byte `0x3EA300` (= 4 104 960), 11 rows *before* the presumed
boundary, with dead zones before and after (the "green bar" of legend was the
tail read into the gap → replicate the last good row).
But position alone wasn't the whole story. Comparing a live frame **byte for
byte** against an old reference frame of the same screen showed the output
matched only with **U/V pairs swapped**: in this plane the 16-bit words are
stored **V-first**. Mean absolute error dropped 15.5 → 1.7 → 0.06 once the
pair order was corrected in the P010→NV12 conversion (a second shuffle mask
for the chroma region only).
**Honest note.** Before that discovery, the wrong knob was shipped: a chroma
**gain** to "lower the red" — it made the cast less visible but never right.
The gain knob survives as an optional tuning, default 1.0 (= raw).
**How the truth was found.** Three independent anchors: neutral white stays
(128,128) either way, the bluish background flips the right way only with the
swap, and the byte-level diff against the reference settles it numerically.
Rendering the *same bytes* with two different YUV formulas long ago had also
caused a wild goose chase — the rule became: **compare numerically, never
eye-render across periods** (see Lessons).
### A5. Kernel reads without crashing the console (three crashes, one rule)
**Crash #1/#2.** Full console lock-ups (all TCP ports mute, ICMP half-dead)
after storms of live payload instances + IPMI maintenance traffic. Recovered
only by physical power cycle. Root cause never *proven*; the permanent
hygiene (single instance, kill-istanze at boot) removed the whole class.
**Crash #3 (diagnosed exactly).** A kernel-dump experiment read kernel VA
through `kernel_copyout` — a fault there is a **kernel-mode fault**: instant
panic. **Rule, now absolute:** kernel memory is only ever read via a page
table walk with present-bit validation + 8-byte reads through the direct map;
`kernel_copyout` on kernel addresses is banned.
**Related guard bug (fun one).** The cacheable-mapping builder refused
physical addresses above 16 GB as "suspicious" — but the display surfaces
live at `0x459200000` (≈18.7 GB). Result: silent fallback to the slow path
(45 MB/s, 6 fps). Fix: raise the guard to 32 GB; the real safety net is the
page-table validation, not the numeric limit.
### A6. Speed: from 45 MB/s to 4 GB/s
**Diagnosis.** Reading the ring through syscall copies serialized inside the
process (multi-threading didn't help — the copies bottleneck in the kernel
path).
**Fix.** Build a **cacheable mapping** of the DCE physical range inside the
payload process: allocate our own PDPT/PD pages, replicate the daemon's PDE
flags (write-back, no PCD/PWT), write **one** PML4 entry, done. Bulk frame
reads went to ~4 GB/s.
**Tearing.** The DCE rewrites a surface at 60 Hz; a read during a rewrite
yields a mixed frame (ghosts, colored tails). Fix: read, wait 2 ms, compare;
if the surface changed it was alive — move to the next one.
---
## Part B — The PC side: from frames to a "webcam"
### B1. Pipeline shape
Feeder (TCP receiver) → shared memory ring → **DirectShow source filter**
(`ps5cam.c` → `pscapture.dll`) that every consumer sees as a camera called
**PS5 Capture**.
### B2. Discord error 2014 (spinner → no camera), root cause found in their binary
**Symptoms.** Camera selectable, infinite spinner, then Discord error 2014 —
with *any* virtual camera (ours and OBS Virtual Camera alike).
**Diagnosis.** Discord's own media logs contained the smoking gun:
`Failed to create capturer (video_capture_module_source.cpp:105)`. Reading
their capture engine (WebRTC's DShow path inside the Discord binary):
the engine's device **picker requests 1280×720@30** and only accepts a device
that lists a *matching* capability. Ours offered only 1920×1080 → no match →
capturer never created. OBS Virtual Camera exposed zero caps → same fate.
**Fix — "real webcam" capability profile.** The driver now exposes 11 UVC-like
capabilities (YUY2/NV12/I420/RGB24/RGB32 at 1080p/720p/480p, negative-height
variants first), plus a `DevicePath` string in the registry (the modern
identifier consumers use instead of the friendly name). Verified in their
logs: `Capturer running at 1280x720@30`, first frame in 10–13 ms.
**Tooling discoveries that made this tractable:**
- Discord can be driven with `--remote-debugging-port`; from it the **media
engine is scriptable** (device selection, camera on/off) without touching
the UI — see `docs/` history and our notes.
- The binary is searchable for log strings (`(file:line)` hints) and
RIP-relative xrefs; that's how the picker logic was pinned down.
### B3. Flips and orientations
**Symptom.** 720p output upside-down for one consumer but not another.
**Diagnosis.** DIB convention sign vs YUV conventions differ between
consumers: ffmpeg/WebRTC treat YUV as top-down regardless of `biHeight` sign,
but XRGB-style DIBs as bottom-up.
**Fix.** Flip **only** for RGB subtypes when `biHeight > 0`; all YUV paths
stay top-down. Verified with ffmpeg grabs.
### B4. Alignment ("more space on the left than on the right")
**Symptom.** The image sat a few dozen pixels off; users described margins,
not offsets.
**Diagnosis.** Measured **inside** the frame: the centered title text of the
screen had its glyph center at x≈993 (960 = screen center) → the capture was
~33 px to the right; button margins agreed (~29 px, arrow ~18 px — the title
is the cleanest anchor).
**Fix.** A **cyclic horizontal shift** in the feeder (whole luma plane and UV
plane, same byte count; **even** pixels only — an odd shift re-labels U/V
pairs and hue-inverts), exposed as the live `shift px` knob.
**Long-term.** Bake the value into the payload's pan and drop the PC-side
shift (planned).
### B5. The 1080p question (and what Discord really does)
Users compared with capture cards: "Elgatos stream at 1080p!".
**Findings.** (1) The capture path in Discord calls is capped by *Discord's
engine* (it asks every camera for 720p — capture cards included); (2) as a
generic DirectShow source, PSCapture **already delivers 1920×1080** to OBS and
ffmpeg (verified with an ffmpeg 1080p grab while Discord was live) and that's
the road for 1080p streaming; (3) 1080p *inside* a Discord call via screen
share is possible up to 1080p60 with Nitro.
---
### B6. The installer crash-course: a cryptic module error and a lying log
**Symptom.** Pressing *Install driver* showed *"the specified module could not
be found"* - the classic rundll32 message, with no other clue anywhere.
**Diagnosis.** Two faults stacked: when the app was not elevated, writing
`pscapture.dll` into `%ProgramFiles%` failed silently - the old code logged
the extraction as successful anyway - and then `rundll32` was asked to load a
file that did not exist (hence the generic, useless message). On top of that,
`rundll32` never reports what went wrong back to the caller.
**Fix.** Every filesystem step is now verified (write -> check size ->
report), the install aborts honestly when the dll cannot be placed, and
registration happens in-process (`LoadLibrary` + `DllRegisterServer`) so
failures come back as real HRESULTs. The driver check also verifies that the
registered dll still *exists* on disk before declaring victory.
### B7. The modern control center UI
The first GUI iteration was a plain Win32 dialog. It was rebuilt as a dark
custom-chrome window: painted title bar with integrated minimize/close,
rounded window region, owner-drawn buttons, a live status pill with a state
dot, and a dark monospace console pane. Everything is still pure C with zero
frameworks - the UI is drawn by hand.
## Part C — The road to 60 fps
**The math.** Raw NV12 1080p is 3.11 MB/frame → 30 fps ≈ 93 MB/s (a gigabit
LAN is already at the wall at ~110 MB/s); 60 fps ≈ 186 MB/s → needs 2.5 GbE
*or* compression.
**Raw path (2.5 GbE adapter on the console).** The user's adapter is a TP-Link
UE302C (Realtek RTL8156B). Findings: the console kernel is **FreeBSD 9.0**
based; the matching driver (`ure`) appears in FreeBSD 13+, and Sony ships only
the drivers it uses — so nothing will recognize the adapter out of the box.
Two enablers were secured anyway: the **PLD payload format** and the kernel
payload loader on port **9019** (how the scene's kernel payloads ship), plus
the kernel offsets table for 13.60 — enough to *deliver* a custom kernel
module when the port is ready. (Full port = a separate workstream; the
homework is in `REVERSE_ENGINEERING.md` §4.)
**Compressed path (stays on gigabit).** The console has a hardware JPEG
encoder (`libSceJpegEnc`). At ~250-350 KB/frame, **60 fps ≈ 15-21 MB/s** —
five times inside a gigabit. The API surface was recovered from the library
itself (exports, NIDs, argument structs, validation rules — see the notes in
`REVERSE_ENGINEERING.md`), and a measuring payload is ready to run on the
console (each call in a forked child with timeout — Sony APIs abort on wrong
arguments, so a bad call must die alone).
---
## Lessons (the rules we now follow)
1. **Never** hand a kernel address to `kernel_copyout` — page-walk + present
check + direct-map reads, always.
2. One live payload instance. Kill stale ones at boot; never accumulate.
3. Measure with bytes, not eyes: same formula, same reference frame, signed
deltas. Eye-comparison across renderers lies.
4. Even-pixel shifts only (U/V pairing), for the same reason.
5. Never call `sceAvControlChangeScanInMode` from a payload (it can wedge the
display engine).
6. When an API can abort the host on bad arguments, isolate the call in a
child process with a timeout.
7. When a suspicious "fix" targets a symptom (the chroma gain), keep looking
for the *identity* of the problem (the U/V swap).
8. **Never log success without checking the I/O result.** A lying log (the
installer claiming "extracted ok" on a failed write) costs far more time
than a missing one - it sends you hunting in the wrong place.
+116
View File
@@ -0,0 +1,116 @@
# PSCapture — la guida passo-passo completa
Da zero alla tua PS5 dentro Discord. Ogni passo dice esattamente cosa devi
vedere sullo schermo. Se qualcosa non torna, salta alla tabella dei problemi
in fondo.
🇬🇧 English version: [GUIDE.md](GUIDE.md)
---
## 0. Cosa serve
**Sulla console** — tutto standard nei setup jailbreak di oggi:
| Cosa | Porta | A che serve |
|---|---|---|
| Payload loader (tipo `elfldr`) | TCP **9021** | esegue il payload che gli mandiamo |
| FTP anonimo | TCP **2121** | l'app legge/scrive file sulla console |
| `pldmgr` autoloader (opzionale) | — | fa partire il payload a ogni boot |
**Sul PC**: Windows 10/11 64-bit, rete cablata consigliata.
**L'app**: `PSCapture.exe` — **un solo file**. Scaricalo dalla pagina Releases
della repo. È il pacchetto completo: il payload per la console e il driver
per Windows sono dentro di lui.
---
## 1. Primo avvio sul PC
1. Metti `PSCapture.exe` dove vuoi (va benissimo il Desktop) e **fai doppio click**.
2. Windows chiede *"Consenti a questa app di apportare modifiche al
dispositivo?"* → clicca **Sì**.
*Perché:* registrare la webcam virtuale per tutti i programmi del PC
richiede i privilegi di amministratore, una volta sola. L'app si eleva da
sola — niente tasto destro.
3. Si apre la finestra (scura, con il bottone blu grande **START STREAMING**).
Se la tua PS5 non è a `192.168.0.36`: scrivi il suo indirizzo nel campo
**PS5 IP** e premi **Apply**.
4. Da qui in poi l'app fa tutto da sola. Guarda il pannello log scuro:
| Dovresti leggere | Significa |
|---|---|
| `driver gia' installato` | la camera è già registrata — ok |
| `driver estratto dall'eseguibile (… byte)` + `driver registrato - la webcam 'PS5 Capture' e' pronta` | primo avvio: driver installato |
| `payload dall'eseguibile (… byte)` + `payload avviato - la PS5 deve mostrare la notifica PSCapture` | il payload sta girando sulla console |
| `porta stream: 5599` poi `collegato a 192.168.0.36:5599` | stream connesso |
| la pillola diventa **verde**: `STREAMING - scegli 'PS5 Capture' in Discord` | sei live |
5. Sulla PS5 compare la notifica di sistema: **PSCapture is streaming on:PORT**.
(Quel toast è la console che ti dice che il payload è vivo.)
> Se la pillola resta su *"IN ATTESA DELLA PS5 — accesa e col jailbreak
> avviato?"* la console è spenta, non è jailbroken, o l'IP è sbagliato.
> Vedi la sezione 5.
## 2. Usarlo
**Discord (in chiamata)** — *Impostazioni → Voce e video → Telecamera* →
**PS5 Capture**. In chiamata l'immagine è 720p: è il motore di Discord, chiede
1280x720 a *tutte* le camere (capture card incluse).
**OBS** — *Sorgenti → **+** → Video Capture Device* → **PS5 Capture**.
Qui va alla piena risoluzione **1920x1080**.
**ffmpeg** — `ffmpeg -f dshow -i video="PS5 Capture" registrazione.mkv`
## 3. Uso quotidiano
- Apri `PSCapture.exe` → **Sì** all'UAC. L'app si ricollega da sola: se il
payload è già sulla console (autoloader) lo stream parte subito; se la
console è spenta la pillola dice *"IN ATTESA DELLA PS5"* e si connette nel
momento in cui la console è pronta.
- Premi **STOP** (o chiudi l'app) per fermare lo stream. La camera si congela
sull'ultimo frame; gli altri programmi continuano a vedere il dispositivo.
- **Deploy payload** rimanda il payload a mano (utile dopo un riavvio della
console senza autoloader). **Install driver** riregistra la camera a mano
(utile se il log mostra un errore del driver).
## 4. Le due manopole
| Manopola | Cosa fa | Regole |
|---|---|---|
| `gain` | forza del colore (croma) | `1.0` = grezzo, è il valore giusto. Toccala solo se i colori sembrano sbagliati. |
| `shift px` | sposta l'immagine a destra/sinistra | **solo numeri PARI** (un numero dispari inverte i colori). Regola finché l'immagine non è centrata: il testo del titolo della console è un buon riferimento. |
Entrambe si applicano in diretta (circa un secondo) e sono salvate in
`pscapture.ini` accanto all'exe, quindi sopravvivono ai riavvii.
## 5. Problemi — sintomo per sintomo
| Sintomo | Causa | Rimedio |
|---|---|---|
| La pillola resta su `AVVIO...` / `IN ATTESA DELLA PS5` | console spenta, **non jailbroken**, o IP sbagliato | accendi la console, avvia il jailbreak (le porte 9021/2121 devono essere aperte), controlla il campo IP |
| Il log ripete `PS5 non raggiungibile (...), riprovo` | come sopra | come sopra |
| La camera `PS5 Capture` non appare in Discord/OBS | driver mai registrato (install fallito) | premi **Install driver**; leggi la riga di log che segue — ora riporta l'errore *vero* |
| Log: `ERRORE: driver non scrivibile in C:\Program Files\PSCapture (serve admin?)` | app non elevata | chiudi e riavvia `PSCapture.exe` accettando il prompt UAC |
| Log: `ERRORE caricamento dll (err=126)` | l'antivirus ha messo in quarantena la dll estratta | controlla la quarantena dell'antivirus per `pscapture.dll`, sbloccala, premi di nuovo **Install driver** |
| La camera appare ma l'immagine è nera | stream non partito | controlla la pillola; premi **Deploy payload** e/o **START STREAMING** |
| I colori sembrano sbagliati (pelle rossastra, cielo arancione) | qualcuno ha toccato `gain` | rimetti `gain` a `1.0` → Apply |
| Immagine decentrata / barra nera | allineamento | regola `shift px` con numeri pari finché non è centrata |
| Discord continua a mostrare 720p | il motore di Discord limita ogni camera a 720p | usa OBS per il 1080p, o la condivisione schermo di Discord (1080p60 con Nitro) |
| OBS vede la camera, Discord no | Discord era già aperto quando hai installato il driver | chiudi Discord COMPLETAMENTE (anche dalla tray) e riaprilo |
| Tutto il resto | — | leggi `pscapture-app.log` (accanto all'exe, o in `%TEMP%`) e il log della console `/data/pscapture.log` (via FTP) |
## 6. Disinstallazione
1. Chiudi l'app.
2. `rundll32 "%ProgramFiles%\PSCapture\pscapture.dll",DllUnregisterServer`
(rimuove la registrazione della camera).
3. Cancella `C:\Program Files\PSCapture`.
4. Opzionale, sulla console: cancella `/data/pldmgr/payloads/pscapture/` così
l'autoloader smette di avviarlo.
+114
View File
@@ -0,0 +1,114 @@
# PSCapture — the complete step-by-step guide
🇮🇹 Versione italiana: [GUIDE.it.md](GUIDE.it.md)
From zero to your PS5 inside Discord. Every step says exactly what you should
see on screen. If something doesn't match, jump to the troubleshooting table
at the bottom.
---
## 0. What you need
**On the console** — all standard in current jailbreak setups:
| Thing | Port | Why |
|---|---|---|
| Payload loader (`elfldr`-style) | TCP **9021** | runs the payload we send |
| Anonymous FTP | TCP **2121** | the app reads/writes files on the console |
| `pldmgr` autoloader (optional) | — | makes the payload start at every boot |
**On the PC**: Windows 10/11 64-bit, wired network recommended.
**The app**: `PSCapture.exe` — **one single file**. Download it from the
repository's Releases page. That's the whole package: the console payload and
the Windows capture-card driver are inside it.
---
## 1. First run on the PC
1. Put `PSCapture.exe` anywhere (the Desktop is fine) and **double-click it**.
2. Windows asks *"Do you want to allow this app to make changes to your
device?"* → click **Yes**.
*Why:* registering the virtual camera for every app on the machine needs
administrator rights, once. The app elevates itself — no right-click needed.
3. The window opens (dark, with a big blue **START STREAMING** button).
If your PS5 is not at `192.168.0.36`: type its address in the **PS5 IP**
field and press **Apply**.
4. From here the app does everything alone. Watch the dark log pane:
| You should read | Meaning |
|---|---|
| `driver gia' installato` | camera already registered — fine |
| `driver estratto dall'eseguibile (… byte)` + `driver registrato - la webcam 'PS5 Capture' e' pronta` | first run: driver installed |
| `payload dall'eseguibile (… byte)` + `payload avviato - la PS5 deve mostrare la notifica PSCapture` | the payload is now running on the console |
| `porta stream: 5599` then `collegato a 192.168.0.36:5599` | stream connected |
| status pill turns **green**: `STREAMING - scegli 'PS5 Capture' in Discord` | you're live |
5. The PS5 shows a system notification: **PSCapture is streaming on:PORT**.
(That toast is the console telling you the payload is alive.)
> If the pill stays on *"IN ATTESA DELLA PS5 — accesa e col jailbreak avviato?"*
> the console is off, not jailbroken, or the IP is wrong. See section 5.
## 2. Use it
**Discord (in a call)** — *Settings → Voice & Video → Camera* → **PS5 Capture**.
The in-call image is 720p: that's Discord's engine, it asks *every* camera
(capture cards included) for 1280x720.
**OBS** — *Sources → **+** → Video Capture Device* → **PS5 Capture**.
This runs at the full **1920x1080**.
**ffmpeg** — `ffmpeg -f dshow -i video="PS5 Capture" capture.mkv`
## 3. Daily use
- Open `PSCapture.exe` → **Yes** to the admin prompt. The app reconnects by
itself: if the payload is already on the console (autoloader) the stream
starts by itself; if the console is off the pill says
*"IN ATTESA DELLA PS5"* and connects the moment the console is ready.
- Press **STOP** (or close the app) to stop streaming. The camera goes black /
freezes on the last frame; consumers keep the device listed.
- **Deploy payload** re-sends the payload by hand (useful after a console
reboot without autoloader). **Install driver** re-registers the camera by
hand (useful if the log shows a driver error).
## 4. The two knobs
| Knob | What it does | Rules |
|---|---|---|
| `gain` | chroma (color) strength | `1.0` = raw, the correct default. Only touch it if colors look wrong. |
| `shift px` | moves the image left/right | **even numbers only** (an odd number inverts the colors). Tune until the picture is centered; the console's title text is a good anchor. |
Both apply live (about a second) and are saved in `pscapture.ini` next to the
exe, so they survive restarts.
## 5. Troubleshooting — symptom by symptom
| Symptom | Cause | Fix |
|---|---|---|
| Pill stays `AVVIO...` / `IN ATTESA DELLA PS5` | console off, **not jailbroken**, or wrong IP | turn the console on, run the jailbreak (ports 9021/2121 must be up), check the IP field |
| Log repeats `PS5 non raggiungibile (...)`, riprova | same as above | same |
| No `PS5 Capture` camera appears in Discord/OBS | driver never registered (install failed) | press **Install driver**; read the log line after it — it now reports the *real* error |
| Log: `ERRORE: driver non scrivibile in C:\Program Files\PSCapture (serve admin?)` | app not elevated | close it, restart `PSCapture.exe` and accept the UAC prompt |
| Log: `ERRORE caricamento dll (err=126)` | antivirus quarantined the extracted dll | check the antivirus quarantine for `pscapture.dll`, allow it, press **Install driver** again |
| Camera listed but the image is black | stream not running | check the pill; press **Deploy payload** and/or **START STREAMING** |
| Colors look wrong (skin reddish, sky orange) | `gain` was changed | set `gain` back to `1.0` → Apply |
| Image has a black bar / is off-center | alignment | tune `shift px` (even values) until centered |
| Discord keeps showing 720p | Discord's engine caps every camera at 720p | use OBS for 1080p, or Discord screen-share (1080p60 with Nitro) |
| OBS shows the camera, Discord doesn't | Discord was already open when the driver was installed | fully quit Discord (also from the tray) and reopen it |
| Everything else | — | read `pscapture-app.log` (next to the exe, or in `%TEMP%`) and the console log `/data/pscapture.log` (FTP) |
## 6. Uninstall
1. Close the app.
2. `rundll32 "%ProgramFiles%\PSCapture\pscapture.dll",DllUnregisterServer`
(removes the camera registration).
3. Delete `C:\Program Files\PSCapture`.
4. Optional, on the console: delete `/data/pldmgr/payloads/pscapture/` so the
autoloader stops starting it.
+13 -13
View File
@@ -31,23 +31,23 @@ the driver is considered installed and this step is skipped.
## Step 2 — Installing the driver (first run only)
1. Creates `%ProgramFiles%\PSCapture`.
2. Copies `pscapture.dll` there — the DirectShow source filter. (The app also
copies itself, so the installed folder is self-contained.)
3. Registers the filter by running:
2. **Extracts `pscapture.dll` from its own resources** (the driver travels
embedded inside the executable, resource `RCDATA` n.3) and writes it
there. It also copies itself, so the installed folder is self-contained
— the distributed installer is literally **one file**.
3. Registers the filter by loading the dll into the app itself and calling
`DllRegisterServer` directly. *(Earlier builds shelled out to `rundll32`,
which turned a missing dll into a cryptic "the specified module could not
be found" dialog; the in-process call returns real error codes.)* Every
step is checked: if the dll could not be written (not elevated, no
rights), the app says so instead of attempting a doomed registration.
```
rundll32.exe "%ProgramFiles%\PSCapture\pscapture.dll",DllRegisterServer
```
`DllRegisterServer` writes the COM registration for the filter CLSID,
DllRegisterServer writes the COM registration for the filter CLSID,
including the **DevicePath** entry that modern capture consumers
(Chromium/Discord media engine among them) use to pick the device.
`DllUnregisterServer` removes it — that is the uninstall path today
DllUnregisterServer removes it - that is the uninstall path today
(delete the folder + call it).
After this step the system has a camera called **PS5 Capture**. It will show
a black frame until a stream arrives — the feeder is what fills it.
## Step 3 — Deploying the console payload
The payload (`pscapture.elf`) ships **inside the executable** as a Windows
@@ -82,7 +82,7 @@ reconnects automatically the next time you press **Start**.
| Where | What |
|---|---|
| `%ProgramFiles%\PSCapture\` | `pscapture.exe`, `pscapture.dll` |
| `%ProgramFiles%\PSCapture\` | `pscapture.exe` (self-contained) + the `pscapture.dll` it extracts |
| Registry `HKCR\CLSID\{50533543-…-192080102030}` | the capture-card filter registration |
| Console `/data/pldmgr/payloads/pscapture/pscapture.elf` | payload (autoloader) |
| Console `/data/pscapture.port` | stream port announced by the payload |
+420 -175
View File
@@ -1,199 +1,444 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PSCapture by fstat13 — PS5 screen as a capture card on PC</title>
<style>
:root{
--bg:#0d1015; --card:#151a22; --card2:#1b222d; --line:#242c38;
--fg:#e8edf4; --dim:#96a0b0; --accent:#0070d1; --accent2:#35a3ff;
--rec:#e22236; --ok:#1fbf6b;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--fg);
font:16px/1.65 "Segoe UI",system-ui,-apple-system,Roboto,sans-serif;}
a{color:var(--accent2);text-decoration:none}
a:hover{text-decoration:underline}
.wrap{max-width:880px;margin:0 auto;padding:0 20px}
header{background:
radial-gradient(900px 320px at 20% -40%, rgba(0,112,209,.35), transparent 60%),
radial-gradient(700px 300px at 90% -30%, rgba(226,34,54,.22), transparent 60%),
var(--bg);
border-bottom:1px solid var(--line);padding:56px 0 40px}
.brand{display:flex;align-items:center;gap:18px}
.brand img{width:84px;height:84px;border-radius:20px;box-shadow:0 8px 30px rgba(0,0,0,.5)}
h1{font-size:44px;margin:0;letter-spacing:.5px}
.tag{color:var(--dim);font-size:18px;margin-top:6px}
.badges{margin-top:22px;display:flex;gap:10px;flex-wrap:wrap}
.badge{border:1px solid var(--line);background:var(--card);
padding:5px 12px;border-radius:999px;font-size:13px;color:var(--dim)}
.badge b{color:var(--fg)}
h2{margin:52px 0 14px;font-size:26px}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px}
.card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:18px 20px}
.card h3{margin:0 0 8px;font-size:17px}
.card p{margin:0;color:var(--dim);font-size:14.5px}
.flow{display:flex;gap:10px;align-items:stretch;flex-wrap:wrap;margin-top:16px}
.box{flex:1;min-width:180px;background:var(--card2);border:1px solid var(--line);
border-radius:12px;padding:14px 16px}
.box b{display:block;margin-bottom:6px}
.box div{color:var(--dim);font-size:13.5px}
.arrow{align-self:center;color:var(--accent2);font-size:22px;padding:0 2px}
ol.steps{counter-reset:s;list-style:none;padding:0;margin:18px 0 0}
ol.steps li{counter-increment:s;position:relative;padding:0 0 18px 54px}
ol.steps li::before{content:counter(s);position:absolute;left:0;top:0;
width:34px;height:34px;border-radius:10px;background:var(--accent);
color:#fff;font-weight:600;display:flex;align-items:center;justify-content:center}
ol.steps b{color:var(--fg)}
ol.steps span{color:var(--dim)}
table{width:100%;border-collapse:collapse;margin-top:14px;font-size:14.5px}
th,td{text-align:left;padding:10px 12px;border-bottom:1px solid var(--line)}
th{color:var(--dim);font-weight:500}
code,kbd{background:#0a0d12;border:1px solid var(--line);border-radius:6px;
padding:1px 7px;font:13.5px/1.5 Consolas,monospace;color:#cfe3ff}
details{background:var(--card);border:1px solid var(--line);border-radius:12px;
padding:14px 18px;margin:10px 0}
summary{cursor:pointer;font-weight:600}
details p{color:var(--dim);margin:10px 0 0}
.note{background:linear-gradient(0deg,rgba(0,112,209,.08),rgba(0,112,209,.08)),
var(--card);border:1px solid #23405f;border-radius:12px;padding:14px 18px;color:#cfe3ff}
footer{color:var(--dim);border-top:1px solid var(--line);margin-top:70px;
padding:26px 0 40px;font-size:13.5px}
.dot{display:inline-block;width:9px;height:9px;border-radius:50%;
background:var(--rec);margin-right:8px;vertical-align:1px}
.ok{color:var(--ok);font-weight:600}
</style>
<title>PSCapture — your PS5 screen as a capture card on PC</title>
<meta name="description" content="PSCapture turns a jailbroken PS5 into a capture card for your PC: the console screen streams over your LAN and shows up in Discord, OBS and ffmpeg as a camera called “PS5 Capture”.">
<meta name="theme-color" content="#07090d">
<link rel="icon" type="image/png" href="pscapture.png">
<link rel="alternate" hreflang="en" href="https://fstat13.github.io/PSCapture/">
<link rel="alternate" hreflang="it" href="https://fstat13.github.io/PSCapture/index.it.html">
<meta property="og:type" content="website">
<meta property="og:title" content="PSCapture — your PS5 screen, live on your PC">
<meta property="og:description" content="The PS5 screen shows up in Discord, OBS and ffmpeg as the camera “PS5 Capture”. No capture card, no Remote Play, no cloud.">
<meta property="og:image" content="https://fstat13.github.io/PSCapture/social-preview.png">
<meta property="og:image:width" content="1280">
<meta property="og:image:height" content="640">
<meta property="og:url" content="https://fstat13.github.io/PSCapture/">
<meta name="twitter:card" content="summary_large_image">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<symbol id="i-download" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></symbol>
<symbol id="i-star" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></symbol>
<symbol id="i-check" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></symbol>
<symbol id="i-video" viewBox="0 0 24 24"><path d="m22 8-6 4 6 4V8Z"/><rect x="2" y="6" width="14" height="12" rx="2"/></symbol>
<symbol id="i-zap" viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></symbol>
<symbol id="i-file" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></symbol>
<symbol id="i-refresh" viewBox="0 0 24 24"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></symbol>
<symbol id="i-sliders" viewBox="0 0 24 24"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></symbol>
<symbol id="i-lock" viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></symbol>
<symbol id="i-console" viewBox="0 0 24 24"><rect x="2" y="6" width="20" height="12" rx="3"/><line x1="6" y1="12" x2="10" y2="12"/><line x1="8" y1="10" x2="8" y2="14"/><circle cx="16" cy="11" r=".6"/><circle cx="18" cy="13" r=".6"/></symbol>
<symbol id="i-monitor" viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></symbol>
<symbol id="i-info" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></symbol>
<symbol id="i-alert" viewBox="0 0 24 24"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></symbol>
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></symbol>
</svg>
<nav class="nav">
<div class="wrap">
<div class="brand">
<img src="pscapture.png" alt="PSCapture">
<div>
<h1>PSCapture</h1>
<div class="tag">The PlayStation&nbsp;5 screen, live on your PC — as a real capture card.</div>
<a class="logo" href="#top"><img src="pscapture.png" alt="">PSCapture</a>
<div class="nav-links">
<a href="#how">How it works</a>
<a href="#start">Setup</a>
<a href="#trouble">Troubleshooting</a>
<a href="#build">Build</a>
<a href="#faq">FAQ</a>
</div>
<div class="nav-right">
<a class="lang" href="index.it.html" hreflang="it">Italiano</a>
<a class="btn btn-ghost btn-sm" href="https://github.com/fstat13/PSCapture"><svg class="i"><use href="#i-star"/></svg>Star <span class="stars" data-stars hidden></span></a>
</div>
</div>
</nav>
<header class="hero" id="top">
<div class="wrap hero-grid">
<div>
<span class="eyebrow"><span class="tagv">v0.1</span>Tested on PS5 firmware 13.60</span>
<h1>Your PS5 screen,<br><span class="grad">live on your PC.</span></h1>
<p class="lead">PSCapture turns a jailbroken PlayStation&nbsp;5 into a capture card.
The console streams its own screen over your LAN, and Windows sees it as a
camera called <b>PS5 Capture</b>, ready for Discord, OBS and ffmpeg.</p>
<div class="cta">
<a class="btn btn-primary" href="https://github.com/fstat13/PSCapture/releases"><svg class="i"><use href="#i-download"/></svg>Download for Windows</a>
<a class="btn btn-ghost" href="https://github.com/fstat13/PSCapture"><svg class="i"><use href="#i-star"/></svg>Star on GitHub <span class="stars" data-stars hidden></span></a>
</div>
<div class="hero-meta">
<span><svg class="i"><use href="#i-check"/></svg>No capture card</span>
<span><svg class="i"><use href="#i-check"/></svg>No Remote Play</span>
<span><svg class="i"><use href="#i-check"/></svg>No cloud</span>
<span><svg class="i"><use href="#i-check"/></svg>One .exe</span>
</div>
</div>
<div class="badges">
<span class="badge"><b>PS5</b> jailbroken</span>
<span class="badge"><b>Windows</b> 10 / 11</span>
<span class="badge"><span class="dot"></span><b>No</b> capture card</span>
<span class="badge"><b>No</b> Remote&nbsp;Play</span>
<span class="badge">1080p &middot; ~30 fps</span>
<div class="app" role="img" aria-label="The PSCapture window: the log shows the driver check, the payload being sent and the stream connecting; the status pill reads STREAMING.">
<div class="app-bar">
<img src="pscapture.png" alt="">
<span class="t">PSCapture by fstat13 - PS5 screen to PC</span>
<span class="w"><span>–</span><span>□</span><span>✕</span></span>
</div>
<div class="app-body">
<div class="app-head">
<div class="app-title">PSCapture<small>PS5 screen to PC</small></div>
<span class="pill live"><i></i> STREAMING 29.8 fps</span>
</div>
<div class="app-row">
<span class="app-lbl">PS5 IP</span><span class="app-in">192.168.0.36</span>
<span class="app-btn">Deploy payload</span><span class="app-btn">Install driver</span>
</div>
<div class="app-row">
<span class="app-btn big">START STREAMING</span><span class="app-btn stop">STOP</span>
<span class="app-knobs app-row" style="margin:0">
<span class="app-lbl">gain</span><span class="app-in">1.0</span>
<span class="app-lbl">shift px</span><span class="app-in">0</span>
</span>
</div>
<div class="app-log" aria-hidden="true">
<div>PSCapture pronto: premo Start e faccio tutto da solo.</div>
<div>--- AVVIO AUTOMATICO ---</div>
<div>driver gia' installato</div>
<div>payload dall'eseguibile (175480 byte)</div>
<div>payload 175480 byte, invio a 192.168.0.36</div>
<div>autoloader aggiornato (pldmgr)</div>
<div class="hl">payload avviato - la PS5 deve mostrare la notifica PSCapture</div>
<div>porta stream: 5599</div>
<div class="ok">collegato a 192.168.0.36:5599</div>
</div>
</div>
</div>
</div>
<div class="wrap">
<div class="stats">
<div class="stat"><div class="n">1920×1080</div><div class="l">full HD in OBS</div></div>
<div class="stat"><div class="n">~30 fps</div><div class="l">over a plain LAN</div></div>
<div class="stat"><div class="n">0</div><div class="l">re-encodes on the console</div></div>
<div class="stat"><div class="n">1 file</div><div class="l">driver and payload inside</div></div>
</div>
</div>
</header>
<div class="wrap">
<main class="wrap">
<h2>What it is</h2>
<div class="cards">
<div class="card">
<h3>🎥 Virtual capture card</h3>
<p>PSCapture registers a DirectShow device named <b>PS5 Capture</b>.
Discord, OBS and ffmpeg see it like an Elgato — pick it and go live.</p>
<section id="why" class="reveal">
<div class="sec-head">
<div class="kicker">Why PSCapture</div>
<h2>A capture card, without the capture card.</h2>
<p class="sub">Everything runs on hardware you already own: the console, your network and your PC.</p>
</div>
<div class="card">
<h3>⚡ Straight from the GPU</h3>
<p>The console payload reads the display engine's scan-out buffer directly
and streams NV12 frames over your LAN. No re-encode delay, no cloud.</p>
<div class="grid g3">
<div class="card"><div class="ico"><svg class="i"><use href="#i-video"/></svg></div>
<h3>A real camera device</h3>
<p>A DirectShow source filter named <b>PS5 Capture</b>. Any app that can open a webcam can open your console.</p></div>
<div class="card"><div class="ico v"><svg class="i"><use href="#i-zap"/></svg></div>
<h3>Straight from the display engine</h3>
<p>The payload reads the console's scan-out buffer and sends raw NV12 frames. Nothing is re-encoded, so nothing waits for an encoder.</p></div>
<div class="card"><div class="ico g"><svg class="i"><use href="#i-file"/></svg></div>
<h3>One file, one window</h3>
<p><b>PSCapture.exe</b> installs the driver, sends the payload, registers it in the autoloader and connects, all on its own.</p></div>
<div class="card"><div class="ico a"><svg class="i"><use href="#i-refresh"/></svg></div>
<h3>Recovers by itself</h3>
<p>Console still off? It retries every 2 seconds. Stream dropped? It reconnects. You don't have to babysit it.</p></div>
<div class="card"><div class="ico"><svg class="i"><use href="#i-sliders"/></svg></div>
<h3>Live tuning</h3>
<p>Colour <code>gain</code> and horizontal <code>shift</code> apply in about a second and are saved in <code>pscapture.ini</code>.</p></div>
<div class="card"><div class="ico v"><svg class="i"><use href="#i-lock"/></svg></div>
<h3>Stays on your network</h3>
<p>No accounts, no cloud relay, no Remote Play. Frames go from the console to your PC and nowhere else.</p></div>
</div>
<div class="card">
<h3>🖱️ One executable</h3>
<p><b>PSCapture.exe</b> carries the console payload inside itself:
install the driver, push the payload, start streaming — one window,
one click.</p>
</section>
<section id="how" class="reveal">
<div class="sec-head">
<div class="kicker">How it works</div>
<h2>Three small pieces, one camera.</h2>
<p class="sub">A payload on the console, a feeder and a driver on the PC. The apps you already use just see a webcam.</p>
</div>
<div class="pipe">
<div class="node"><div class="where">PS5</div><h3>Payload</h3>
<p>Reads the display engine's scan-out ring, builds NV12 frames and serves them on TCP <code>5599</code>. Pops the toast <i>“PSCapture is streaming on:5599”</i>.</p></div>
<div class="link"><span>TCP · NV12<br>your LAN</span><div class="line"></div></div>
<div class="node"><div class="where">PC · PSCapture.exe</div><h3>Feeder</h3>
<p>Receives the frames and publishes them into a shared-memory ring.</p></div>
<div class="link"><span>shared<br>memory</span><div class="line"></div></div>
<div class="node"><div class="where">PC · pscapture.dll</div><h3>Driver</h3>
<p>A DirectShow source filter that exposes the ring as the camera <b>PS5 Capture</b>.</p></div>
<div class="link"><span>DirectShow</span><div class="line"></div></div>
<div class="node end"><div class="where">Your apps</div><h3>Any camera app</h3>
<p>Pick <b>PS5 Capture</b> like any webcam.</p>
<div class="chips"><span class="chip">Discord</span><span class="chip">OBS</span><span class="chip">ffmpeg</span></div></div>
</div>
</section>
<section id="requirements" class="reveal">
<div class="sec-head">
<div class="kicker">Before you start</div>
<h2>What you need.</h2>
</div>
<div class="grid g2">
<div class="req">
<div class="req-h"><div class="ico"><svg class="i"><use href="#i-console"/></svg></div><h3>On the PS5</h3></div>
<ul>
<li><svg class="i"><use href="#i-check"/></svg><span>A jailbroken console<small>Built and tuned on firmware <b>13.60</b>. See the FAQ for other versions.</small></span></li>
<li><svg class="i"><use href="#i-check"/></svg><span>Payload loader on TCP <code>9021</code><small><code>elfldr</code>-style, as in every current jailbreak setup.</small></span></li>
<li><svg class="i"><use href="#i-check"/></svg><span>Anonymous FTP on TCP <code>2121</code><small>The app reads and writes a few files on the console.</small></span></li>
<li class="opt"><svg class="i"><use href="#i-check"/></svg><span><code>pldmgr</code> autoloader <span class="tagopt">optional</span><small>Starts the payload on every boot, so you never deploy by hand.</small></span></li>
</ul>
</div>
<div class="req">
<div class="req-h"><div class="ico v"><svg class="i"><use href="#i-monitor"/></svg></div><h3>On the PC</h3></div>
<ul>
<li><svg class="i"><use href="#i-check"/></svg><span>Windows 10 or 11, 64-bit<small>The camera is a DirectShow device, so the PC side is Windows-only.</small></span></li>
<li><svg class="i"><use href="#i-check"/></svg><span>Same network as the console<small>Wired recommended. Gigabit is plenty for 1080p at 30 fps.</small></span></li>
<li><svg class="i"><use href="#i-check"/></svg><span>Admin rights, once<small>To register the camera for every program on the machine.</small></span></li>
<li><svg class="i"><use href="#i-check"/></svg><span><b>PSCapture.exe</b> from <a href="https://github.com/fstat13/PSCapture/releases">Releases</a><small>One single file: the payload and the driver are inside it.</small></span></li>
</ul>
</div>
</div>
</section>
<section id="start" class="reveal">
<div class="sec-head">
<div class="kicker">Setup</div>
<h2>From zero to live in five steps.</h2>
<p class="sub">After step 2 you mostly watch: the app does the rest and tells you what it's doing.</p>
</div>
<ol class="steps">
<li><h3>Jailbreak the console</h3>
<p>Run your usual jailbreak so the payload loader (<code>9021</code>) and FTP (<code>2121</code>) are up.</p></li>
<li><h3>Double-click <code>PSCapture.exe</code></h3>
<p>Windows asks <i>“Do you want to allow this app to make changes to your device?”</i>. Click <b>Yes</b>. The app elevates itself, no right-click needed.</p></li>
<li><h3>Check the IP</h3>
<p>The default is <code>192.168.0.36</code>. If your PS5 has a different address, type it in <b>PS5 IP</b> and press <b>Apply</b>. It's remembered. You can find it on the console under <i>Settings → Network → Connection Status → View Connection Status</i>.</p></li>
<li><h3>Hands off</h3>
<p>On the first run the app installs the driver, then sends the payload and connects. The console shows <i>“PSCapture is streaming on:5599”</i> and the status pill turns <b style="color:var(--green)">green: STREAMING</b>.</p></li>
<li><h3>Pick <b>PS5 Capture</b> in your app</h3>
<p>Discord, OBS or ffmpeg. See <a href="#use">where to find it</a> below.</p></li>
</ol>
<h3 style="margin:30px 0 6px">Reading the log</h3>
<p style="color:var(--dim)">The app's log is in Italian. Here's what each line means, in the order you'll see them.</p>
<div class="term" style="margin-top:14px">
<div class="term-bar"><i></i><i></i><i></i><b>pscapture-app.log</b></div>
<div class="term-row"><code>driver gia' installato</code><span class="m">The camera is already registered. Nothing to do.</span></div>
<div class="term-row"><code>driver non installato: lo installo ora (primo avvio)<br>driver estratto dall'eseguibile (… byte)<br>driver registrato - la webcam 'PS5 Capture' e' pronta</code><span class="m">First run: the driver is unpacked from the exe and registered.</span></div>
<div class="term-row"><code>payload dall'eseguibile (… byte)<br>payload … byte, invio a 192.168.0.36</code><span class="m">The payload is on its way to the console.</span></div>
<div class="term-row"><code>autoloader aggiornato (pldmgr)</code><span class="m">The payload was copied to the autoloader, so it'll start on every boot.</span></div>
<div class="term-row"><code>payload avviato - la PS5 deve mostrare la notifica PSCapture</code><span class="m">The payload is running. Look for the toast on the TV.</span></div>
<div class="term-row ok"><code>porta stream: 5599<br>collegato a 192.168.0.36:5599</code><span class="m">Connected. The pill reads <b>STREAMING</b> with the live fps.</span></div>
</div>
<div class="callout warn" style="margin-top:18px">
<svg class="i"><use href="#i-alert"/></svg>
<p><b>Pill stuck on “IN ATTESA DELLA PS5”</b> (“waiting for the PS5”)? The console is off, not jailbroken yet, or the IP is wrong. You don't need to restart anything: the app retries every 2 seconds and connects the moment ports <code>9021</code> and <code>2121</code> answer.</p>
</div>
</section>
<section id="use" class="reveal">
<div class="sec-head">
<div class="kicker">Use it</div>
<h2>Works wherever a webcam works.</h2>
</div>
<div class="grid g3">
<div class="card target"><h3>Discord</h3>
<div class="path"><i>Settings → Voice &amp; Video → Camera</i> → <b>PS5 Capture</b></div>
<p style="margin-top:10px">Discord asks every camera for 720p, capture cards included. For 1080p60 use screen-share (Nitro).</p>
<div class="res">in calls <b>1280×720</b></div></div>
<div class="card target"><h3>OBS Studio</h3>
<div class="path"><i>Sources → + → Video Capture Device</i> → <b>PS5 Capture</b></div>
<p style="margin-top:10px">Full resolution, ready for recording or streaming.</p>
<div class="res">native <b>1920×1080</b></div></div>
<div class="card target"><h3>ffmpeg</h3>
<div class="path"><code>ffmpeg -f dshow -i video="PS5 Capture" capture.mkv</code></div>
<p style="margin-top:10px">Scripted captures and quick tests.</p>
<div class="res">native <b>1920×1080</b></div></div>
</div>
</section>
<section id="daily" class="reveal">
<div class="sec-head">
<div class="kicker">Daily use</div>
<h2>Open it and play.</h2>
</div>
<div class="grid g3">
<div class="card knob"><span class="k">every day</span>
<p>Open <b>PSCapture.exe</b> and accept the UAC prompt. With the autoloader the stream starts by itself.</p>
<ul>
<li><b>Deploy payload</b>: re-sends the payload by hand, after a reboot without autoloader.</li>
<li><b>Install driver</b>: re-registers the camera if the log shows a driver error.</li>
<li><b>STOP</b> or closing the app: the camera freezes on the last frame and stays listed.</li>
</ul></div>
<div class="card knob"><span class="k">gain</span>
<p>Colour (chroma) strength.</p>
<ul><li><code>1.0</code> is raw and correct.</li><li>Only touch it if colours look off.</li></ul></div>
<div class="card knob"><span class="k">shift px</span>
<p>Moves the picture left or right.</p>
<ul><li><b>Even numbers only</b>: an odd value swaps the colours.</li><li>The console's title text makes a good reference.</li></ul></div>
</div>
</section>
<section id="trouble" class="reveal">
<div class="sec-head">
<div class="kicker">Troubleshooting</div>
<h2>Symptom → fix.</h2>
<p class="sub">Type what you see, or paste a line from the log.</p>
</div>
<div class="search"><svg class="i"><use href="#i-search"/></svg>
<input id="q" type="search" placeholder="e.g. black, 720p, err=126, ATTESA…" autocomplete="off" aria-label="Filter problems"></div>
<div class="issues">
<div class="issue"><div class="sym">Pill stays on <code>AVVIO…</code> or <code>IN ATTESA DELLA PS5</code></div>
<div class="fix"><span class="cause">console off, not jailbroken, or wrong IP</span>Turn the console on, run the jailbreak (ports 9021 and 2121 must be up), check the <b>PS5 IP</b> field. The app keeps retrying.</div></div>
<div class="issue"><div class="sym">Log repeats <code>PS5 non raggiungibile (…), riprovo</code></div>
<div class="fix"><span class="cause">console not reachable</span>Same as above. It connects by itself as soon as the console answers.</div></div>
<div class="issue"><div class="sym">Log: <code>elfldr :9021 non raggiungibile</code></div>
<div class="fix"><span class="cause">payload loader not running</span>Run the jailbreak again, then press <b>Deploy payload</b>.</div></div>
<div class="issue"><div class="sym">No <b>PS5 Capture</b> camera in Discord or OBS</div>
<div class="fix"><span class="cause">driver not registered</span>Press <b>Install driver</b> and read the next log line: it reports the real error.</div></div>
<div class="issue"><div class="sym">Log: <code>ERRORE: driver non scrivibile in C:\Program Files\PSCapture (serve admin?)</code></div>
<div class="fix"><span class="cause">app not elevated</span>Close it, start <code>PSCapture.exe</code> again and answer <b>Yes</b> to the UAC prompt.</div></div>
<div class="issue"><div class="sym">Log: <code>elevazione rifiutata: continuo senza privilegi</code></div>
<div class="fix"><span class="cause">UAC prompt answered “No”</span>Streaming can still work, but the driver can't be installed. Restart the app and click <b>Yes</b>.</div></div>
<div class="issue"><div class="sym">Log: <code>ERRORE caricamento dll (err=126)</code></div>
<div class="fix"><span class="cause">antivirus quarantined the driver</span>Restore and allow <code>pscapture.dll</code> in your antivirus, then press <b>Install driver</b> again.</div></div>
<div class="issue"><div class="sym">Log: <code>autoloader: copia FALLITA (pldmgr c'e'?)</code></div>
<div class="fix"><span class="cause"><code>pldmgr</code> not installed</span>Harmless: streaming works now. Without the autoloader, press <b>Deploy payload</b> after every console reboot.</div></div>
<div class="issue"><div class="sym">Camera is listed but the image is black</div>
<div class="fix"><span class="cause">stream not running</span>Check the pill. Press <b>Deploy payload</b> and/or <b>START STREAMING</b>.</div></div>
<div class="issue"><div class="sym">Log: <code>stream caduto, riconnetto</code></div>
<div class="fix"><span class="cause">network hiccup or console went to rest mode</span>The app reconnects by itself. If it happens often, use a cable instead of Wi-Fi.</div></div>
<div class="issue"><div class="sym">Colours look wrong (reddish skin, orange sky)</div>
<div class="fix"><span class="cause"><code>gain</code> was changed</span>Set <code>gain</code> back to <code>1.0</code> and press <b>Apply</b>.</div></div>
<div class="issue"><div class="sym">Colours inverted, black bar, picture off-centre</div>
<div class="fix"><span class="cause">alignment</span>Tune <code>shift px</code> using <b>even</b> values only. An odd value swaps the colours.</div></div>
<div class="issue"><div class="sym">Discord shows only 720p</div>
<div class="fix"><span class="cause">Discord's limit, not PSCapture's</span>Discord caps every camera at 720p in calls. Use OBS for 1080p, or Discord screen-share (1080p60 with Nitro).</div></div>
<div class="issue"><div class="sym">OBS sees the camera, Discord doesn't</div>
<div class="fix"><span class="cause">Discord was open during the driver install</span>Quit Discord completely (tray icon too) and reopen it.</div></div>
<div class="issue"><div class="sym">Anything else</div>
<div class="fix"><span class="cause">read the logs</span><code>pscapture-app.log</code> (next to the exe, or in <code>%TEMP%</code>) and the console log <code>/data/pscapture.log</code> over FTP. Then <a href="https://github.com/fstat13/PSCapture/issues">open an issue</a> with both.</div></div>
<div class="noresult" hidden>No match. <a href="https://github.com/fstat13/PSCapture/issues">Open an issue</a> with your <code>pscapture-app.log</code>.</div>
</div>
</section>
<section id="build" class="reveal">
<div class="sec-head">
<div class="kicker">Build from source</div>
<h2>Two halves, two toolchains.</h2>
<p class="sub">The repository holds sources only; ready-made binaries are on <a href="https://github.com/fstat13/PSCapture/releases">Releases</a>. The full walkthrough (toolchain install, checks, pitfalls) is in <a href="https://github.com/fstat13/PSCapture/blob/main/docs/BUILD.md">BUILD.md</a>.</p>
</div>
<div class="build-step">
<div class="num">Step 1 · console</div>
<h3>Payload → <code>pscapture.elf</code></h3>
<p>Needs Docker and the PS5 payload SDK image (prospero-clang; any tag works, here <code>ps5sdk:18</code>).</p>
<div class="codeblock"><pre><span class="p">$ </span>docker run --rm -v "$PWD/payload:/payload" -w /payload ps5sdk:18 make</pre></div>
</div>
<div class="build-step">
<div class="num">Step 2 · PC</div>
<h3>App and driver → <code>pscapture.exe</code> + <code>pscapture.dll</code></h3>
<p>Needs MinGW-w64 (<code>gcc</code> + <code>windres</code>). <b>The order matters</b>: the <code>.res</code> embeds the driver and the payload exactly as they are on disk.</p>
<div class="codeblock"><pre><span class="c"># from the repo root</span>
cp payload/pscapture.elf pc/
cd pc
<span class="c"># 1) the capture-card driver (DirectShow source filter)</span>
gcc -O2 -shared -static -static-libgcc -o pscapture.dll ps5cam.c \
-lole32 -loleaut32 -luser32 -ladvapi32 -lws2_32
<span class="c"># 2) bundle icon + payload + driver into the resource object</span>
windres pscapture.rc -O coff -o pscapture.res
<span class="c"># 3) the single-file app</span>
gcc -O2 -mwindows -o pscapture.exe pscapture.c pscapture.res \
-lws2_32 -lshell32 -ladvapi32 -luxtheme</pre></div>
</div>
<div class="callout info" style="margin-top:22px">
<svg class="i"><use href="#i-info"/></svg>
<p><b>The two classic mistakes.</b> Rebuilding the DLL or the payload <i>after</i> <code>windres</code> leaves the old ones inside the exe, so always go DLL → windres → exe. And <code>-static -static-libgcc</code> on the DLL is mandatory: without it Windows may refuse to load the driver with “the specified module could not be found” (missing MinGW runtime).</p>
</div>
</section>
<section id="faq" class="reveal">
<div class="sec-head">
<div class="kicker">FAQ</div>
<h2>Questions.</h2>
</div>
<div class="faq">
<details><summary>Does it work on firmwares other than 13.60?</summary>
<div class="body"><p>The PC side works with any firmware: the app and the driver only use TCP, FTP and shared memory. The console payload is the version-specific part. It was built and tuned on <b>13.60</b> using display-engine anchors found on that firmware. Other versions may need a small porting pass to find the same anchors. <a href="https://github.com/fstat13/PSCapture/issues">Open an issue</a> with your firmware version.</p></div></details>
<details><summary>Why is the camera 720p in Discord calls?</summary>
<div class="body"><p>That's Discord's voice engine: it requests 1280×720 from every camera. In OBS, PS5 Capture runs at the full 1920×1080. Discord's screen-share goes up to 1080p60 with Nitro.</p></div></details>
<details><summary>Do I need a capture card or Remote Play?</summary>
<div class="body"><p>No. PSCapture never touches Remote Play. The payload reads the display engine on the console itself and sends raw frames over your LAN.</p></div></details>
<details><summary>Does it capture game audio?</summary>
<div class="body"><p>Not yet. PSCapture is video-only for now.</p></div></details>
<details><summary>Does it work on macOS or Linux?</summary>
<div class="body"><p>The PC side is Windows-only, because the camera is a DirectShow device.</p></div></details>
<details><summary>How do I uninstall it?</summary>
<div class="body"><ol>
<li>Close the app.</li>
<li>Run <code>rundll32 "%ProgramFiles%\PSCapture\pscapture.dll",DllUnregisterServer</code> to remove the camera.</li>
<li>Delete <code>C:\Program Files\PSCapture</code>.</li>
<li>Optional, on the console: delete <code>/data/pldmgr/payloads/pscapture/</code> so the autoloader stops starting it.</li>
</ol></div></details>
</div>
</section>
<div class="band reveal">
<div>
<h2>Useful? Leave a star.</h2>
<p>PSCapture is free. A star on GitHub helps other PS5 homebrew users find it.</p>
</div>
<div class="cta">
<a class="btn btn-primary" href="https://github.com/fstat13/PSCapture"><svg class="i"><use href="#i-star"/></svg>Star on GitHub <span class="stars" data-stars hidden></span></a>
<a class="btn btn-ghost" href="https://github.com/fstat13/PSCapture/issues">Report an issue</a>
</div>
</div>
</main>
<h2>How it works</h2>
<div class="flow">
<div class="box"><b>PS5 &middot; payload</b>
<div>reads the DCE scan-out ring, builds NV12 frames, serves them on a TCP
port, pops a toast: <i>“PSCapture is streaming on:PORT”</i>.</div></div>
<div class="arrow">→</div>
<div class="box"><b>PC &middot; feeder</b>
<div>receives frames and publishes them into a shared-memory ring.</div></div>
<div class="arrow">→</div>
<div class="box"><b>PC &middot; driver</b>
<div>DirectShow filter exposes the ring as a camera every app can use.</div></div>
<footer>
<div class="wrap">
<div class="foot">
<div>
<a class="logo" href="#top"><img src="pscapture.png" alt="">PSCapture</a>
<p>Your PS5 screen anywhere on your LAN. No card, no cloud.<br>by <b>fstat13</b></p>
</div>
<div>
<h4>Guides</h4>
<ul>
<li><a href="https://github.com/fstat13/PSCapture/blob/main/docs/GUIDE.md">Step-by-step guide</a></li>
<li><a href="https://github.com/fstat13/PSCapture/blob/main/docs/BUILD.md">Build from source</a></li>
<li><a href="https://github.com/fstat13/PSCapture/blob/main/docs/INSTALLER.md">What the installer does</a></li>
<li><a href="https://github.com/fstat13/PSCapture/blob/main/docs/REVERSE_ENGINEERING.md">Reverse engineering</a></li>
<li><a href="https://github.com/fstat13/PSCapture/blob/main/docs/ENGINEERING_LOG.md">Engineering log</a></li>
</ul>
</div>
<div>
<h4>Project</h4>
<ul>
<li><a href="https://github.com/fstat13/PSCapture">GitHub</a></li>
<li><a href="https://github.com/fstat13/PSCapture/releases">Releases</a></li>
<li><a href="https://github.com/fstat13/PSCapture/issues">Issues</a></li>
<li><a href="https://github.com/fstat13/PSCapture/blob/main/LICENSE">License</a></li>
</ul>
</div>
</div>
<div class="legal">
<span>© 2026 fstat13 · source-available</span>
<span>Not affiliated with Sony Interactive Entertainment. “PlayStation” and “PS5” are trademarks of their respective owners.</span>
</div>
</div>
</footer>
<h2>Quick start</h2>
<ol class="steps">
<li><b>Jailbreak your PS5</b><br>
<span>Any kernel-capable jailbreak that gives you payloads
(<code>elfldr</code> on port 9021, anonymous FTP on 2121 and the
<code>pldmgr</code> autoloader).</span></li>
<li><b>Run <code>PSCapture.exe</code></b><br>
<span>Windows asks for administrator once. Type the PS5 IP address if it
is not the default <code>192.168.0.36</code>.</span></li>
<li><b>Press <i>Start</i></b><br>
<span>The app installs the virtual capture driver (first run), sends the
payload to the console, registers it in the autoloader and starts the
stream. The PS5 shows the streaming notification.</span></li>
<li><b>Discord / OBS / anything</b><br>
<span>Choose the camera <b>PS5 Capture</b>. Done — that is your PS5 on
the other side.</span></li>
</ol>
<div class="note" style="margin-top:22px">
<b>Tuning:</b> the <code>gain</code> field adjusts chroma intensity and
<code>shift&nbsp;px</code> fine-aligns the image horizontally (both live,
saved in <code>pscapture.ini</code> next to the exe).
</div>
<h2>Requirements</h2>
<table>
<tr><th>Where</th><th>What</th></tr>
<tr><td>PS5</td><td>Jailbroken (payload loader + FTP + pldmgr autoloader)</td></tr>
<tr><td>PS5</td><td>Latest tested firmware: <b>13.60</b> — see the FAQ for other versions</td></tr>
<tr><td>PC</td><td>Windows 10/11 64-bit, wired network recommended</td></tr>
<tr><td>Network</td><td>Both on the same LAN (gigabit is plenty for 1080p30)</td></tr>
</table>
<h2>FAQ</h2>
<details>
<summary>Does it work on firmware versions older than 13.60?</summary>
<p>The PC side works on every firmware: the app and the driver only speak
TCP/FTP and shared memory. The console payload is the version-specific
part — it was built and tuned on <b>13.60</b>, using display-engine
anchors found on that firmware. On other firmwares it may need a small
porting pass (the fix is finding the same anchors on that system).
If you have another firmware, open an issue with your version and we
can add a profile.</p>
</details>
<details>
<summary>Why is the call camera 720p and not 1080p?</summary>
<p>That cap is Discord's voice engine, not PSCapture: it requests 1280x720
from every camera. For full-quality capture use OBS (where PS5 Capture
runs at 1920x1080) or Discord's screen-share, which goes up to
1080p60 with Nitro.</p>
</details>
<details>
<summary>Is a capture card or Remote Play needed?</summary>
<p>No. PSCapture never touches Remote Play: the payload reads the display
engine on the console itself and streams raw frames over your LAN.</p>
</details>
<details>
<summary>The camera lists but stays black / spinner</summary>
<p>Open the app: the status line shows what is happening — not reachable
IP, payload not running, or feeder reconnecting. Everything is logged
in the window; the console side logs to <code>/data/pscapture.log</code>
over FTP.</p>
</details>
<footer>
PSCapture — bring your PS5 screen anywhere on your LAN, no card, no cloud.
<br><b>by fstat13</b> &middot; source-available. Built for the homebrew community. <span class="ok">Play responsibly.</span>
</footer>
</div>
<script src="main.js"></script>
</body>
</html>
+445
View File
@@ -0,0 +1,445 @@
<!DOCTYPE html>
<html lang="it" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PSCapture — la PS5 come scheda di acquisizione sul PC</title>
<meta name="description" content="PSCapture trasforma una PS5 jailbroken in una scheda di acquisizione per il PC: lo schermo della console passa sulla tua rete locale e compare in Discord, OBS e ffmpeg come la webcam “PS5 Capture”.">
<meta name="theme-color" content="#07090d">
<link rel="icon" type="image/png" href="pscapture.png">
<link rel="alternate" hreflang="en" href="https://fstat13.github.io/PSCapture/">
<link rel="alternate" hreflang="it" href="https://fstat13.github.io/PSCapture/index.it.html">
<meta property="og:type" content="website">
<meta property="og:locale" content="it_IT">
<meta property="og:title" content="PSCapture — lo schermo della PS5, live sul tuo PC">
<meta property="og:description" content="Lo schermo della PS5 compare in Discord, OBS e ffmpeg come la webcam “PS5 Capture”. Niente capture card, niente Remote Play, niente cloud.">
<meta property="og:image" content="https://fstat13.github.io/PSCapture/social-preview.png">
<meta property="og:image:width" content="1280">
<meta property="og:image:height" content="640">
<meta property="og:url" content="https://fstat13.github.io/PSCapture/index.it.html">
<meta name="twitter:card" content="summary_large_image">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<symbol id="i-download" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></symbol>
<symbol id="i-star" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></symbol>
<symbol id="i-check" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></symbol>
<symbol id="i-video" viewBox="0 0 24 24"><path d="m22 8-6 4 6 4V8Z"/><rect x="2" y="6" width="14" height="12" rx="2"/></symbol>
<symbol id="i-zap" viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></symbol>
<symbol id="i-file" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></symbol>
<symbol id="i-refresh" viewBox="0 0 24 24"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></symbol>
<symbol id="i-sliders" viewBox="0 0 24 24"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></symbol>
<symbol id="i-lock" viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></symbol>
<symbol id="i-console" viewBox="0 0 24 24"><rect x="2" y="6" width="20" height="12" rx="3"/><line x1="6" y1="12" x2="10" y2="12"/><line x1="8" y1="10" x2="8" y2="14"/><circle cx="16" cy="11" r=".6"/><circle cx="18" cy="13" r=".6"/></symbol>
<symbol id="i-monitor" viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></symbol>
<symbol id="i-info" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></symbol>
<symbol id="i-alert" viewBox="0 0 24 24"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></symbol>
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></symbol>
</svg>
<nav class="nav">
<div class="wrap">
<a class="logo" href="#top"><img src="pscapture.png" alt="">PSCapture</a>
<div class="nav-links">
<a href="#how">Come funziona</a>
<a href="#start">Installazione</a>
<a href="#trouble">Problemi</a>
<a href="#build">Build</a>
<a href="#faq">FAQ</a>
</div>
<div class="nav-right">
<a class="lang" href="index.html" hreflang="en">English</a>
<a class="btn btn-ghost btn-sm" href="https://github.com/fstat13/PSCapture"><svg class="i"><use href="#i-star"/></svg>Star <span class="stars" data-stars hidden></span></a>
</div>
</div>
</nav>
<header class="hero" id="top">
<div class="wrap hero-grid">
<div>
<span class="eyebrow"><span class="tagv">v0.1</span>Testato sul firmware PS5 13.60</span>
<h1>Lo schermo della PS5,<br><span class="grad">live sul tuo PC.</span></h1>
<p class="lead">PSCapture trasforma una PlayStation&nbsp;5 jailbroken in una scheda di acquisizione.
La console manda il proprio schermo sulla tua rete locale e Windows lo vede come una
webcam chiamata <b>PS5 Capture</b>, pronta per Discord, OBS e ffmpeg.</p>
<div class="cta">
<a class="btn btn-primary" href="https://github.com/fstat13/PSCapture/releases"><svg class="i"><use href="#i-download"/></svg>Scarica per Windows</a>
<a class="btn btn-ghost" href="https://github.com/fstat13/PSCapture"><svg class="i"><use href="#i-star"/></svg>Metti una stella <span class="stars" data-stars hidden></span></a>
</div>
<div class="hero-meta">
<span><svg class="i"><use href="#i-check"/></svg>Niente capture card</span>
<span><svg class="i"><use href="#i-check"/></svg>Niente Remote Play</span>
<span><svg class="i"><use href="#i-check"/></svg>Niente cloud</span>
<span><svg class="i"><use href="#i-check"/></svg>Un solo .exe</span>
</div>
</div>
<div class="app" role="img" aria-label="La finestra di PSCapture: il log mostra il controllo del driver, l'invio del payload e la connessione dello stream; la pillola di stato dice STREAMING.">
<div class="app-bar">
<img src="pscapture.png" alt="">
<span class="t">PSCapture by fstat13 - PS5 screen to PC</span>
<span class="w"><span>–</span><span>□</span><span>✕</span></span>
</div>
<div class="app-body">
<div class="app-head">
<div class="app-title">PSCapture<small>PS5 screen to PC</small></div>
<span class="pill live"><i></i> STREAMING 29.8 fps</span>
</div>
<div class="app-row">
<span class="app-lbl">PS5 IP</span><span class="app-in">192.168.0.36</span>
<span class="app-btn">Deploy payload</span><span class="app-btn">Install driver</span>
</div>
<div class="app-row">
<span class="app-btn big">START STREAMING</span><span class="app-btn stop">STOP</span>
<span class="app-knobs app-row" style="margin:0">
<span class="app-lbl">gain</span><span class="app-in">1.0</span>
<span class="app-lbl">shift px</span><span class="app-in">0</span>
</span>
</div>
<div class="app-log" aria-hidden="true">
<div>PSCapture pronto: premo Start e faccio tutto da solo.</div>
<div>--- AVVIO AUTOMATICO ---</div>
<div>driver gia' installato</div>
<div>payload dall'eseguibile (175480 byte)</div>
<div>payload 175480 byte, invio a 192.168.0.36</div>
<div>autoloader aggiornato (pldmgr)</div>
<div class="hl">payload avviato - la PS5 deve mostrare la notifica PSCapture</div>
<div>porta stream: 5599</div>
<div class="ok">collegato a 192.168.0.36:5599</div>
</div>
</div>
</div>
</div>
<div class="wrap">
<div class="stats">
<div class="stat"><div class="n">1920×1080</div><div class="l">full HD in OBS</div></div>
<div class="stat"><div class="n">~30 fps</div><div class="l">su una normale LAN</div></div>
<div class="stat"><div class="n">0</div><div class="l">ricodifiche sulla console</div></div>
<div class="stat"><div class="n">1 file</div><div class="l">driver e payload inclusi</div></div>
</div>
</div>
</header>
<main class="wrap">
<section id="why" class="reveal">
<div class="sec-head">
<div class="kicker">Perché PSCapture</div>
<h2>Una capture card, senza la capture card.</h2>
<p class="sub">Funziona tutto con quello che hai già: la console, la tua rete e il tuo PC.</p>
</div>
<div class="grid g3">
<div class="card"><div class="ico"><svg class="i"><use href="#i-video"/></svg></div>
<h3>Una vera webcam</h3>
<p>Un filtro sorgente DirectShow chiamato <b>PS5 Capture</b>. Qualsiasi programma che apre una webcam può aprire la tua console.</p></div>
<div class="card"><div class="ico v"><svg class="i"><use href="#i-zap"/></svg></div>
<h3>Direttamente dal display engine</h3>
<p>Il payload legge il buffer di scan-out della console e manda frame NV12 grezzi. Niente ricodifica, quindi niente attese per un encoder.</p></div>
<div class="card"><div class="ico g"><svg class="i"><use href="#i-file"/></svg></div>
<h3>Un file, una finestra</h3>
<p><b>PSCapture.exe</b> installa il driver, manda il payload, lo registra nell'autoloader e si collega. Tutto da solo.</p></div>
<div class="card"><div class="ico a"><svg class="i"><use href="#i-refresh"/></svg></div>
<h3>Si riprende da solo</h3>
<p>Console ancora spenta? Riprova ogni 2 secondi. Stream caduto? Si ricollega. Non devi stargli dietro.</p></div>
<div class="card"><div class="ico"><svg class="i"><use href="#i-sliders"/></svg></div>
<h3>Regolazioni in diretta</h3>
<p><code>gain</code> del colore e <code>shift</code> orizzontale si applicano in circa un secondo e restano salvati in <code>pscapture.ini</code>.</p></div>
<div class="card"><div class="ico v"><svg class="i"><use href="#i-lock"/></svg></div>
<h3>Resta nella tua rete</h3>
<p>Niente account, niente server esterni, niente Remote Play. I frame vanno dalla console al PC e basta.</p></div>
</div>
</section>
<section id="how" class="reveal">
<div class="sec-head">
<div class="kicker">Come funziona</div>
<h2>Tre pezzi piccoli, una webcam.</h2>
<p class="sub">Un payload sulla console, un feeder e un driver sul PC. I programmi che usi già vedono semplicemente una webcam.</p>
</div>
<div class="pipe">
<div class="node"><div class="where">PS5</div><h3>Payload</h3>
<p>Legge il ring di scan-out del display engine, costruisce i frame NV12 e li serve sulla porta TCP <code>5599</code>. Mostra la notifica <i>“PSCapture is streaming on:5599”</i>.</p></div>
<div class="link"><span>TCP · NV12<br>rete locale</span><div class="line"></div></div>
<div class="node"><div class="where">PC · PSCapture.exe</div><h3>Feeder</h3>
<p>Riceve i frame e li pubblica in un ring di memoria condivisa.</p></div>
<div class="link"><span>memoria<br>condivisa</span><div class="line"></div></div>
<div class="node"><div class="where">PC · pscapture.dll</div><h3>Driver</h3>
<p>Un filtro sorgente DirectShow che espone il ring come la webcam <b>PS5 Capture</b>.</p></div>
<div class="link"><span>DirectShow</span><div class="line"></div></div>
<div class="node end"><div class="where">I tuoi programmi</div><h3>Qualsiasi app video</h3>
<p>Scegli <b>PS5 Capture</b> come una webcam qualunque.</p>
<div class="chips"><span class="chip">Discord</span><span class="chip">OBS</span><span class="chip">ffmpeg</span></div></div>
</div>
</section>
<section id="requirements" class="reveal">
<div class="sec-head">
<div class="kicker">Prima di iniziare</div>
<h2>Cosa ti serve.</h2>
</div>
<div class="grid g2">
<div class="req">
<div class="req-h"><div class="ico"><svg class="i"><use href="#i-console"/></svg></div><h3>Sulla PS5</h3></div>
<ul>
<li><svg class="i"><use href="#i-check"/></svg><span>Una console jailbroken<small>Sviluppato e tarato sul firmware <b>13.60</b>. Per le altre versioni vedi la FAQ.</small></span></li>
<li><svg class="i"><use href="#i-check"/></svg><span>Payload loader sulla porta TCP <code>9021</code><small>Tipo <code>elfldr</code>, come in tutti i setup di jailbreak attuali.</small></span></li>
<li><svg class="i"><use href="#i-check"/></svg><span>FTP anonimo sulla porta TCP <code>2121</code><small>L'app legge e scrive alcuni file sulla console.</small></span></li>
<li class="opt"><svg class="i"><use href="#i-check"/></svg><span>Autoloader <code>pldmgr</code> <span class="tagopt">opzionale</span><small>Avvia il payload a ogni accensione, così non devi mai mandarlo a mano.</small></span></li>
</ul>
</div>
<div class="req">
<div class="req-h"><div class="ico v"><svg class="i"><use href="#i-monitor"/></svg></div><h3>Sul PC</h3></div>
<ul>
<li><svg class="i"><use href="#i-check"/></svg><span>Windows 10 o 11, 64 bit<small>La webcam è un dispositivo DirectShow, quindi il lato PC è solo per Windows.</small></span></li>
<li><svg class="i"><use href="#i-check"/></svg><span>Stessa rete della console<small>Meglio via cavo. Il gigabit basta e avanza per 1080p a 30 fps.</small></span></li>
<li><svg class="i"><use href="#i-check"/></svg><span>Permessi di amministratore, una volta<small>Per registrare la webcam per tutti i programmi del PC.</small></span></li>
<li><svg class="i"><use href="#i-check"/></svg><span><b>PSCapture.exe</b> dalle <a href="https://github.com/fstat13/PSCapture/releases">Releases</a><small>Un unico file: payload e driver sono già dentro.</small></span></li>
</ul>
</div>
</div>
</section>
<section id="start" class="reveal">
<div class="sec-head">
<div class="kicker">Installazione</div>
<h2>Da zero a live in cinque passi.</h2>
<p class="sub">Dal passo 2 in poi ti basta guardare: l'app fa il resto e ti dice cosa sta facendo.</p>
</div>
<ol class="steps">
<li><h3>Fai il jailbreak della console</h3>
<p>Avvia il tuo solito jailbreak, così il payload loader (<code>9021</code>) e l'FTP (<code>2121</code>) sono attivi.</p></li>
<li><h3>Doppio clic su <code>PSCapture.exe</code></h3>
<p>Windows chiede <i>“Consentire a questa app di apportare modifiche al dispositivo?”</i>. Clicca <b>Sì</b>. L'app chiede da sola i permessi, non serve il tasto destro.</p></li>
<li><h3>Controlla l'IP</h3>
<p>Quello predefinito è <code>192.168.0.36</code>. Se la tua PS5 ha un altro indirizzo, scrivilo in <b>PS5 IP</b> e premi <b>Apply</b>: viene ricordato. Lo trovi sulla console in <i>Impostazioni → Rete → Stato della connessione → Visualizza stato della connessione</i>.</p></li>
<li><h3>Non toccare niente</h3>
<p>Al primo avvio l'app installa il driver, poi manda il payload e si collega. Sulla console compare <i>“PSCapture is streaming on:5599”</i> e la pillola di stato diventa <b style="color:var(--green)">verde: STREAMING</b>.</p></li>
<li><h3>Scegli <b>PS5 Capture</b> nel tuo programma</h3>
<p>Discord, OBS o ffmpeg. Più sotto trovi <a href="#use">dove trovarla</a>.</p></li>
</ol>
<h3 style="margin:30px 0 6px">Come leggere il log</h3>
<p style="color:var(--dim)">Queste sono le righe che vedrai, nell'ordine in cui compaiono.</p>
<div class="term" style="margin-top:14px">
<div class="term-bar"><i></i><i></i><i></i><b>pscapture-app.log</b></div>
<div class="term-row"><code>driver gia' installato</code><span class="m">La webcam è già registrata. Niente da fare.</span></div>
<div class="term-row"><code>driver non installato: lo installo ora (primo avvio)<br>driver estratto dall'eseguibile (… byte)<br>driver registrato - la webcam 'PS5 Capture' e' pronta</code><span class="m">Primo avvio: il driver viene estratto dall'exe e registrato.</span></div>
<div class="term-row"><code>payload dall'eseguibile (… byte)<br>payload … byte, invio a 192.168.0.36</code><span class="m">Il payload sta andando verso la console.</span></div>
<div class="term-row"><code>autoloader aggiornato (pldmgr)</code><span class="m">Il payload è stato copiato nell'autoloader: partirà a ogni accensione.</span></div>
<div class="term-row"><code>payload avviato - la PS5 deve mostrare la notifica PSCapture</code><span class="m">Il payload gira. Cerca la notifica sulla TV.</span></div>
<div class="term-row ok"><code>porta stream: 5599<br>collegato a 192.168.0.36:5599</code><span class="m">Collegato. La pillola dice <b>STREAMING</b> con gli fps in tempo reale.</span></div>
</div>
<div class="callout warn" style="margin-top:18px">
<svg class="i"><use href="#i-alert"/></svg>
<p><b>Pillola ferma su “IN ATTESA DELLA PS5”?</b> La console è spenta, il jailbreak non è ancora partito o l'IP è sbagliato. Non serve riavviare niente: l'app riprova ogni 2 secondi e si collega appena le porte <code>9021</code> e <code>2121</code> rispondono.</p>
</div>
</section>
<section id="use" class="reveal">
<div class="sec-head">
<div class="kicker">Usalo</div>
<h2>Funziona ovunque funzioni una webcam.</h2>
</div>
<div class="grid g3">
<div class="card target"><h3>Discord</h3>
<div class="path"><i>Impostazioni → Voce e video → Videocamera</i> → <b>PS5 Capture</b></div>
<p style="margin-top:10px">Discord chiede 720p a tutte le webcam, capture card comprese. Per il 1080p60 usa la condivisione schermo (Nitro).</p>
<div class="res">in chiamata <b>1280×720</b></div></div>
<div class="card target"><h3>OBS Studio</h3>
<div class="path"><i>Fonti → + → Dispositivo di acquisizione video</i> → <b>PS5 Capture</b></div>
<p style="margin-top:10px">Risoluzione piena, pronta per registrare o andare in live.</p>
<div class="res">nativo <b>1920×1080</b></div></div>
<div class="card target"><h3>ffmpeg</h3>
<div class="path"><code>ffmpeg -f dshow -i video="PS5 Capture" capture.mkv</code></div>
<p style="margin-top:10px">Per catture da script e test veloci.</p>
<div class="res">nativo <b>1920×1080</b></div></div>
</div>
</section>
<section id="daily" class="reveal">
<div class="sec-head">
<div class="kicker">Uso quotidiano</div>
<h2>Aprilo e gioca.</h2>
</div>
<div class="grid g3">
<div class="card knob"><span class="k">ogni giorno</span>
<p>Apri <b>PSCapture.exe</b> e accetta il prompt UAC. Con l'autoloader lo stream parte da solo.</p>
<ul>
<li><b>Deploy payload</b>: rimanda il payload a mano, dopo un riavvio senza autoloader.</li>
<li><b>Install driver</b>: registra di nuovo la webcam se il log mostra un errore del driver.</li>
<li><b>STOP</b> o chiudere l'app: la webcam si ferma sull'ultimo frame e resta nell'elenco.</li>
</ul></div>
<div class="card knob"><span class="k">gain</span>
<p>Intensità del colore (croma).</p>
<ul><li><code>1.0</code> è il valore grezzo e corretto.</li><li>Cambialo solo se i colori sembrano sbagliati.</li></ul></div>
<div class="card knob"><span class="k">shift px</span>
<p>Sposta l'immagine a destra o a sinistra.</p>
<ul><li><b>Solo numeri pari</b>: un valore dispari scambia i colori.</li><li>Le scritte dei titoli della console sono un buon riferimento.</li></ul></div>
</div>
</section>
<section id="trouble" class="reveal">
<div class="sec-head">
<div class="kicker">Problemi</div>
<h2>Sintomo → rimedio.</h2>
<p class="sub">Scrivi cosa vedi, oppure incolla una riga del log.</p>
</div>
<div class="search"><svg class="i"><use href="#i-search"/></svg>
<input id="q" type="search" placeholder="es. nera, 720p, err=126, ATTESA…" autocomplete="off" aria-label="Filtra i problemi"></div>
<div class="issues">
<div class="issue"><div class="sym">La pillola resta su <code>AVVIO…</code> o <code>IN ATTESA DELLA PS5</code></div>
<div class="fix"><span class="cause">console spenta, senza jailbreak o IP sbagliato</span>Accendi la console, avvia il jailbreak (le porte 9021 e 2121 devono essere attive), controlla il campo <b>PS5 IP</b>. L'app continua a riprovare.</div></div>
<div class="issue"><div class="sym">Il log ripete <code>PS5 non raggiungibile (…), riprovo</code></div>
<div class="fix"><span class="cause">console non raggiungibile</span>Come sopra. Si collega da sola appena la console risponde.</div></div>
<div class="issue"><div class="sym">Log: <code>elfldr :9021 non raggiungibile</code></div>
<div class="fix"><span class="cause">payload loader non attivo</span>Rilancia il jailbreak, poi premi <b>Deploy payload</b>.</div></div>
<div class="issue"><div class="sym">La webcam <b>PS5 Capture</b> non compare in Discord o OBS</div>
<div class="fix"><span class="cause">driver non registrato</span>Premi <b>Install driver</b> e leggi la riga successiva del log: riporta l'errore vero.</div></div>
<div class="issue"><div class="sym">Log: <code>ERRORE: driver non scrivibile in C:\Program Files\PSCapture (serve admin?)</code></div>
<div class="fix"><span class="cause">app senza permessi di amministratore</span>Chiudila, riapri <code>PSCapture.exe</code> e rispondi <b>Sì</b> al prompt UAC.</div></div>
<div class="issue"><div class="sym">Log: <code>elevazione rifiutata: continuo senza privilegi</code></div>
<div class="fix"><span class="cause">al prompt UAC hai risposto “No”</span>Lo stream può funzionare lo stesso, ma il driver non si installa. Riavvia l'app e clicca <b>Sì</b>.</div></div>
<div class="issue"><div class="sym">Log: <code>ERRORE caricamento dll (err=126)</code></div>
<div class="fix"><span class="cause">l'antivirus ha messo in quarantena il driver</span>Ripristina e consenti <code>pscapture.dll</code> nell'antivirus, poi premi di nuovo <b>Install driver</b>.</div></div>
<div class="issue"><div class="sym">Log: <code>autoloader: copia FALLITA (pldmgr c'e'?)</code></div>
<div class="fix"><span class="cause"><code>pldmgr</code> non installato</span>Nessun danno: lo stream funziona. Senza autoloader, però, dopo ogni riavvio della console devi premere <b>Deploy payload</b>.</div></div>
<div class="issue"><div class="sym">La webcam compare ma l'immagine è nera</div>
<div class="fix"><span class="cause">stream non partito</span>Guarda la pillola. Premi <b>Deploy payload</b> e/o <b>START STREAMING</b>.</div></div>
<div class="issue"><div class="sym">Log: <code>stream caduto, riconnetto</code></div>
<div class="fix"><span class="cause">problema di rete o console in modalità riposo</span>L'app si ricollega da sola. Se succede spesso, passa dal Wi-Fi al cavo.</div></div>
<div class="issue"><div class="sym">I colori sembrano sbagliati (pelle rossastra, cielo arancione)</div>
<div class="fix"><span class="cause">hai cambiato <code>gain</code></span>Rimetti <code>gain</code> a <code>1.0</code> e premi <b>Apply</b>.</div></div>
<div class="issue"><div class="sym">Colori invertiti, barra nera, immagine decentrata</div>
<div class="fix"><span class="cause">allineamento</span>Regola <code>shift px</code> usando solo valori <b>pari</b>. Un valore dispari scambia i colori.</div></div>
<div class="issue"><div class="sym">Discord mostra solo 720p</div>
<div class="fix"><span class="cause">limite di Discord, non di PSCapture</span>In chiamata Discord limita ogni webcam a 720p. Usa OBS per il 1080p, o la condivisione schermo di Discord (1080p60 con Nitro).</div></div>
<div class="issue"><div class="sym">OBS vede la webcam, Discord no</div>
<div class="fix"><span class="cause">Discord era aperto durante l'installazione del driver</span>Chiudi Discord del tutto (anche dall'icona nella tray) e riaprilo.</div></div>
<div class="issue"><div class="sym">Tutto il resto</div>
<div class="fix"><span class="cause">leggi i log</span><code>pscapture-app.log</code> (accanto all'exe o in <code>%TEMP%</code>) e il log della console <code>/data/pscapture.log</code> via FTP. Poi <a href="https://github.com/fstat13/PSCapture/issues">apri una issue</a> allegandoli.</div></div>
<div class="noresult" hidden>Nessun risultato. <a href="https://github.com/fstat13/PSCapture/issues">Apri una issue</a> con il tuo <code>pscapture-app.log</code>.</div>
</div>
</section>
<section id="build" class="reveal">
<div class="sec-head">
<div class="kicker">Build dai sorgenti</div>
<h2>Due metà, due toolchain.</h2>
<p class="sub">La repo contiene solo i sorgenti; i binari pronti sono nelle <a href="https://github.com/fstat13/PSCapture/releases">Releases</a>. La guida completa (installazione toolchain, verifiche, trappole) è in <a href="https://github.com/fstat13/PSCapture/blob/main/docs/BUILD.it.md">BUILD.it.md</a>.</p>
</div>
<div class="build-step">
<div class="num">Passo 1 · console</div>
<h3>Payload → <code>pscapture.elf</code></h3>
<p>Servono Docker e l'immagine del PS5 payload SDK (prospero-clang; va bene qualsiasi tag, qui <code>ps5sdk:18</code>).</p>
<div class="codeblock"><pre><span class="p">$ </span>docker run --rm -v "$PWD/payload:/payload" -w /payload ps5sdk:18 make</pre></div>
</div>
<div class="build-step">
<div class="num">Passo 2 · PC</div>
<h3>App e driver → <code>pscapture.exe</code> + <code>pscapture.dll</code></h3>
<p>Serve MinGW-w64 (<code>gcc</code> + <code>windres</code>). <b>L'ordine conta</b>: il <code>.res</code> incorpora driver e payload esattamente come sono su disco.</p>
<div class="codeblock"><pre><span class="c"># dalla radice della repo</span>
cp payload/pscapture.elf pc/
cd pc
<span class="c"># 1) il driver della capture card (filtro sorgente DirectShow)</span>
gcc -O2 -shared -static -static-libgcc -o pscapture.dll ps5cam.c \
-lole32 -loleaut32 -luser32 -ladvapi32 -lws2_32
<span class="c"># 2) icona + payload + driver nell'oggetto risorse</span>
windres pscapture.rc -O coff -o pscapture.res
<span class="c"># 3) l'app in un unico file</span>
gcc -O2 -mwindows -o pscapture.exe pscapture.c pscapture.res \
-lws2_32 -lshell32 -ladvapi32 -luxtheme</pre></div>
</div>
<div class="callout info" style="margin-top:22px">
<svg class="i"><use href="#i-info"/></svg>
<p><b>I due errori classici.</b> Se ricompili la DLL o il payload <i>dopo</i> <code>windres</code>, l'exe si porta dentro quelli vecchi: l'ordine è sempre DLL → windres → exe. E <code>-static -static-libgcc</code> sulla DLL è obbligatorio: senza, Windows può rifiutarsi di caricare il driver con “impossibile trovare il modulo specificato” (manca il runtime MinGW).</p>
</div>
</section>
<section id="faq" class="reveal">
<div class="sec-head">
<div class="kicker">FAQ</div>
<h2>Domande.</h2>
</div>
<div class="faq">
<details><summary>Funziona su firmware diversi dal 13.60?</summary>
<div class="body"><p>Il lato PC funziona con qualsiasi firmware: app e driver usano solo TCP, FTP e memoria condivisa. La parte legata alla versione è il payload della console, sviluppato e tarato sul <b>13.60</b> usando gli ancoraggi del display engine di quel firmware. Su altre versioni può servire un piccolo porting per ritrovare gli stessi ancoraggi. <a href="https://github.com/fstat13/PSCapture/issues">Apri una issue</a> indicando il tuo firmware.</p></div></details>
<details><summary>Perché in chiamata su Discord la webcam è a 720p?</summary>
<div class="body"><p>È il motore vocale di Discord: chiede 1280×720 a ogni webcam. In OBS, PS5 Capture va a 1920×1080 pieni. La condivisione schermo di Discord arriva a 1080p60 con Nitro.</p></div></details>
<details><summary>Servono una capture card o il Remote Play?</summary>
<div class="body"><p>No. PSCapture non usa mai il Remote Play: il payload legge il display engine direttamente sulla console e manda i frame grezzi sulla tua rete locale.</p></div></details>
<details><summary>Cattura anche l'audio del gioco?</summary>
<div class="body"><p>Non ancora. Per ora PSCapture trasmette solo il video.</p></div></details>
<details><summary>Funziona su macOS o Linux?</summary>
<div class="body"><p>Il lato PC è solo per Windows, perché la webcam è un dispositivo DirectShow.</p></div></details>
<details><summary>Come si disinstalla?</summary>
<div class="body"><ol>
<li>Chiudi l'app.</li>
<li>Esegui <code>rundll32 "%ProgramFiles%\PSCapture\pscapture.dll",DllUnregisterServer</code> per rimuovere la webcam.</li>
<li>Cancella <code>C:\Program Files\PSCapture</code>.</li>
<li>Facoltativo, sulla console: cancella <code>/data/pldmgr/payloads/pscapture/</code> così l'autoloader smette di avviarlo.</li>
</ol></div></details>
</div>
</section>
<div class="band reveal">
<div>
<h2>Ti è utile? Lascia una stella.</h2>
<p>PSCapture è gratuito. Una stella su GitHub aiuta altri appassionati di homebrew PS5 a trovarlo.</p>
</div>
<div class="cta">
<a class="btn btn-primary" href="https://github.com/fstat13/PSCapture"><svg class="i"><use href="#i-star"/></svg>Metti una stella <span class="stars" data-stars hidden></span></a>
<a class="btn btn-ghost" href="https://github.com/fstat13/PSCapture/issues">Segnala un problema</a>
</div>
</div>
</main>
<footer>
<div class="wrap">
<div class="foot">
<div>
<a class="logo" href="#top"><img src="pscapture.png" alt="">PSCapture</a>
<p>Lo schermo della PS5 ovunque nella tua rete. Senza card, senza cloud.<br>di <b>fstat13</b></p>
</div>
<div>
<h4>Guide</h4>
<ul>
<li><a href="https://github.com/fstat13/PSCapture/blob/main/docs/GUIDE.it.md">Guida passo-passo</a></li>
<li><a href="https://github.com/fstat13/PSCapture/blob/main/docs/BUILD.it.md">Build dai sorgenti</a></li>
<li><a href="https://github.com/fstat13/PSCapture/blob/main/docs/INSTALLER.md">Cosa fa l'installer (EN)</a></li>
<li><a href="https://github.com/fstat13/PSCapture/blob/main/docs/REVERSE_ENGINEERING.md">Reverse engineering (EN)</a></li>
<li><a href="https://github.com/fstat13/PSCapture/blob/main/docs/ENGINEERING_LOG.md">Engineering log (EN)</a></li>
</ul>
</div>
<div>
<h4>Progetto</h4>
<ul>
<li><a href="https://github.com/fstat13/PSCapture">GitHub</a></li>
<li><a href="https://github.com/fstat13/PSCapture/releases">Releases</a></li>
<li><a href="https://github.com/fstat13/PSCapture/issues">Issues</a></li>
<li><a href="https://github.com/fstat13/PSCapture/blob/main/LICENSE">Licenza</a></li>
</ul>
</div>
</div>
<div class="legal">
<span>© 2026 fstat13 · source-available</span>
<span>Non affiliato a Sony Interactive Entertainment. “PlayStation” e “PS5” sono marchi dei rispettivi proprietari.</span>
</div>
</div>
</footer>
<script src="main.js"></script>
</body>
</html>
+107
View File
@@ -0,0 +1,107 @@
/* PSCapture — page behaviour shared by index.html / index.it.html */
(function () {
var doc = document.documentElement;
doc.classList.remove('no-js');
var it = doc.lang === 'it';
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
/* nav border once the page scrolls */
var nav = document.querySelector('.nav');
function onScroll() { nav.classList.toggle('scrolled', window.scrollY > 8); }
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
/* app mock: replay the real start-up log, then flip the pill to STREAMING */
var log = document.querySelector('.app-log');
var pill = document.querySelector('.pill');
if (log && pill) {
var lines = Array.prototype.slice.call(log.children);
var live = function () {
pill.classList.add('live');
pill.lastChild.textContent = ' STREAMING 29.8 fps';
};
/* the HTML ships in the final state, so no-JS and reduced-motion visitors see it as-is */
if (!reduce) {
pill.classList.remove('live');
pill.lastChild.textContent = ' AVVIO...';
lines.forEach(function (l) { l.style.display = 'none'; });
var i = 0;
(function next() {
if (i < lines.length) {
lines[i].style.display = '';
i++;
setTimeout(next, i === 1 ? 500 : 420 + Math.random() * 380);
} else {
live();
}
})();
}
}
/* copy buttons on code blocks (prompts and comments are not copied) */
document.querySelectorAll('.codeblock').forEach(function (block) {
var btn = document.createElement('button');
btn.className = 'copy';
btn.type = 'button';
btn.textContent = it ? 'Copia' : 'Copy';
btn.addEventListener('click', function () {
var pre = block.querySelector('pre').cloneNode(true);
pre.querySelectorAll('.p, .c').forEach(function (n) { n.remove(); });
var text = pre.textContent.split('\n')
.map(function (s) { return s.replace(/\s+$/, ''); })
.filter(function (s) { return s.length; })
.join('\n');
navigator.clipboard.writeText(text).then(function () {
btn.textContent = it ? 'Copiato ✓' : 'Copied ✓';
btn.classList.add('done');
setTimeout(function () {
btn.textContent = it ? 'Copia' : 'Copy';
btn.classList.remove('done');
}, 1600);
});
});
block.appendChild(btn);
});
/* troubleshooting filter */
var q = document.getElementById('q');
if (q) {
var issues = document.querySelectorAll('.issue');
var none = document.querySelector('.noresult');
q.addEventListener('input', function () {
var words = q.value.toLowerCase().trim().split(/\s+/).filter(Boolean);
var shown = 0;
issues.forEach(function (el) {
var txt = el.textContent.toLowerCase();
var ok = words.every(function (w) { return txt.indexOf(w) !== -1; });
el.hidden = !ok;
if (ok) shown++;
});
none.hidden = shown !== 0;
});
}
/* live star count on the GitHub buttons */
fetch('https://api.github.com/repos/fstat13/PSCapture')
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
if (!d || typeof d.stargazers_count !== 'number') return;
document.querySelectorAll('[data-stars]').forEach(function (el) {
el.textContent = d.stargazers_count.toLocaleString(doc.lang);
el.hidden = false;
});
})
.catch(function () {});
/* fade sections in as they scroll into view */
if ('IntersectionObserver' in window && !reduce) {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); }
});
}, { rootMargin: '0px 0px -8% 0px' });
document.querySelectorAll('.reveal').forEach(function (el) { io.observe(el); });
} else {
document.querySelectorAll('.reveal').forEach(function (el) { el.classList.add('in'); });
}
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

+395
View File
@@ -0,0 +1,395 @@
/* PSCapture — shared stylesheet for index.html / index.it.html */
:root{
color-scheme: dark;
--bg:#07090d;
--bg-2:#0b0e14;
--surface:#10141c;
--surface-2:#151b25;
--surface-3:#1b2230;
--line:#1f2733;
--line-2:#2a3445;
--fg:#eef2f8;
--fg-2:#c3cbd8;
--dim:#8a95a8;
--dim-2:#5f6b7f;
--accent:#2986eb; /* same blue as the app's START button */
--accent-2:#5aa8ff;
--accent-glow:rgba(41,134,235,.35);
--violet:#7c5cff;
--green:#22c55e;
--amber:#f59e0b;
--red:#ef4444;
--radius:16px;
--radius-sm:10px;
--maxw:1120px;
--font:"Inter",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
--mono:"JetBrains Mono",ui-monospace,Consolas,"Cascadia Mono",monospace;
}
*,*::before,*::after{box-sizing:border-box}
html{scroll-behavior:smooth;scroll-padding-top:84px;-webkit-text-size-adjust:100%}
body{
margin:0;background:var(--bg);color:var(--fg);
font:16px/1.65 var(--font);
-webkit-font-smoothing:antialiased;
overflow-x:hidden;
}
img{max-width:100%;display:block}
a{color:var(--accent-2);text-decoration:none}
a:hover{text-decoration:underline;text-underline-offset:3px}
b,strong{color:var(--fg);font-weight:600}
::selection{background:var(--accent);color:#fff}
.wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px}
@media (max-width:600px){.wrap{padding:0 16px}}
/* ───────── nav ───────── */
.nav{
position:sticky;top:0;z-index:50;
background:rgba(7,9,13,.72);
backdrop-filter:saturate(160%) blur(14px);
-webkit-backdrop-filter:saturate(160%) blur(14px);
border-bottom:1px solid transparent;
transition:border-color .2s;
}
.nav.scrolled{border-bottom-color:var(--line)}
.nav .wrap{display:flex;align-items:center;gap:20px;height:64px}
.logo{display:flex;align-items:center;gap:10px;color:var(--fg);font-weight:700;letter-spacing:-.01em}
.logo:hover{text-decoration:none}
.logo img{width:30px;height:30px;border-radius:8px}
.nav-links{display:flex;gap:4px;margin-left:auto}
.nav-links a{color:var(--dim);font-size:14px;padding:6px 10px;border-radius:8px}
.nav-links a:hover{color:var(--fg);background:var(--surface);text-decoration:none}
.nav-right{display:flex;align-items:center;gap:8px}
.lang{font-size:13px;color:var(--dim);border:1px solid var(--line-2);border-radius:999px;padding:4px 11px}
.lang:hover{color:var(--fg);text-decoration:none;border-color:var(--dim-2)}
@media (max-width:860px){.nav-links{display:none}.nav-right{margin-left:auto}}
/* ───────── buttons ───────── */
.btn{
display:inline-flex;align-items:center;gap:9px;
font:600 15px/1 var(--font);
padding:13px 20px;border-radius:12px;border:1px solid transparent;
cursor:pointer;transition:transform .15s,background .15s,border-color .15s,box-shadow .15s;
white-space:nowrap;
}
.btn:hover{text-decoration:none;transform:translateY(-1px)}
.btn svg{width:18px;height:18px;flex:none}
.btn-primary{background:var(--accent);color:#fff;box-shadow:0 8px 30px -8px var(--accent-glow),inset 0 1px 0 rgba(255,255,255,.18)}
.btn-primary:hover{background:#3592f5}
.btn-ghost{background:var(--surface);color:var(--fg);border-color:var(--line-2)}
.btn-ghost:hover{border-color:var(--dim-2)}
.btn-sm{padding:8px 13px;font-size:13.5px;border-radius:9px}
/* ───────── hero ───────── */
.hero{position:relative;padding:72px 0 56px;isolation:isolate}
.hero::before{
content:"";position:absolute;inset:-64px 0 0;z-index:-1;
background:
radial-gradient(600px 380px at 12% 8%, rgba(41,134,235,.22), transparent 70%),
radial-gradient(520px 360px at 88% 18%, rgba(124,92,255,.16), transparent 70%);
}
.hero::after{
content:"";position:absolute;inset:-64px 0 0;z-index:-1;
background-image:linear-gradient(var(--line) 1px,transparent 1px),linear-gradient(90deg,var(--line) 1px,transparent 1px);
background-size:56px 56px;opacity:.28;
-webkit-mask-image:radial-gradient(ellipse 70% 60% at 50% 20%,#000 30%,transparent 75%);
mask-image:radial-gradient(ellipse 70% 60% at 50% 20%,#000 30%,transparent 75%);
}
.hero-grid{display:grid;grid-template-columns:1fr 1.08fr;gap:56px;align-items:center}
@media (max-width:960px){.hero-grid{grid-template-columns:1fr;gap:44px}}
.eyebrow{
display:inline-flex;align-items:center;gap:8px;
font-size:13px;color:var(--fg-2);
border:1px solid var(--line-2);background:rgba(16,20,28,.7);
padding:5px 12px 5px 8px;border-radius:999px;
}
.eyebrow .tagv{background:var(--accent);color:#fff;font-weight:700;font-size:11.5px;padding:2px 8px;border-radius:999px}
.hero h1{
font-size:clamp(38px,5.6vw,62px);line-height:1.04;letter-spacing:-.035em;
margin:20px 0 18px;font-weight:800;
}
.grad{background:linear-gradient(100deg,#fff 10%,var(--accent-2) 55%,var(--violet));-webkit-background-clip:text;background-clip:text;color:transparent}
.lead{font-size:18.5px;color:var(--fg-2);margin:0 0 30px;max-width:540px}
.cta{display:flex;gap:12px;flex-wrap:wrap}
.hero-meta{display:flex;flex-wrap:wrap;gap:8px 18px;margin-top:26px;color:var(--dim);font-size:14px}
.hero-meta span{display:inline-flex;align-items:center;gap:7px}
.hero-meta svg{width:16px;height:16px;color:var(--green)}
/* ───────── app mock ───────── */
.app{
position:relative;
background:#10141b;border:1px solid var(--line-2);border-radius:14px;
box-shadow:0 40px 80px -30px rgba(0,0,0,.8),0 0 0 1px rgba(255,255,255,.02),0 0 120px -40px var(--accent-glow);
overflow:hidden;font-size:13px;
}
.app-bar{display:flex;align-items:center;gap:10px;background:#0b0e13;border-bottom:1px solid #202733;padding:10px 14px}
.app-bar img{width:18px;height:18px;border-radius:4px}
.app-bar .t{color:var(--fg-2);font-size:12.5px;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.app-bar .w{display:flex;gap:14px;color:var(--dim-2);font-size:12px}
.app-body{padding:18px 18px 16px}
.app-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:16px}
.app-title{font-weight:700;font-size:17px;letter-spacing:-.01em}
.app-title small{display:block;color:var(--dim);font-weight:400;font-size:12px}
.pill{
display:inline-flex;align-items:center;gap:8px;white-space:nowrap;
background:#1a2130;border:1px solid #273042;border-radius:999px;
padding:6px 12px;font:600 11.5px/1 var(--mono);letter-spacing:.02em;color:var(--amber);
transition:color .3s;
}
.pill i{width:8px;height:8px;border-radius:50%;background:currentColor;box-shadow:0 0 0 0 currentColor;animation:pulse 1.6s infinite}
.pill.live{color:var(--green)}
@keyframes pulse{0%{box-shadow:0 0 0 0 color-mix(in srgb,currentColor 60%,transparent)}70%{box-shadow:0 0 0 7px transparent}100%{box-shadow:0 0 0 0 transparent}}
.app-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:10px}
.app-lbl{color:var(--dim);font-size:12px}
.app-in{background:#0c0f15;border:1px solid #202733;border-radius:7px;padding:6px 10px;color:#cbd3e0;font:12px var(--mono)}
.app-btn{background:#1a2029;border:1px solid #2c3646;border-radius:7px;padding:6px 11px;color:var(--fg-2);font-size:12px}
.app-btn.big{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:700;padding:12px 22px;font-size:13px;letter-spacing:.02em}
.app-btn.stop{padding:12px 18px;font-weight:600}
.app-log{
margin-top:14px;background:#0c0f15;border:1px solid #202733;border-radius:9px;
padding:12px 14px;height:196px;overflow:hidden;
font:11.5px/1.75 var(--mono);color:#aeb8c8;
display:flex;flex-direction:column;justify-content:flex-end;
}
.app-log div{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;animation:logIn .35s ease-out both}
.app-log .ok{color:#7ee2a5}
.app-log .hl{color:#8fc3ff}
@keyframes logIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
@media (max-width:520px){
.app-bar .w{display:none}
.app-knobs{display:none}
}
/* ───────── sections ───────── */
section{padding:84px 0 0}
.sec-head{max-width:720px;margin-bottom:34px}
.kicker{font:600 12.5px/1 var(--mono);letter-spacing:.12em;text-transform:uppercase;color:var(--accent-2);margin-bottom:14px}
h2{font-size:clamp(28px,3.6vw,40px);line-height:1.12;letter-spacing:-.028em;margin:0 0 14px;font-weight:750}
h3{font-size:18px;letter-spacing:-.01em;margin:0 0 8px;font-weight:650}
.sub{color:var(--dim);font-size:17px;margin:0}
p{margin:0 0 14px}
/* stats */
.stats{display:grid;grid-template-columns:repeat(4,1fr);border:1px solid var(--line);border-radius:var(--radius);background:var(--bg-2);overflow:hidden;margin-top:12px}
.stat{padding:22px 24px;border-left:1px solid var(--line)}
.stat:first-child{border-left:0}
.stat .n{font-size:28px;font-weight:750;letter-spacing:-.03em}
.stat .l{color:var(--dim);font-size:13.5px}
@media (max-width:760px){.stats{grid-template-columns:repeat(2,1fr)}.stat:nth-child(3){border-left:0}.stat:nth-child(n+3){border-top:1px solid var(--line)}}
/* feature cards */
.grid{display:grid;gap:16px}
.g3{grid-template-columns:repeat(3,1fr)}
.g2{grid-template-columns:repeat(2,1fr)}
@media (max-width:900px){.g3{grid-template-columns:1fr 1fr}}
@media (max-width:640px){.g3,.g2{grid-template-columns:1fr}}
.card{
position:relative;background:linear-gradient(180deg,var(--surface),var(--bg-2));
border:1px solid var(--line);border-radius:var(--radius);padding:24px;
transition:border-color .2s,transform .2s;
}
.card:hover{border-color:var(--line-2)}
.card p{color:var(--dim);font-size:15px;margin:0}
.ico{
width:42px;height:42px;border-radius:11px;display:grid;place-items:center;margin-bottom:16px;
background:rgba(41,134,235,.12);color:var(--accent-2);border:1px solid rgba(41,134,235,.25);
}
.ico svg{width:21px;height:21px}
.ico.v{background:rgba(124,92,255,.12);color:#a996ff;border-color:rgba(124,92,255,.28)}
.ico.g{background:rgba(34,197,94,.1);color:#5fdc8f;border-color:rgba(34,197,94,.25)}
.ico.a{background:rgba(245,158,11,.1);color:#fbbf4a;border-color:rgba(245,158,11,.25)}
/* pipeline */
.pipe{display:grid;grid-template-columns:1fr auto 1fr auto 1fr auto 1fr;align-items:stretch;gap:0}
.node{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:20px}
.node .where{font:600 11px/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--dim-2);margin-bottom:10px}
.node h3{font-size:16px}
.node p{color:var(--dim);font-size:14px;margin:0}
.node.end{background:linear-gradient(160deg,rgba(41,134,235,.14),var(--surface) 60%);border-color:rgba(41,134,235,.35)}
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px}
.chip{font-size:12px;color:var(--fg-2);background:var(--surface-3);border:1px solid var(--line-2);border-radius:999px;padding:3px 10px}
.link{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:0 10px;color:var(--dim-2);min-width:64px}
.link span{font:500 10.5px/1.2 var(--mono);text-align:center;margin-bottom:6px;color:var(--dim)}
.link .line{position:relative;width:100%;height:2px;background:linear-gradient(90deg,var(--line-2),var(--accent),var(--line-2));background-size:200% 100%;animation:flow 2.2s linear infinite;border-radius:2px}
.link .line::after{content:"";position:absolute;right:-1px;top:-4px;border:5px solid transparent;border-left-color:var(--accent);border-right:0}
@keyframes flow{from{background-position:100% 0}to{background-position:-100% 0}}
@media (max-width:960px){
.pipe{grid-template-columns:1fr}
.link{flex-direction:row;gap:10px;padding:10px 0;min-height:44px}
.link span{margin:0;order:2}
.link .line{width:2px;height:28px;background:linear-gradient(180deg,var(--line-2),var(--accent),var(--line-2));background-size:100% 200%}
.link .line::after{right:-4px;top:auto;bottom:-1px;border:5px solid transparent;border-top-color:var(--accent);border-bottom:0}
}
/* requirements */
.req{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:24px}
.req-h{display:flex;align-items:center;gap:12px;margin-bottom:16px}
.req-h .ico{margin:0}
.req-h h3{margin:0}
.req ul{list-style:none;margin:0;padding:0}
.req li{display:flex;gap:12px;padding:11px 0;border-top:1px solid var(--line);font-size:15px;color:var(--fg-2)}
.req li:first-child{border-top:0;padding-top:0}
.req li svg{width:18px;height:18px;flex:none;margin-top:3px;color:var(--green)}
.req li.opt svg{color:var(--dim-2)}
.req li small{color:var(--dim);display:block;font-size:13.5px}
.tagopt{font:600 10.5px/1 var(--mono);color:var(--dim);border:1px solid var(--line-2);border-radius:5px;padding:2px 6px;margin-left:6px;vertical-align:2px;text-transform:uppercase;letter-spacing:.06em}
/* steps */
.steps{list-style:none;margin:0;padding:0;counter-reset:s;position:relative}
.steps>li{counter-increment:s;position:relative;padding:0 0 30px 68px}
.steps>li::before{
content:counter(s);position:absolute;left:0;top:-2px;
width:44px;height:44px;border-radius:13px;display:grid;place-items:center;
font:700 17px/1 var(--font);color:#fff;
background:linear-gradient(160deg,var(--accent),#1a5fb4);
box-shadow:0 8px 24px -8px var(--accent-glow),inset 0 1px 0 rgba(255,255,255,.2);
}
.steps>li::after{content:"";position:absolute;left:21px;top:50px;bottom:6px;width:2px;background:linear-gradient(var(--line-2),transparent)}
.steps>li:last-child::after{display:none}
.steps h3{margin:6px 0 6px;font-size:18.5px}
.steps p{color:var(--dim);margin:0}
/* callouts */
.callout{display:flex;gap:14px;border-radius:var(--radius);padding:18px 20px;border:1px solid;font-size:15px}
.callout svg{width:20px;height:20px;flex:none;margin-top:2px}
.callout p{margin:0;color:inherit}
.callout.info{background:rgba(41,134,235,.07);border-color:rgba(41,134,235,.3);color:#cfe3ff}
.callout.info svg{color:var(--accent-2)}
.callout.warn{background:rgba(245,158,11,.06);border-color:rgba(245,158,11,.3);color:#f6e2bd}
.callout.warn svg{color:var(--amber)}
/* terminal-style log legend */
.term{background:#0a0d12;border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}
.term-bar{display:flex;align-items:center;gap:8px;padding:11px 16px;border-bottom:1px solid var(--line);background:var(--bg-2);font:12px var(--mono);color:var(--dim)}
.term-bar i{width:10px;height:10px;border-radius:50%;background:var(--line-2)}
.term-bar b{margin-left:8px;font-weight:500;color:var(--dim)}
.term-row{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(0,1fr);gap:20px;padding:14px 18px;border-top:1px solid var(--line)}
.term-row:first-of-type{border-top:0}
.term-row code{background:none;border:0;padding:0;color:#9fd0ff;font-size:13px;display:block;word-break:break-word}
.term-row .m{color:var(--fg-2);font-size:14.5px}
.term-row.ok code{color:#7ee2a5}
@media (max-width:700px){.term-row{grid-template-columns:1fr;gap:6px}}
/* app targets */
.target{display:flex;flex-direction:column}
.target .res{margin-top:auto;padding-top:16px;display:flex;align-items:center;justify-content:space-between;gap:10px;border-top:1px dashed var(--line-2);margin-top:18px;font-size:13.5px;color:var(--dim)}
.target .res b{font:700 15px var(--mono);color:var(--fg)}
.target .path{font-size:14px;color:var(--fg-2);margin-top:6px}
/* knobs */
.knob .k{display:inline-block;font:600 14px var(--mono);color:var(--fg);background:var(--surface-3);border:1px solid var(--line-2);border-radius:7px;padding:3px 9px;margin-bottom:12px}
.knob ul{margin:12px 0 0;padding-left:18px;color:var(--dim);font-size:14.5px}
.knob li{margin:4px 0}
/* troubleshooting */
.search{position:relative;margin-bottom:16px}
.search svg{position:absolute;left:16px;top:50%;transform:translateY(-50%);width:18px;height:18px;color:var(--dim-2)}
.search input{
width:100%;font:15px var(--font);color:var(--fg);
background:var(--surface);border:1px solid var(--line-2);border-radius:12px;
padding:14px 16px 14px 46px;outline:none;transition:border-color .15s,box-shadow .15s;
}
.search input::placeholder{color:var(--dim-2)}
.search input:focus{border-color:var(--accent);box-shadow:0 0 0 4px rgba(41,134,235,.15)}
.issues{display:grid;gap:10px}
.issue{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.15fr);gap:24px;background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:18px 20px}
.issue .sym{font-weight:600;color:var(--fg)}
.issue .sym code{font-weight:400}
.issue .fix{color:var(--dim);font-size:15px}
.issue .fix b{color:var(--fg-2)}
.issue .cause{display:block;font-size:12px;font-family:var(--mono);color:var(--dim-2);text-transform:uppercase;letter-spacing:.08em;margin-bottom:4px}
.issue[hidden]{display:none}
.noresult{color:var(--dim);padding:18px;text-align:center;border:1px dashed var(--line-2);border-radius:14px}
@media (max-width:700px){.issue{grid-template-columns:1fr;gap:10px}}
/* code */
code,kbd{
font:.86em/1.5 var(--mono);color:#cfe3ff;
background:var(--surface-3);border:1px solid var(--line-2);border-radius:6px;padding:1px 6px;
overflow-wrap:anywhere;
}
.codeblock{position:relative;margin:12px 0 0}
.codeblock pre{
margin:0;background:#0a0d12;border:1px solid var(--line);border-radius:14px;
padding:18px 20px;overflow-x:auto;font:13.5px/1.7 var(--mono);color:#d6e4f5;
}
.codeblock pre .c{color:#5d6b80}
.codeblock pre .p{color:var(--accent-2);user-select:none}
.copy{
position:absolute;top:10px;right:10px;
font:500 12px var(--font);color:var(--dim);
background:var(--surface-2);border:1px solid var(--line-2);border-radius:8px;
padding:5px 10px;cursor:pointer;opacity:.85;transition:all .15s;
}
.copy:hover{color:var(--fg);opacity:1}
.copy.done{color:var(--green);border-color:rgba(34,197,94,.4)}
.build-step{margin-top:34px}
.build-step:first-child{margin-top:0}
.build-step .num{font:600 12px var(--mono);color:var(--accent-2);letter-spacing:.08em;text-transform:uppercase}
.build-step h3{margin:6px 0 6px;font-size:20px}
.build-step>p{color:var(--dim);margin:0}
/* faq */
.faq{display:grid;gap:10px}
details{background:var(--surface);border:1px solid var(--line);border-radius:14px;transition:border-color .2s}
details[open]{border-color:var(--line-2)}
summary{
list-style:none;cursor:pointer;padding:18px 54px 18px 20px;position:relative;
font-weight:600;font-size:16px;
}
summary::-webkit-details-marker{display:none}
summary::after{
content:"";position:absolute;right:22px;top:50%;width:9px;height:9px;
border-right:2px solid var(--dim);border-bottom:2px solid var(--dim);
transform:translateY(-70%) rotate(45deg);transition:transform .2s;
}
details[open] summary::after{transform:translateY(-30%) rotate(-135deg)}
details .body{padding:0 20px 18px;color:var(--dim);font-size:15px}
details .body p:last-child{margin:0}
details .body ol{margin:0;padding-left:20px}
details .body li{margin:6px 0}
/* cta band */
.band{
margin-top:96px;position:relative;overflow:hidden;
border:1px solid rgba(41,134,235,.3);border-radius:22px;padding:48px 40px;
background:
radial-gradient(500px 260px at 0% 0%,rgba(41,134,235,.25),transparent 70%),
radial-gradient(500px 260px at 100% 100%,rgba(124,92,255,.18),transparent 70%),
var(--bg-2);
display:flex;align-items:center;justify-content:space-between;gap:28px;flex-wrap:wrap;
}
.band h2{margin:0 0 6px;font-size:clamp(24px,3vw,32px)}
.band p{margin:0;color:var(--fg-2)}
@media (max-width:600px){.band{padding:32px 22px}}
/* footer */
footer{margin-top:80px;border-top:1px solid var(--line);padding:44px 0 56px;color:var(--dim);font-size:14px}
.foot{display:grid;grid-template-columns:1.4fr 1fr 1fr;gap:32px}
@media (max-width:760px){.foot{grid-template-columns:1fr}}
.foot h4{margin:0 0 12px;font-size:12px;font-family:var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--dim-2);font-weight:600}
.foot ul{list-style:none;margin:0;padding:0}
.foot li{margin:7px 0}
.foot a{color:var(--fg-2)}
.foot .logo{margin-bottom:12px}
.legal{margin-top:36px;padding-top:22px;border-top:1px solid var(--line);font-size:12.5px;color:var(--dim-2);display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap}
/* reveal on scroll */
.reveal{opacity:0;transform:translateY(14px);transition:opacity .6s ease,transform .6s ease}
.reveal.in{opacity:1;transform:none}
.no-js .reveal{opacity:1;transform:none}
@media (prefers-reduced-motion:reduce){
html{scroll-behavior:auto}
*,*::before,*::after{animation:none!important;transition:none!important}
.reveal{opacity:1;transform:none}
}
/* star counter inside GitHub buttons */
.stars{font:600 12.5px/1 var(--mono);color:var(--fg-2);background:var(--surface-3);border:1px solid var(--line-2);border-radius:6px;padding:3px 7px;margin-left:2px}
.btn-primary .stars{background:rgba(255,255,255,.16);border-color:rgba(255,255,255,.22);color:#fff}
/* inline svg icons (sprite in each page) */
.i{fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
+12 -1
View File
@@ -2545,11 +2545,22 @@ __declspec(dllexport) HRESULT WINAPI
DllRegisterServer(void)
{
WCHAR dll[MAX_PATH];
WCHAR p[512];
WCHAR guid[64];
HKEY k;
if(!GetModuleFileNameW(g_hinst_ps5cam, dll, MAX_PATH)) return E_FAIL;
registra(HKEY_CURRENT_USER, dll); /* niente admin */
registra(HKEY_LOCAL_MACHINE, dll); /* se siamo admin, meglio */
return S_OK;
/* il risultato si verifica davvero: se almeno la chiave per-utente e'
* leggibile la registrazione e' utilizzabile (HKCR unisce HKCU+HKLM) */
StringFromGUID2(&CLSID_Ps5Cam, guid, 64);
wsprintfW(p, L"Software\\Classes\\CLSID\\%s\\InprocServer32", guid);
if(RegOpenKeyExW(HKEY_CURRENT_USER, p, 0, KEY_READ, &k) == ERROR_SUCCESS) {
RegCloseKey(k);
return S_OK;
}
return E_FAIL;
}
__declspec(dllexport) HRESULT WINAPI
+471 -101
View File
@@ -19,10 +19,12 @@
#include <ws2tcpip.h>
#include <shellapi.h>
#include <windows.h>
#include <uxtheme.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include "shm.h"
@@ -38,9 +40,28 @@ static DWORD WINAPI lavoro_deploy(LPVOID p);
static volatile long g_streaming;
static HANDLE g_thread;
static char g_dir[MAX_PATH];
static HFONT g_font, g_font_b, g_font_t;
static HWND g_h_sub;
static int g_stato_on;
static HFONT g_font, g_font_b, g_font_t, g_font_s, g_font_log;
static HBRUSH g_br_bg, g_br_edit, g_br_pill;
static char g_stato_text[96] = "IDLE";
static int g_stato_level; /* 0 idle, 1 attesa, 2 streaming */
/* palette scura */
#define CL_BG RGB(16, 20, 27)
#define CL_TBAR RGB(11, 14, 19)
#define CL_EDIT RGB(12, 15, 21)
#define CL_LINE RGB(32, 39, 51)
#define CL_TXT RGB(232, 237, 244)
#define CL_DIM RGB(152, 162, 179)
#define CL_ACC RGB(41, 134, 235)
#define CL_ACC_D RGB(28, 100, 180)
#define CL_GHOST RGB(26, 32, 41)
#define CL_GHOST_D RGB(38, 46, 59)
#define CL_GBD RGB(44, 54, 70)
#define CL_GREEN RGB(34, 197, 94)
#define CL_AMBER RGB(245, 158, 11)
#define CL_DIM2 RGB(120, 130, 145)
#define CL_PILL RGB(26, 33, 48)
#define CL_EDTX RGB(203, 211, 224)
static BOOL CALLBACK
set_font_cb(HWND c, LPARAM f)
@@ -97,12 +118,45 @@ logUI(const char *fmt, ...)
va_start(ap, fmt);
vsnprintf(b, sizeof(b), fmt, ap);
va_end(ap);
{
char p[MAX_PATH];
FILE *f;
snprintf(p, sizeof(p), "%s\\pscapture-app.log", g_dir);
f = fopen(p, "a");
if(!f) {
char t[MAX_PATH];
int e = errno;
DWORD n = GetTempPathA(sizeof(t), t);
if(n > 0 && n < sizeof(t)) {
snprintf(p, sizeof(p), "%sps5capture-app.log", t);
f = fopen(p, "a");
}
if(f) {
char nota[300];
snprintf(nota, sizeof(nota),
"log accanto all'exe non scrivibile (errno=%d), uso: %s", e, p);
fprintf(f, "%s\n", nota);
}
}
if(f) {
SYSTEMTIME st;
GetLocalTime(&st);
fprintf(f, "[%02d:%02d:%02d] %s\n", st.wHour, st.wMinute,
st.wSecond, b);
fclose(f);
}
}
if(g_hwnd) PostMessageA(g_hwnd, WM_APP_LOG, 0, (LPARAM)_strdup(b));
}
static void
statoUI(const char *s)
{
if(!strcmp(s, g_stato_text)) return;
if(g_hwnd) PostMessageA(g_hwnd, WM_APP_STATO, 0, (LPARAM)_strdup(s));
}
@@ -189,6 +243,22 @@ ftp_cmd(int c, const char *cmd, char *risp, int sz)
return buf[0] == '2' || buf[0] == '3' || buf[0] == '1' ? 0 : -1;
}
/* invio completo: TCP non garantisce che una singola send() trasferisca
* tutti i byte richiesti */
static int
send_all(int s, const char *d, int len)
{
int off = 0;
while(off < len) {
int n = send((SOCKET)s, d + off, len - off, 0);
if(n <= 0) return -1;
off += n;
}
return 0;
}
static int
ftp_leggi_file(const char *ip, const char *file, char *out, int outsz)
{
@@ -197,7 +267,8 @@ ftp_leggi_file(const char *ip, const char *file, char *out, int outsz)
int n;
if(c < 0) return -1;
ftp_cmd(c, "PASV\r\n", buf, sizeof(buf));
memset(buf, 0, sizeof(buf));
if(ftp_cmd(c, "PASV\r\n", buf, sizeof(buf))) goto ko;
{
char *p = strchr(buf, '(');
unsigned h1, h2, h3, h4, p1, p2;
@@ -238,7 +309,8 @@ ftp_manda_file(const char *ip, const char *remoto, const unsigned char *d,
char buf[4096];
if(c < 0) return -1;
ftp_cmd(c, "PASV\r\n", buf, sizeof(buf));
memset(buf, 0, sizeof(buf));
if(ftp_cmd(c, "PASV\r\n", buf, sizeof(buf))) goto ko;
{
char *p = strchr(buf, '(');
unsigned h1, h2, h3, h4, p1, p2;
@@ -256,7 +328,7 @@ ftp_manda_file(const char *ip, const char *remoto, const unsigned char *d,
if(recv((SOCKET)c, buf, sizeof(buf) - 1, 0) <= 0 || buf[0] != '1')
goto ko;
}
if(send((SOCKET)ds, (const char *)d, len, 0) != len) goto ko;
if(send_all(ds, (const char *)d, len)) goto ko;
closesocket((SOCKET)ds);
ds = -1;
recv((SOCKET)c, buf, sizeof(buf) - 1, 0);
@@ -373,6 +445,7 @@ feeder(LPVOID p)
s = connetti(g_ip, port, 8000);
if(s < 0) {
logUI("PS5 non raggiungibile (%s), riprovo", g_ip);
statoUI("IN ATTESA DELLA PS5 - accesa e col jailbreak avviato?");
Sleep(2000);
port = -1;
continue;
@@ -520,7 +593,12 @@ lavoro_deploy(LPVOID p)
free(elf);
return 1;
}
send((SOCKET)s, (const char *)elf, len, 0);
if(send_all(s, (const char *)elf, len)) {
logUI("ERRORE: invio payload all'elfldr incompleto");
closesocket((SOCKET)s);
free(elf);
return 1;
}
/* 3) leggi lo stream di log per qualche secondo */
{
DWORD t0 = GetTickCount();
@@ -539,7 +617,7 @@ lavoro_deploy(LPVOID p)
}
closesocket((SOCKET)s);
free(elf);
logUI("payload avviato — la PS5 deve mostrare la notifica PSCapture");
logUI("payload avviato - la PS5 deve mostrare la notifica PSCapture");
return 0;
}
@@ -547,9 +625,6 @@ static DWORD WINAPI
lavoro_installa(LPVOID p)
{
char dst[MAX_PATH], src[MAX_PATH], cmd[MAX_PATH * 2];
STARTUPINFOA si;
PROCESS_INFORMATION pi;
char win[MAX_PATH];
(void)p;
{
@@ -561,43 +636,87 @@ lavoro_installa(LPVOID p)
CreateDirectoryA(dst, NULL);
logUI("installazione in %s", dst);
/* copia exe (payload embedded) e dll driver (pscapture.dll o ps5cam.dll) */
/* il driver viaggia DENTRO l'eseguibile (risorsa RCDATA n.3): lo si
* estrae e lo si scrive in Program Files; fallback: copia dal disco */
snprintf(cmd, sizeof(cmd), "%s\\pscapture.dll", dst);
{
const char *dlls[] = { "pscapture.dll", "ps5cam.dll", NULL };
int i;
HRSRC r = FindResourceA(NULL, MAKEINTRESOURCEA(3), RT_RCDATA);
int scritto = 0;
for(i = 0; dlls[i]; i++) {
snprintf(src, sizeof(src), "%s\\%s", g_dir, dlls[i]);
if(GetFileAttributesA(src) != INVALID_FILE_ATTRIBUTES) {
snprintf(cmd, sizeof(cmd), "%s\\%s", dst, "pscapture.dll");
CopyFileA(src, cmd, FALSE);
logUI("driver copiato: %s", dlls[i]);
break;
if(r) {
HGLOBAL g = LoadResource(NULL, r);
DWORD n = SizeofResource(NULL, r);
void *sd = g ? LockResource(g) : NULL;
if(sd && n) {
HANDLE h = CreateFileA(cmd, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL, NULL);
DWORD wr = 0;
if(h != INVALID_HANDLE_VALUE) {
if(WriteFile(h, sd, n, &wr, NULL) && wr == n) scritto = 1;
CloseHandle(h);
}
if(scritto) {
logUI("driver estratto dall'eseguibile (%u byte)", (unsigned)n);
} else {
logUI("ERRORE: driver non scrivibile in %s (serve admin?)", dst);
}
}
} else {
const char *dlls[] = { "pscapture.dll", "ps5cam.dll", NULL };
int i;
for(i = 0; dlls[i]; i++) {
snprintf(src, sizeof(src), "%s\\%s", g_dir, dlls[i]);
if(GetFileAttributesA(src) != INVALID_FILE_ATTRIBUTES &&
CopyFileA(src, cmd, FALSE)) {
scritto = 1;
logUI("driver copiato dal disco: %s", dlls[i]);
break;
}
}
}
if(!dlls[i]) logUI("ATTENZIONE: nessuna dll driver accanto all'exe");
{
char self[MAX_PATH];
GetModuleFileNameA(NULL, self, sizeof(self));
snprintf(cmd, sizeof(cmd), "%s\\pscapture.exe", dst);
CopyFileA(self, cmd, FALSE);
logUI("eseguibile installato (payload incluso)");
if(!CopyFileA(self, cmd, FALSE)) {
logUI("eseguibile gia' presente in %s", dst);
} else {
logUI("eseguibile installato (payload e driver inclusi)");
}
snprintf(cmd, sizeof(cmd), "%s\\pscapture.dll", dst);
}
}
/* registra il filtro DirectShow */
snprintf(cmd, sizeof(cmd),
"rundll32.exe \"%s\\pscapture.dll\",DllRegisterServer", dst);
memset(&si, 0, sizeof(si));
si.cb = sizeof(si);
if(CreateProcessA(NULL, cmd, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {
WaitForSingleObject(pi.hProcess, 20000);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
logUI("driver registrato — la webcam 'PS5 Capture' e' pronta");
} else {
logUI("ERRORE registrazione driver (serve admin?)");
/* registra il filtro DirectShow (con codici errore veri, niente
* rundll32: se la dll non si carica lo diciamo e basta) */
if(GetFileAttributesA(cmd) != INVALID_FILE_ATTRIBUTES) {
HMODULE m = LoadLibraryExA(cmd, NULL, LOAD_WITH_ALTERED_SEARCH_PATH);
if(m) {
typedef HRESULT (WINAPI *reg_t)(void);
reg_t fn = (reg_t)(void *)GetProcAddress(m, "DllRegisterServer");
if(fn) {
HRESULT hr = fn();
if(SUCCEEDED(hr)) {
logUI("driver registrato - la webcam 'PS5 Capture' e' pronta");
} else {
logUI("ERRORE registrazione driver (hr=0x%08lx)", (unsigned long)hr);
}
} else {
logUI("ERRORE: DllRegisterServer assente nella dll");
}
FreeLibrary(m);
} else {
logUI("ERRORE caricamento dll (err=%lu)", GetLastError());
}
} else {
logUI("registrazione saltata: dll assente in %s", dst);
}
}
return 0;
}
@@ -658,7 +777,6 @@ WndProc(HWND h, UINT m, WPARAM w, LPARAM l)
case WM_CREATE: {
char b[64];
HINSTANCE hi = g_inst;
HWND c;
g_font = CreateFontA(-15, 0, 0, 0, FW_NORMAL, 0, 0, 0, DEFAULT_CHARSET,
OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
@@ -666,59 +784,66 @@ WndProc(HWND h, UINT m, WPARAM w, LPARAM l)
g_font_b = CreateFontA(-16, 0, 0, 0, FW_SEMIBOLD, 0, 0, 0, DEFAULT_CHARSET,
OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
DEFAULT_PITCH, "Segoe UI");
g_font_t = CreateFontA(-30, 0, 0, 0, FW_SEMIBOLD, 0, 0, 0, DEFAULT_CHARSET,
g_font_t = CreateFontA(-34, 0, 0, 0, FW_SEMIBOLD, 0, 0, 0, DEFAULT_CHARSET,
OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
DEFAULT_PITCH, "Segoe UI");
g_font_s = CreateFontA(-14, 0, 0, 0, FW_NORMAL, 0, 0, 0, DEFAULT_CHARSET,
OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
DEFAULT_PITCH, "Segoe UI");
g_font_log = CreateFontA(-13, 0, 0, 0, FW_NORMAL, 0, 0, 0, DEFAULT_CHARSET,
OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
FIXED_PITCH, "Consolas");
c = CreateWindowA("STATIC", "PSCapture", WS_CHILD | WS_VISIBLE,
20, 14, 400, 42, h, 0, hi, 0);
SendMessageA(c, WM_SETFONT, (WPARAM)g_font_t, 1);
g_h_sub = CreateWindowA("STATIC", "PS5 screen to PC capture card - by fstat13 - v0.1",
WS_CHILD | WS_VISIBLE, 22, 58, 600, 20, h, 0, hi, 0);
g_br_bg = CreateSolidBrush(CL_BG);
g_br_edit = CreateSolidBrush(CL_EDIT);
g_br_pill = CreateSolidBrush(CL_PILL);
CreateWindowA("STATIC", "PS5 IP:", WS_CHILD | WS_VISIBLE,
22, 98, 55, 22, h, 0, hi, 0);
g_h_ip = CreateWindowA("EDIT", "", WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL,
78, 96, 140, 24, h, (HMENU)ID_IP, hi, 0);
/* riga 1: indirizzo + azioni */
CreateWindowA("STATIC", "PS5 IP", WS_CHILD | WS_VISIBLE,
26, 148, 50, 18, h, 0, hi, 0);
g_h_ip = CreateWindowA("EDIT", "", WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL,
82, 142, 150, 30, h, (HMENU)ID_IP, hi, 0);
ini_leggi("IP", "192.168.0.36", b, sizeof(b));
SetWindowTextA(g_h_ip, b);
CreateWindowA("BUTTON", "Deploy payload", WS_CHILD | WS_VISIBLE,
238, 95, 130, 26, h, (HMENU)ID_DEPLOY, hi, 0);
CreateWindowA("BUTTON", "Install driver", WS_CHILD | WS_VISIBLE,
376, 95, 120, 26, h, (HMENU)ID_INSTALL, hi, 0);
c = CreateWindowA("BUTTON", "Start", WS_CHILD | WS_VISIBLE,
504, 95, 92, 26, h, (HMENU)ID_START, hi, 0);
SendMessageA(c, WM_SETFONT, (WPARAM)g_font_b, 1);
c = CreateWindowA("BUTTON", "Stop", WS_CHILD | WS_VISIBLE,
604, 95, 92, 26, h, (HMENU)ID_STOP, hi, 0);
SendMessageA(c, WM_SETFONT, (WPARAM)g_font_b, 1);
CreateWindowA("BUTTON", "", WS_CHILD | WS_VISIBLE | BS_OWNERDRAW,
250, 142, 132, 30, h, (HMENU)ID_DEPLOY, hi, 0);
CreateWindowA("BUTTON", "", WS_CHILD | WS_VISIBLE | BS_OWNERDRAW,
392, 142, 124, 30, h, (HMENU)ID_INSTALL, hi, 0);
CreateWindowA("STATIC", "gain:", WS_CHILD | WS_VISIBLE,
22, 138, 38, 22, h, 0, hi, 0);
g_h_gain = CreateWindowA("EDIT", "1.0", WS_CHILD | WS_VISIBLE | WS_BORDER,
62, 136, 55, 24, h, (HMENU)ID_GAIN, hi, 0);
/* riga 2: start/stop + regolazioni */
CreateWindowA("BUTTON", "", WS_CHILD | WS_VISIBLE | BS_OWNERDRAW,
26, 188, 210, 48, h, (HMENU)ID_START, hi, 0);
CreateWindowA("BUTTON", "", WS_CHILD | WS_VISIBLE | BS_OWNERDRAW,
250, 188, 110, 48, h, (HMENU)ID_STOP, hi, 0);
CreateWindowA("STATIC", "gain", WS_CHILD | WS_VISIBLE,
388, 204, 32, 18, h, 0, hi, 0);
g_h_gain = CreateWindowA("EDIT", "1.0", WS_CHILD | WS_VISIBLE,
424, 199, 58, 26, h, (HMENU)ID_GAIN, hi, 0);
ini_leggi("GAIN", "1.0", b, sizeof(b));
SetWindowTextA(g_h_gain, b);
CreateWindowA("STATIC", "shift px:", WS_CHILD | WS_VISIBLE,
130, 138, 58, 22, h, 0, hi, 0);
g_h_shift = CreateWindowA("EDIT", "0", WS_CHILD | WS_VISIBLE | WS_BORDER,
190, 136, 55, 24, h, (HMENU)ID_SHIFT, hi, 0);
CreateWindowA("STATIC", "shift px", WS_CHILD | WS_VISIBLE,
496, 204, 48, 18, h, 0, hi, 0);
g_h_shift = CreateWindowA("EDIT", "0", WS_CHILD | WS_VISIBLE,
550, 199, 58, 26, h, (HMENU)ID_SHIFT, hi, 0);
ini_leggi("SHIFT", "0", b, sizeof(b));
SetWindowTextA(g_h_shift, b);
CreateWindowA("BUTTON", "Apply", WS_CHILD | WS_VISIBLE,
258, 135, 78, 26, h, (HMENU)ID_APPLY, hi, 0);
CreateWindowA("BUTTON", "", WS_CHILD | WS_VISIBLE | BS_OWNERDRAW,
622, 197, 100, 30, h, (HMENU)ID_APPLY, hi, 0);
g_h_stato = CreateWindowA("STATIC", "IDLE", WS_CHILD | WS_VISIBLE,
24, 176, 660, 24, h, (HMENU)ID_STATO, hi, 0);
g_h_log = CreateWindowA("LISTBOX", "",
WS_CHILD | WS_VISIBLE | WS_BORDER | WS_VSCROLL |
LBS_NOINTEGRALHEIGHT,
22, 210, 664, 320, h, (HMENU)ID_LOG, hi, 0);
/* log */
g_h_log = CreateWindowA("EDIT", "",
WS_CHILD | WS_VISIBLE | WS_VSCROLL |
ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL,
26, 324, 728, 312, h, (HMENU)ID_LOG, hi, 0);
SendMessageA(g_h_log, EM_SETLIMITTEXT, 250000, 0);
SendMessageA(g_h_log, EM_SETMARGINS, EC_LEFTMARGIN | EC_RIGHTMARGIN,
MAKELPARAM(12, 12));
SetWindowTheme(g_h_log, L"DarkMode_Explorer", NULL);
EnumChildWindows(h, set_font_cb, (LPARAM)g_font);
SendMessageA(g_h_sub, WM_SETFONT, (WPARAM)g_font, 1);
SendMessageA(g_h_stato, WM_SETFONT, (WPARAM)g_font_b, 1);
SendMessageA(g_h_log, WM_SETFONT, (WPARAM)g_font_log, 1);
logUI("PSCapture pronto: premo Start e faccio tutto da solo.");
return 0;
@@ -762,30 +887,223 @@ WndProc(HWND h, UINT m, WPARAM w, LPARAM l)
return 0;
}
return 0;
case WM_PAINT: {
PAINTSTRUCT ps;
HDC dc = BeginPaint(h, &ps);
RECT r, rr;
HBRUSH b, ob;
HPEN op;
HFONT old;
GetClientRect(h, &r);
FillRect(dc, &r, g_br_bg);
/* barra titolo con logo */
rr = r;
rr.bottom = 44;
b = CreateSolidBrush(CL_TBAR);
FillRect(dc, &rr, b);
DeleteObject(b);
b = CreateSolidBrush(CL_ACC);
ob = (HBRUSH)SelectObject(dc, b);
op = (HPEN)SelectObject(dc, GetStockObject(NULL_PEN));
RoundRect(dc, 16, 9, 42, 35, 10, 10);
SelectObject(dc, ob);
SelectObject(dc, op);
DeleteObject(b);
{
POINT pt[3];
pt[0].x = 24; pt[0].y = 15;
pt[1].x = 24; pt[1].y = 29;
pt[2].x = 34; pt[2].y = 22;
b = CreateSolidBrush(RGB(255, 255, 255));
ob = (HBRUSH)SelectObject(dc, b);
op = (HPEN)SelectObject(dc, GetStockObject(NULL_PEN));
Polygon(dc, pt, 3);
SelectObject(dc, ob);
SelectObject(dc, op);
DeleteObject(b);
}
SetBkMode(dc, TRANSPARENT);
SetTextColor(dc, CL_TXT);
old = SelectObject(dc, g_font_b);
{
RECT t = {52, 12, 300, 34};
DrawTextA(dc, "PSCapture", -1, &t, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
}
SelectObject(dc, old);
{
HPEN pn = CreatePen(PS_SOLID, 2, CL_DIM);
int W = r.right;
op = (HPEN)SelectObject(dc, pn);
MoveToEx(dc, W - 72, 22, NULL);
LineTo(dc, W - 58, 22);
MoveToEx(dc, W - 34, 16, NULL);
LineTo(dc, W - 20, 30);
MoveToEx(dc, W - 34, 30, NULL);
LineTo(dc, W - 20, 16);
SelectObject(dc, op);
DeleteObject(pn);
}
/* hero */
SetTextColor(dc, CL_TXT);
old = SelectObject(dc, g_font_t);
{
RECT t = {26, 50, 700, 92};
DrawTextA(dc, "PSCapture", -1, &t, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
}
SelectObject(dc, old);
old = SelectObject(dc, g_font_s);
SetTextColor(dc, CL_DIM);
{
RECT t = {28, 92, 750, 114};
DrawTextA(dc, "PS5 screen to PC capture card - by fstat13 - v0.1",
-1, &t, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
}
SelectObject(dc, old);
b = CreateSolidBrush(CL_LINE);
rr.left = 0;
rr.top = 126;
rr.right = r.right;
rr.bottom = 127;
FillRect(dc, &rr, b);
DeleteObject(b);
/* cornici dei campi e del log */
b = CreateSolidBrush(CL_GBD);
{
RECT e = {81, 141, 233, 173};
FrameRect(dc, &e, b);
e.left = 423; e.top = 198; e.right = 483; e.bottom = 226;
FrameRect(dc, &e, b);
e.left = 549; e.top = 198; e.right = 609; e.bottom = 226;
FrameRect(dc, &e, b);
e.left = 25; e.top = 323; e.right = 755; e.bottom = 637;
FrameRect(dc, &e, b);
}
DeleteObject(b);
/* pill di stato */
{
COLORREF dot = (g_stato_level == 2) ? CL_GREEN :
(g_stato_level == 1) ? CL_AMBER : CL_DIM2;
b = CreateSolidBrush(CL_PILL);
ob = (HBRUSH)SelectObject(dc, b);
op = (HPEN)SelectObject(dc, GetStockObject(NULL_PEN));
RoundRect(dc, 26, 258, 754, 302, 44, 44);
SelectObject(dc, ob);
SelectObject(dc, op);
DeleteObject(b);
b = CreateSolidBrush(dot);
ob = (HBRUSH)SelectObject(dc, b);
op = (HPEN)SelectObject(dc, GetStockObject(NULL_PEN));
Ellipse(dc, 46, 269, 58, 281);
SelectObject(dc, ob);
SelectObject(dc, op);
DeleteObject(b);
SetTextColor(dc, (g_stato_level == 2) ? CL_TXT : CL_DIM);
old = SelectObject(dc, g_font_b);
{
RECT t = {68, 258, 740, 302};
DrawTextA(dc, g_stato_text, -1, &t,
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
}
SelectObject(dc, old);
}
EndPaint(h, &ps);
return 0;
}
case WM_DRAWITEM: {
DRAWITEMSTRUCT *d = (DRAWITEMSTRUCT *)l;
RECT r = d->rcItem;
int start = (d->CtlID == ID_START);
int sel = (d->itemState & ODS_SELECTED) != 0;
int dis = (d->itemState & ODS_DISABLED) != 0;
const char *lbl = "";
COLORREF bg, fg, bd;
HBRUSH br, ob;
HPEN pn, op;
HFONT of;
switch(d->CtlID) {
case ID_START: lbl = "START STREAMING"; break;
case ID_STOP: lbl = "STOP"; break;
case ID_DEPLOY: lbl = "Deploy payload"; break;
case ID_INSTALL: lbl = "Install driver"; break;
case ID_APPLY: lbl = "Apply"; break;
}
if(start) {
bg = sel ? CL_ACC_D : CL_ACC;
fg = RGB(255, 255, 255);
bd = bg;
} else {
bg = sel ? CL_GHOST_D : CL_GHOST;
fg = CL_TXT;
bd = CL_GBD;
}
if(dis) {
bg = CL_GHOST;
fg = CL_DIM2;
}
br = CreateSolidBrush(bg);
pn = CreatePen(PS_SOLID, 1, bd);
ob = (HBRUSH)SelectObject(d->hDC, br);
op = (HPEN)SelectObject(d->hDC, pn);
RoundRect(d->hDC, r.left + 1, r.top + 1, r.right - 1, r.bottom - 1,
start ? 14 : 10, start ? 14 : 10);
SelectObject(d->hDC, ob);
SelectObject(d->hDC, op);
DeleteObject(br);
DeleteObject(pn);
SetBkMode(d->hDC, TRANSPARENT);
SetTextColor(d->hDC, fg);
of = (HFONT)SelectObject(d->hDC, g_font_b);
DrawTextA(d->hDC, lbl, -1, &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
SelectObject(d->hDC, of);
return TRUE;
}
case WM_CTLCOLORSTATIC: {
HDC dc = (HDC)w;
HWND ctl = (HWND)l;
if(ctl == g_h_log) { /* l'EDIT readonly arriva qui */
SetBkMode(dc, OPAQUE);
SetBkColor(dc, CL_EDIT);
SetTextColor(dc, CL_EDTX);
return (LRESULT)g_br_edit;
}
SetBkMode(dc, TRANSPARENT);
if(ctl == g_h_stato) {
SetTextColor(dc, g_stato_on ? RGB(14, 150, 80) : RGB(120, 120, 120));
return (LRESULT)GetSysColorBrush(COLOR_BTNFACE);
}
if(ctl == g_h_sub) {
SetTextColor(dc, RGB(110, 110, 110));
return (LRESULT)GetSysColorBrush(COLOR_BTNFACE);
}
return (LRESULT)GetSysColorBrush(COLOR_BTNFACE);
SetTextColor(dc, CL_DIM);
return (LRESULT)g_br_bg;
}
case WM_CTLCOLOREDIT: {
HDC dc = (HDC)w;
SetBkColor(dc, CL_EDIT);
SetTextColor(dc, CL_TXT);
return (LRESULT)g_br_edit;
}
case WM_APP_LOG: {
char *s = (char *)l;
if(s) {
int i = (int)SendMessageA(g_h_log, LB_ADDSTRING, 0, (LPARAM)s);
int cnt = (int)SendMessageA(g_h_log, LB_GETCOUNT, 0, 0);
int len = (int)SendMessageA(g_h_log, WM_GETTEXTLENGTH, 0, 0);
if(cnt > 500) SendMessageA(g_h_log, LB_DELETESTRING, 0, 0);
SendMessageA(g_h_log, LB_SETTOPINDEX, i, 0);
if(len > 150000) {
SendMessageA(g_h_log, EM_SETSEL, 0, 60000);
SendMessageA(g_h_log, EM_REPLACESEL, FALSE, (LPARAM)"");
len = (int)SendMessageA(g_h_log, WM_GETTEXTLENGTH, 0, 0);
}
SendMessageA(g_h_log, EM_SETSEL, len, len);
SendMessageA(g_h_log, EM_REPLACESEL, FALSE, (LPARAM)s);
SendMessageA(g_h_log, EM_REPLACESEL, FALSE, (LPARAM)"\r\n");
SendMessageA(g_h_log, EM_SCROLLCARET, 0, 0);
free(s);
}
return 0;
@@ -794,13 +1112,42 @@ WndProc(HWND h, UINT m, WPARAM w, LPARAM l)
char *s = (char *)l;
if(s) {
SetWindowTextA(g_h_stato, s);
g_stato_on = (strstr(s, "STREAMING") || strstr(s, "AVVIO")) ? 1 : 0;
InvalidateRect(g_h_stato, NULL, TRUE);
RECT pr = {26, 258, 754, 302};
snprintf(g_stato_text, sizeof(g_stato_text), "%s", s);
g_stato_level = strstr(s, "STREAMING") ? 2 :
(strstr(s, "ATTESA") || strstr(s, "AVVIO")) ? 1 : 0;
InvalidateRect(h, &pr, FALSE);
free(s);
}
return 0;
}
case WM_NCHITTEST: {
POINT p = {LOWORD(l), HIWORD(l)};
RECT wr;
int W;
GetWindowRect(h, &wr);
p.x -= wr.left;
p.y -= wr.top;
W = wr.right - wr.left;
if(p.y >= 0 && p.y < 44) {
if(p.x >= W - 80 && p.x < W - 44) return HTMINBUTTON;
if(p.x >= W - 44) return HTCLOSE;
return HTCAPTION;
}
return HTCLIENT;
}
case WM_NCLBUTTONDOWN:
if(w == HTMINBUTTON) {
ShowWindow(h, SW_MINIMIZE);
return 0;
}
if(w == HTCLOSE) {
PostMessageA(h, WM_CLOSE, 0, 0);
return 0;
}
break;
case WM_CLOSE:
if(g_streaming) {
g_streaming = 0;
@@ -839,7 +1186,14 @@ driver_installato(void)
if(RegOpenKeyA(HKEY_CLASSES_ROOT,
"CLSID\\{50533543-414d-4def-9b0e-192080102030}\\InprocServer32",
&k) == ERROR_SUCCESS) {
ok = 1;
char dll[MAX_PATH];
DWORD sz = sizeof(dll), tipo = 0;
if(RegQueryValueExA(k, NULL, NULL, &tipo, (LPBYTE)dll, &sz) == ERROR_SUCCESS &&
(tipo == REG_SZ || tipo == REG_EXPAND_SZ) &&
GetFileAttributesA(dll) != INVALID_FILE_ATTRIBUTES) {
ok = 1;
}
RegCloseKey(k);
}
return ok;
@@ -854,7 +1208,13 @@ WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmd, int show)
(void)prev;
(void)cmd;
if(!is_admin()) {
g_inst = inst;
exe_dir(g_dir, sizeof(g_dir));
logUI("PSCapture avvio: admin=%d no_elevate=%d dir=%s", is_admin(),
getenv("PSCAPTURE_NO_ELEVATE") != NULL, g_dir);
if(!is_admin() && !getenv("PSCAPTURE_NO_ELEVATE")) {
SHELLEXECUTEINFOA sei;
char self[MAX_PATH];
@@ -866,10 +1226,10 @@ WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmd, int show)
sei.lpFile = self;
sei.lpDirectory = NULL;
sei.nShow = SW_SHOWNORMAL;
logUI("richiedo elevazione (prompt UAC)...");
if(ShellExecuteExA(&sei)) return 0; /* rilanciata come admin */
logUI("elevazione rifiutata: continuo senza privilegi");
}
g_inst = inst;
exe_dir(g_dir, sizeof(g_dir));
WSAStartup(MAKEWORD(2, 2), &(WSADATA){0});
@@ -880,23 +1240,33 @@ WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmd, int show)
sizeof(ps5cam_shm_t));
}
if(!g_shm) {
logUI("ERRORE: shared memory non creabile");
MessageBoxA(NULL, "Impossibile creare la shared memory del driver.",
"PSCapture", MB_ICONERROR);
return 1;
}
logUI("shared memory OK");
memset(&wc, 0, sizeof(wc));
wc.lpfnWndProc = WndProc;
wc.hInstance = inst;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
wc.hbrBackground = CreateSolidBrush(CL_BG);
wc.hIcon = LoadIconA(inst, MAKEINTRESOURCEA(1));
wc.lpszClassName = "PSCaptureWnd";
RegisterClassA(&wc);
g_hwnd = CreateWindowA("PSCaptureWnd", "PSCapture by fstat13 — PS5 screen to PC",
WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
CW_USEDEFAULT, CW_USEDEFAULT, 728, 580,
NULL, NULL, inst, NULL);
{
g_hwnd = CreateWindowExA(WS_EX_APPWINDOW, "PSCaptureWnd",
"PSCapture by fstat13 - PS5 screen to PC",
WS_POPUP,
CW_USEDEFAULT, CW_USEDEFAULT, 780, 660,
NULL, NULL, inst, NULL);
if(g_hwnd) {
HRGN rg = CreateRoundRectRgn(0, 0, 781, 661, 20, 20);
if(rg) SetWindowRgn(g_hwnd, rg, TRUE);
}
}
ShowWindow(g_hwnd, show);
UpdateWindow(g_hwnd);
PostMessageA(g_hwnd, WM_APP_AUTOSTART, 0, 0);
+1
View File
@@ -1,3 +1,4 @@
#include <windows.h>
1 ICON "pscapture.ico"
2 RCDATA "pscapture.elf"
3 RCDATA "pscapture.dll"