Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18a50d38ab | ||
|
|
c404131924 | ||
|
|
09a5f73393 | ||
|
|
dfc1664806 | ||
|
|
6c0bfd4e4a | ||
|
|
6bfec8bf19 | ||
|
|
1c18c2c328 | ||
|
|
c8e738ef21 | ||
|
|
7e5915e115 | ||
|
|
be5ea10875 | ||
|
|
ba662f848b | ||
|
|
aa896397fe | ||
|
|
7bf367419f | ||
|
|
83cf192d2d | ||
|
|
2a613ebed5 | ||
|
|
8454ab5de3 | ||
|
|
2c74923701 |
No files matched your search
@@ -0,0 +1,9 @@
|
||||
# Force LF line endings for scripts and workflows so Linux/SteamOS execution never breaks
|
||||
*.sh text eol=lf
|
||||
*.py text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.desktop text eol=lf
|
||||
*.spv binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
@@ -1,3 +1,7 @@
|
||||
<p align="center">
|
||||
<img src="assets/hero.png" width="100%" alt="Pixel VR Player" />
|
||||
</p>
|
||||
|
||||
# Pixel VR Player
|
||||
|
||||
A native VR video player for the **Valve Steam Frame**, playing flat 2D, 180°, and
|
||||
@@ -13,25 +17,55 @@ A native VR video player for the **Valve Steam Frame**, playing flat 2D, 180°,
|
||||
> NV12) with zero-copy dma-buf import; flat / 180° / 360° projection and mono /
|
||||
> SBS (including half-SBS) / TB stereo, auto-detected from the filename; spatial
|
||||
> audio with a master clock. In-headset UI: file browser (with file type and
|
||||
> size), transport bar, settings menu, and a downloads manager. Native Valve
|
||||
> size), transport bar, settings menu, downloads manager, and an encrypted hidden vault. Native Valve
|
||||
> Steam Frame controller support (`XR_VALVE_frame_controller_interaction`) using
|
||||
> the runtime's own render model, with per-button animation on the model and
|
||||
> press-to-bind remapping.
|
||||
|
||||
## Install (prebuilt release)
|
||||
## Screenshots
|
||||
|
||||
One command, from a terminal on the headset (Desktop Mode or over SSH):
|
||||
<p align="center">
|
||||
<img src="docs/ui-redesign/mockup-unified-1-files.jpg" width="49%" alt="Spatial File Browser" />
|
||||
<img src="docs/ui-redesign/mockup-unified-2-downloads.jpg" width="49%" alt="Downloads Manager" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/ui-redesign/mockup-unified-3-settings.jpg" width="49%" alt="Settings Panel" />
|
||||
<img src="docs/ui-redesign/mockup-unified-4-cinema-hud.jpg" width="49%" alt="Cinema Playback HUD" />
|
||||
</p>
|
||||
|
||||
## Installation
|
||||
|
||||
Choose whichever method fits your setup:
|
||||
|
||||
### Option A: USB Drive / MicroSD Card (No typing & No Dev Mode needed)
|
||||
|
||||
The simplest method for standard users without touching the terminal:
|
||||
|
||||
1. Download `pixelvr-linux-arm64.tar.gz` from the [Releases](https://github.com/Pixelplanet/Pixel-VR-Player/releases) page onto a USB flash drive or MicroSD card.
|
||||
2. Plug it into your Steam Frame and switch to **Desktop Mode**.
|
||||
3. Open the USB drive in Dolphin and double-click **`Install Pixel VR Player.desktop`**.
|
||||
4. A confirmation dialog will appear when finished. Return to Gaming Mode and launch it directly from your Steam Library!
|
||||
|
||||
### Option B: One command in headset terminal
|
||||
|
||||
If you are already in Desktop Mode on the Steam Frame, open **Konsole** and paste:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/Pixelplanet/Pixel-VR-Player/main/tools/install.sh | sh
|
||||
```
|
||||
|
||||
It downloads the latest ARM64 release, verifies its checksum, installs it to
|
||||
`~/PixelVRPlayer`, and adds **Pixel VR Player** to your Steam library as a
|
||||
Non-Steam Game. Restart Steam if it was already running. SteamVR must be
|
||||
installed.
|
||||
*(No root access or disabled read-only filesystem needed; it installs entirely into user space).*
|
||||
|
||||
Launch **Pixel VR Player** from your library, or directly:
|
||||
### Option C: From your PC over Wi-Fi (Requires SSH or Dev Mode)
|
||||
|
||||
If you have SSH enabled or Developer Mode turned on with your headset paired:
|
||||
|
||||
- **Windows:** Double-click [`tools/install-from-pc.bat`](tools/install-from-pc.bat) and enter your headset's IP.
|
||||
- **macOS / Linux:** Run `./tools/install-from-pc.sh`.
|
||||
|
||||
The script connects over SSH, installs the app, and registers the Steam shortcut remotely.
|
||||
|
||||
Launch **Pixel VR Player** directly from your Steam library, or from terminal:
|
||||
|
||||
```bash
|
||||
~/PixelVRPlayer/run.sh # opens the in-headset file browser
|
||||
@@ -48,11 +82,12 @@ Installer options:
|
||||
| `--restart-steam` | close Steam first, then reopen it so the entry appears at once |
|
||||
| `--uninstall` | remove the app and its Steam shortcut |
|
||||
|
||||
Re-run the same command to upgrade. Settings (`~/.config/pixelvr`) and cached
|
||||
thumbnails (`~/.cache/pixelvr`) live outside the install directory and are kept.
|
||||
Re-run the same command to upgrade. Settings (`~/.config/pixelvr`), cached
|
||||
thumbnails (`~/.cache/pixelvr`), and hidden vault media (`~/.pixelvr_vault`) live
|
||||
outside the install directory and are kept intact.
|
||||
|
||||
**Offline install:** download `pixelvr-linux-arm64.tar.gz` from the
|
||||
[Releases](../../releases) page, copy it to the headset, extract it and run
|
||||
[Releases](https://github.com/Pixelplanet/Pixel-VR-Player/releases) page, copy it to the headset, extract it and run
|
||||
`./install.sh` from the extracted folder — or use `run.sh` directly. The app logs
|
||||
to `~/pixelvr-run.log`.
|
||||
|
||||
@@ -86,6 +121,39 @@ the file browser rather than stacking on top of it.
|
||||
All button bindings can be changed in **Settings ▸ Controls** (press-to-bind).
|
||||
|
||||
|
||||
## Discreet Media Vault
|
||||
|
||||
Pixel VR Player includes a private, zero-trace local storage vault (`~/.pixelvr_vault`) hidden from the standard file browser and startup directory scans.
|
||||
|
||||
<details>
|
||||
<summary>🔒 <b>Accessing the Hidden Vault</b> (click to expand)</summary>
|
||||
|
||||
To open or re-lock the vault in VR without bringing up an on-screen keyboard or visible PIN dialog:
|
||||
|
||||
1. Fully squeeze either controller's **grip** (hold down the mechanical **Full Press** click).
|
||||
2. While holding the grip clicked, enter the 6-key factory combination:
|
||||
|
||||
> 🎵 *The Swedish pop sensation that won Eurovision in 1974 with "Waterloo"...*
|
||||
> 📐 *...followed by the two classic axes of the 2D Cartesian plane.*
|
||||
|
||||
<details>
|
||||
<summary>👉 <i>Need the direct button sequence? Click here to reveal</i></summary>
|
||||
|
||||
Hold **Full Grip Click** and press:
|
||||
> **`A` → `B` → `B` → `A` → `X` → `Y`**
|
||||
*(Within 2.5 seconds per step. Haptic feedback confirms each input).*
|
||||
|
||||
</details>
|
||||
|
||||
- **Instant Auto-Lock:** Re-locks automatically and immediately purges in-memory textures when the headset proximity sensor detects removal, or when clicking **[ 🔒 Lock Vault ]** in the sidebar.
|
||||
- **Custom Combinations:** You can record your own custom button sequence anytime in **Settings ▸ Vault & Privacy**.
|
||||
- **Move to Vault:** While unlocked, local videos can be moved directly into the vault from the browser.
|
||||
- **Safe Persistence:** Media inside `~/.pixelvr_vault` is never deleted when updating or uninstalling the app.
|
||||
|
||||
For full architecture and security details, see the [Hidden Vault Technical Specification](docs/HIDDEN_VAULT_SPEC.md).
|
||||
</details>
|
||||
|
||||
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
@@ -195,8 +263,7 @@ choice, and troubleshooting). In short, after a one-time Developer-Mode pairing:
|
||||
# override the target with --host steamos@<ip> or FRAME_HOST
|
||||
```
|
||||
|
||||
Either way the app appears in **Steam ▸ Library ▸ Non-Steam** as *Devkit Game:
|
||||
PixelVRPlayer*. To run it manually over SSH for debugging:
|
||||
Either way the app appears in **Steam ▸ Library ▸ Non-Steam** as **Pixel VR Player**. To run it manually over SSH for debugging:
|
||||
|
||||
```bash
|
||||
ssh steamos@frame '~/devkit-game/PixelVRPlayer/launch.sh /path/to/video.mp4'
|
||||
|
||||
|
After Width: | Height: | Size: 336 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 798 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 936 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 852 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 860 KiB |
|
After Width: | Height: | Size: 994 KiB |
|
After Width: | Height: | Size: 952 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 759 KiB |
|
After Width: | Height: | Size: 602 KiB |
|
After Width: | Height: | Size: 800 KiB |
|
After Width: | Height: | Size: 784 KiB |
|
After Width: | Height: | Size: 788 KiB |
|
After Width: | Height: | Size: 798 KiB |
|
After Width: | Height: | Size: 812 KiB |
|
After Width: | Height: | Size: 336 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 936 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 852 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 540 KiB |
|
After Width: | Height: | Size: 961 KiB |
|
After Width: | Height: | Size: 642 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 445 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 436 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 455 KiB |
|
After Width: | Height: | Size: 860 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 994 KiB |
|
After Width: | Height: | Size: 952 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 119 KiB |
@@ -12,17 +12,20 @@ libraries.
|
||||
|
||||
## Just installing a release (no devkit, no build)
|
||||
|
||||
End users do not need any of the steps below. From a terminal on the headset
|
||||
(Desktop Mode or over SSH):
|
||||
End users do not need to build anything or use devkits. Choose whichever option fits your setup:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/Pixelplanet/Pixel-VR-Player/main/tools/install.sh | sh
|
||||
```
|
||||
1. **USB Drive / MicroSD Card (No typing & No Dev Mode needed):**
|
||||
Download `pixelvr-linux-arm64.tar.gz` from GitHub Releases onto a USB flash drive or MicroSD card, plug it into the headset, switch to **Desktop Mode**, and double-click **`Install Pixel VR Player.desktop`**.
|
||||
2. **From your PC over Wi-Fi (Requires SSH or Dev Mode):**
|
||||
Run [`tools/install-from-pc.bat`](../tools/install-from-pc.bat) (Windows) or [`tools/install-from-pc.sh`](../tools/install-from-pc.sh) (Mac/Linux) and enter the headset's IP address.
|
||||
3. **One command in terminal (Desktop Mode / SSH):**
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/Pixelplanet/Pixel-VR-Player/main/tools/install.sh | sh
|
||||
```
|
||||
|
||||
That verifies the checksum, installs to `~/PixelVRPlayer`, and registers
|
||||
**Pixel VR Player** as a Non-Steam Game. Relaunch Steam if it was running. Rerun
|
||||
the same command to upgrade; `--uninstall` removes it. The rest of this document
|
||||
covers building and iterating as a developer.
|
||||
The installer verifies checksums, installs to `~/PixelVRPlayer`, and registers **Pixel VR Player** as a Non-Steam Game in your library. Re-running upgrades in-place while keeping all settings, thumbnails, and hidden vault files (`~/.pixelvr_vault`).
|
||||
|
||||
The rest of this document covers building and deploying as a developer.
|
||||
|
||||
---
|
||||
|
||||
@@ -38,8 +41,11 @@ covers building and iterating as a developer.
|
||||
# 2b. Or stage a clean folder for the GUI Devkit Client:
|
||||
./tools/stage.sh # -> dist/PixelVRPlayer/
|
||||
|
||||
# 2c. Or build the release tarball end users install from:
|
||||
./tools/package-release.sh build-aarch64 pixelvr-linux-arm64.tar.gz v1.3.0
|
||||
# 2c. Or assemble release tarball with desktop installer:
|
||||
./tools/package-release.sh build-aarch64 pixelvr-linux-arm64.tar.gz v1.4.2
|
||||
|
||||
# 2d. Capture in-headset screenshots over OpenVR / SteamVR compositor:
|
||||
python tools/take_headset_screenshot.py --delay 5
|
||||
```
|
||||
|
||||
---
|
||||
@@ -80,7 +86,7 @@ All three methods below share the same setup. Do this once.
|
||||
| Method | Per-deploy effort | Best for |
|
||||
|--------|-------------------|----------|
|
||||
| **A. Devkit Client (GUI)** | click Upload → Start | first run; the official, guided path |
|
||||
| **B. `tools/deploy.py` (CLI)** | one command | fast iteration during development |
|
||||
| **B. `tools/deploy.sh` (CLI)** | one command | fast iteration during development |
|
||||
| **C. Manual SSH + scp** | a few commands | debugging, capturing logs live |
|
||||
| ~~ADB / Android~~ | — | APKs only — **not** this app |
|
||||
|
||||
@@ -88,8 +94,7 @@ All three methods below share the same setup. Do this once.
|
||||
- **Easiest to iterate:** Option B (one command builds nothing you didn't ask for
|
||||
and launches in the headset).
|
||||
|
||||
They all end up in the same place: *Steam ▸ Library ▸ Non-Steam ▸ **Devkit Game:
|
||||
PixelVRPlayer***.
|
||||
They all end up in the same place: *Steam ▸ Library ▸ Non-Steam ▸ **Pixel VR Player***.
|
||||
|
||||
---
|
||||
|
||||
@@ -121,7 +126,7 @@ The official, no-scripting path.
|
||||
|
||||
---
|
||||
|
||||
## Option B — One command (`tools/deploy.py`)
|
||||
## Option B — One command (`tools/deploy.sh`)
|
||||
|
||||
Best for the edit-build-run loop. Copies the binary, shaders and `launch.sh` into
|
||||
`~/devkit-game/PixelVRPlayer/`, registers the Non-Steam shortcut, and (optionally)
|
||||
@@ -174,7 +179,7 @@ differ in which system libraries it sees:
|
||||
environment. This is what Valve's docs recommend and what the **GUI (Option A)**
|
||||
selects. Good default.
|
||||
- **Directly on the host** — uses SteamOS's own FFmpeg/SteamVR libraries. This is
|
||||
what **`deploy.py` (Option B)** registers, matching how a shipping native OpenXR
|
||||
what **`deploy.sh` (Option B)** registers, matching how a shipping native OpenXR
|
||||
player runs.
|
||||
|
||||
Both expose the SteamVR OpenXR runtime, so both can work. If the app fails to load
|
||||
@@ -225,6 +230,25 @@ Requirements:
|
||||
|
||||
---
|
||||
|
||||
## Taking in-headset screenshots
|
||||
|
||||
To capture real in-headset screenshots directly from the SteamVR compositor while the app is running:
|
||||
|
||||
```bash
|
||||
# Capture with a 5-second countdown to put the headset on:
|
||||
python tools/take_headset_screenshot.py --delay 5
|
||||
|
||||
# Point to a specific headset IP:
|
||||
python tools/take_headset_screenshot.py --host steamos@192.168.178.61 --delay 3
|
||||
|
||||
# Continuous series (e.g. 5 captures spaced 2 seconds apart):
|
||||
python tools/take_headset_screenshot.py --interval 2 --count 5
|
||||
```
|
||||
|
||||
The script connects over SSH, triggers SteamVR's compositor screenshot API, fetches the resulting PNGs, and places them into `docs/screenshots/` with both the raw stereo composite (`_stereo.png`) and a cropped single-eye preview (`_preview.png`).
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause / fix |
|
||||
|
||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 642 KiB After Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 549 KiB After Width: | Height: | Size: 207 KiB |
@@ -0,0 +1,8 @@
|
||||
[Desktop Entry]
|
||||
Name=Install Pixel VR Player
|
||||
Comment=Install Pixel VR Player for Valve Steam Frame
|
||||
Exec=bash -c 'SCRIPT_DIR="$(dirname "%k")"; if [ -f "$SCRIPT_DIR/install.sh" ]; then cd "$SCRIPT_DIR" && bash install.sh; else curl -fsSL https://raw.githubusercontent.com/Pixelplanet/Pixel-VR-Player/main/tools/install.sh | bash; fi; if [ $? -eq 0 ]; then command -v kdialog >/dev/null && kdialog --title "Pixel VR Player" --msgbox "Installation complete!\n\nPixel VR Player has been added to your Steam Library.\nYou can now return to Gaming Mode."; else command -v kdialog >/dev/null && kdialog --title "Pixel VR Player" --error "Installation encountered an issue.\nCheck ~/pixelvr-install.log or terminal output."; fi; read -p "Press Enter to close..." dummy'
|
||||
Icon=steam
|
||||
Terminal=true
|
||||
Type=Application
|
||||
Categories=AudioVideo;Player;VR;
|
||||
@@ -127,7 +127,7 @@ def main() -> int:
|
||||
'mkdir -p "$target/shaders"; printf %s "$target"'
|
||||
)
|
||||
|
||||
# 2. Copy binary, shaders and the direct-run helper.
|
||||
# 2. Copy binary, shaders, launcher, and icon.
|
||||
print("Copying binary ...")
|
||||
scp([str(binary)], f"{REMOTE_DIR}/pixelvr")
|
||||
if shaders:
|
||||
@@ -136,6 +136,10 @@ def main() -> int:
|
||||
launch_helper = PROJECT_ROOT / "tools" / "launch.sh"
|
||||
if launch_helper.is_file():
|
||||
scp([str(launch_helper)], f"{REMOTE_DIR}/launch.sh")
|
||||
for asset_name in ("icon.png", "hero.png", "capsule.png", "header.png", "logo.png"):
|
||||
asset_file = PROJECT_ROOT / "assets" / asset_name
|
||||
if asset_file.is_file():
|
||||
scp([str(asset_file)], f"{REMOTE_DIR}/{asset_name}")
|
||||
|
||||
# 3. Register the Non-Steam shortcut (runs on the host, not in Sniper).
|
||||
request = {
|
||||
@@ -165,6 +169,83 @@ def main() -> int:
|
||||
if isinstance(response, dict) and response.get("error"):
|
||||
raise RuntimeError(f"shortcut registration failed: {response['error']}")
|
||||
|
||||
# 3b. Sanitize shortcuts.vdf to remove the 'Devkit Game: ' prefix and set icon.
|
||||
sanitize_script = (
|
||||
"import glob, os, struct, shutil\n"
|
||||
"OBJ, STR, INT, U64, END = 0, 1, 2, 7, 8\n"
|
||||
"def read_cstr(buf, pos):\n"
|
||||
" end = buf.find(b'\\0', pos)\n"
|
||||
" return buf[pos:end].decode('utf-8', 'surrogateescape'), end + 1\n"
|
||||
"def parse_obj(buf, pos):\n"
|
||||
" entries = {}\n"
|
||||
" while pos < len(buf):\n"
|
||||
" kind = buf[pos]\n"
|
||||
" if kind in (END, 0x0B): return entries, pos + 1\n"
|
||||
" key, pos = read_cstr(buf, pos + 1)\n"
|
||||
" if kind == OBJ: value, pos = parse_obj(buf, pos)\n"
|
||||
" elif kind == STR: value, pos = read_cstr(buf, pos)\n"
|
||||
" elif kind == INT: value = struct.unpack_from('<i', buf, pos)[0]; pos += 4\n"
|
||||
" elif kind == U64: value = struct.unpack_from('<Q', buf, pos)[0]; pos += 8\n"
|
||||
" else: break\n"
|
||||
" entries[key] = [kind, value]\n"
|
||||
" return entries, pos\n"
|
||||
"def dump(entries):\n"
|
||||
" out = bytearray()\n"
|
||||
" for key, (kind, value) in entries.items():\n"
|
||||
" kb = key.encode('utf-8', 'surrogateescape')\n"
|
||||
" if kind == OBJ: out += bytes([OBJ]) + kb + b'\\0' + dump(value) + bytes([END])\n"
|
||||
" elif kind == STR: out += bytes([STR]) + kb + b'\\0' + value.encode('utf-8', 'surrogateescape') + b'\\0'\n"
|
||||
" elif kind == INT: out += bytes([INT]) + kb + b'\\0' + struct.pack('<i', value)\n"
|
||||
" elif kind == U64: out += bytes([U64]) + kb + b'\\0' + struct.pack('<Q', value)\n"
|
||||
" return bytes(out)\n"
|
||||
"for vdf in glob.glob(os.path.expanduser('~/.steam/steam/userdata/*/config/shortcuts.vdf')):\n"
|
||||
" try:\n"
|
||||
" with open(vdf, 'rb') as f: data = f.read()\n"
|
||||
" if not data or data[0] != OBJ: continue\n"
|
||||
" key, pos = read_cstr(data, 1)\n"
|
||||
" s_dict, _ = parse_obj(data, pos)\n"
|
||||
" mod = False\n"
|
||||
" for k, v in s_dict.items():\n"
|
||||
" if isinstance(v, list) and v[0] == OBJ:\n"
|
||||
" e = v[1]\n"
|
||||
" name = e.get('AppName', [STR, ''])[1]\n"
|
||||
" if 'Pixel' in name:\n"
|
||||
" e['AppName'] = [STR, 'Pixel VR Player']\n"
|
||||
" e['Devkit'] = [INT, 0]\n"
|
||||
" e['DevkitGameID'] = [STR, '']\n"
|
||||
" icon_p = os.path.expanduser('~/" + REMOTE_DIR + "/icon.png')\n"
|
||||
" hero_p = os.path.expanduser('~/" + REMOTE_DIR + "/hero.png')\n"
|
||||
" capsule_p = os.path.expanduser('~/" + REMOTE_DIR + "/capsule.png')\n"
|
||||
" header_p = os.path.expanduser('~/" + REMOTE_DIR + "/header.png')\n"
|
||||
" logo_p = os.path.expanduser('~/" + REMOTE_DIR + "/logo.png')\n"
|
||||
" grid = os.path.join(os.path.dirname(os.path.dirname(vdf)), 'config', 'grid')\n"
|
||||
" os.makedirs(grid, exist_ok=True)\n"
|
||||
" raw_id = e.get('appid', [INT, 0])[1] & 0xFFFFFFFF\n"
|
||||
" if os.path.isfile(icon_p):\n"
|
||||
" e['icon'] = [STR, icon_p]\n"
|
||||
" shutil.copyfile(icon_p, os.path.join(grid, f'{raw_id}_icon.png'))\n"
|
||||
" if os.path.isfile(capsule_p):\n"
|
||||
" shutil.copyfile(capsule_p, os.path.join(grid, f'{raw_id}p.png'))\n"
|
||||
" elif os.path.isfile(icon_p):\n"
|
||||
" shutil.copyfile(icon_p, os.path.join(grid, f'{raw_id}p.png'))\n"
|
||||
" if os.path.isfile(header_p):\n"
|
||||
" shutil.copyfile(header_p, os.path.join(grid, f'{raw_id}.png'))\n"
|
||||
" elif os.path.isfile(icon_p):\n"
|
||||
" shutil.copyfile(icon_p, os.path.join(grid, f'{raw_id}.png'))\n"
|
||||
" if os.path.isfile(hero_p):\n"
|
||||
" shutil.copyfile(hero_p, os.path.join(grid, f'{raw_id}_hero.png'))\n"
|
||||
" if os.path.isfile(logo_p):\n"
|
||||
" shutil.copyfile(logo_p, os.path.join(grid, f'{raw_id}_logo.png'))\n"
|
||||
" mod = True\n"
|
||||
" if mod:\n"
|
||||
" tmp = vdf + '.tmp'\n"
|
||||
" with open(tmp, 'wb') as f: f.write(dump({key: [OBJ, s_dict]}) + bytes([END]))\n"
|
||||
" os.replace(tmp, vdf)\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
)
|
||||
ssh(f"python3 -c {shlex.quote(sanitize_script)}")
|
||||
|
||||
# 4. Optionally launch it in the headset.
|
||||
if args.launch:
|
||||
print("Launching in the headset ...")
|
||||
@@ -184,7 +265,7 @@ def main() -> int:
|
||||
print(f"Deployed to {host}:~/{REMOTE_DIR}")
|
||||
if player_args:
|
||||
print(" argv:", " ".join(["./pixelvr", *player_args]))
|
||||
print(f" Launch from Steam > Library > Non-Steam > 'Devkit Game: {GAME}'")
|
||||
print(" Launch from Steam > Library > Non-Steam > 'Pixel VR Player'")
|
||||
print(f" Or over SSH: ssh {host} '{REMOTE_DIR}/launch.sh <video>'")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
echo ===============================================================
|
||||
echo Pixel VR Player - Steam Frame Remote Installer
|
||||
echo ===============================================================
|
||||
echo This tool installs Pixel VR Player on your Valve Steam Frame
|
||||
echo over your local Wi-Fi network directly from your PC.
|
||||
echo ===============================================================
|
||||
echo.
|
||||
|
||||
set "FRAME_IP=steamframe.local"
|
||||
set /p "INPUT_IP=Enter Steam Frame IP address [press Enter for steamframe.local]: "
|
||||
if not "!INPUT_IP!"=="" set "FRAME_IP=!INPUT_IP!"
|
||||
|
||||
echo.
|
||||
echo Connecting to steamos@!FRAME_IP!...
|
||||
echo (If prompted for a password, enter your SteamOS desktop password)
|
||||
echo.
|
||||
|
||||
where ssh >nul 2>&1
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo [ERROR] SSH is not installed or not in PATH on this PC.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
ssh -o StrictHostKeyChecking=no steamos@!FRAME_IP! "curl -fsSL https://raw.githubusercontent.com/Pixelplanet/Pixel-VR-Player/main/tools/install.sh | sh"
|
||||
|
||||
if %ERRORLEVEL% equ 0 (
|
||||
echo.
|
||||
echo ===============================================================
|
||||
echo [SUCCESS] Pixel VR Player installed on your Steam Frame!
|
||||
echo It is now in your Steam Library under Non-Steam Games.
|
||||
echo ===============================================================
|
||||
) else (
|
||||
echo.
|
||||
echo ===============================================================
|
||||
echo [NOTE] If connection failed, please ensure:
|
||||
echo 1. Both your PC and Steam Frame are on the same Wi-Fi.
|
||||
echo 2. SSH is enabled on the headset.
|
||||
echo (In Desktop Mode, open Konsole and run 'passwd',
|
||||
echo then 'sudo systemctl start sshd', or enable Dev Mode).
|
||||
echo ===============================================================
|
||||
)
|
||||
|
||||
echo.
|
||||
pause
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
# Pixel VR Player - Remote installer from Mac / Linux to Steam Frame
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo "==============================================================="
|
||||
echo " Pixel VR Player - Steam Frame Remote Installer"
|
||||
echo "==============================================================="
|
||||
echo " This tool installs Pixel VR Player on your Valve Steam Frame"
|
||||
echo " over your local Wi-Fi network directly from your Mac or PC."
|
||||
echo "==============================================================="
|
||||
echo ""
|
||||
|
||||
read -rp "Enter Steam Frame IP address [press Enter for steamframe.local]: " INPUT_IP
|
||||
FRAME_IP="${INPUT_IP:-steamframe.local}"
|
||||
|
||||
echo ""
|
||||
echo "Connecting to steamos@${FRAME_IP}..."
|
||||
echo "(If prompted for a password, enter your SteamOS desktop password)"
|
||||
echo ""
|
||||
|
||||
if ssh -o StrictHostKeyChecking=no "steamos@${FRAME_IP}" "curl -fsSL https://raw.githubusercontent.com/Pixelplanet/Pixel-VR-Player/main/tools/install.sh | sh"; then
|
||||
echo ""
|
||||
echo "==============================================================="
|
||||
echo " [SUCCESS] Pixel VR Player installed on your Steam Frame!"
|
||||
echo " It is now in your Steam Library under Non-Steam Games."
|
||||
echo "==============================================================="
|
||||
else
|
||||
echo ""
|
||||
echo "==============================================================="
|
||||
echo " [NOTE] If connection failed, please ensure:"
|
||||
echo " 1. Both devices are on the same Wi-Fi network."
|
||||
echo " 2. SSH is enabled on the headset."
|
||||
echo " (In Desktop Mode, open Konsole and run 'passwd',"
|
||||
echo " then 'sudo systemctl start sshd', or enable Dev Mode)."
|
||||
echo "==============================================================="
|
||||
fi
|
||||
@@ -230,7 +230,8 @@ def find(shortcuts, name, exe):
|
||||
continue
|
||||
stored_exe = entry.get("Exe", [STR, ""])[1].strip('"')
|
||||
stored_name = entry.get("AppName", [STR, ""])[1]
|
||||
if (exe and stored_exe == exe) or stored_name == name:
|
||||
devkit_id = entry.get("DevkitGameID", [STR, ""])[1]
|
||||
if (exe and stored_exe == exe) or stored_name == name or stored_name in ("Devkit Game: PixelVRPlayer", "Devkit Game: Pixel VR Player") or devkit_id == "PixelVRPlayer":
|
||||
return key, entry
|
||||
return None, None
|
||||
|
||||
@@ -276,11 +277,12 @@ def main():
|
||||
shortcuts[str(index)] = [OBJ, entry]
|
||||
if "appid" not in entry:
|
||||
entry["appid"] = [INT, to_i32(app_id(quoted_exe, name))]
|
||||
icon_path = os.path.join(start_dir, "icon.png") if os.path.isfile(os.path.join(start_dir, "icon.png")) else ""
|
||||
for field, value in (
|
||||
("AppName", [STR, name]),
|
||||
("Exe", [STR, quoted_exe]),
|
||||
("StartDir", [STR, '"%s"' % start_dir]),
|
||||
("icon", [STR, ""]),
|
||||
("icon", [STR, icon_path]),
|
||||
("ShortcutPath", [STR, ""]),
|
||||
("LaunchOptions", [STR, ""]),
|
||||
("IsHidden", [INT, 0]),
|
||||
@@ -295,6 +297,31 @@ def main():
|
||||
):
|
||||
entry[field] = value
|
||||
entry.setdefault("tags", [OBJ, {}])
|
||||
if icon_path and os.path.isfile(icon_path):
|
||||
try:
|
||||
import shutil
|
||||
steam_user_dir = os.path.dirname(os.path.dirname(path))
|
||||
grid_dir = os.path.join(steam_user_dir, "config", "grid")
|
||||
if icon_path and os.path.isfile(icon_path):
|
||||
shutil.copyfile(icon_path, os.path.join(grid_dir, "%d_icon.png" % raw_appid))
|
||||
capsule_path = os.path.join(os.path.dirname(icon_path), "capsule.png") if icon_path else ""
|
||||
if capsule_path and os.path.isfile(capsule_path):
|
||||
shutil.copyfile(capsule_path, os.path.join(grid_dir, "%dp.png" % raw_appid))
|
||||
elif icon_path and os.path.isfile(icon_path):
|
||||
shutil.copyfile(icon_path, os.path.join(grid_dir, "%dp.png" % raw_appid))
|
||||
header_path = os.path.join(os.path.dirname(icon_path), "header.png") if icon_path else ""
|
||||
if header_path and os.path.isfile(header_path):
|
||||
shutil.copyfile(header_path, os.path.join(grid_dir, "%d.png" % raw_appid))
|
||||
elif icon_path and os.path.isfile(icon_path):
|
||||
shutil.copyfile(icon_path, os.path.join(grid_dir, "%d.png" % raw_appid))
|
||||
hero_path = os.path.join(os.path.dirname(icon_path), "hero.png") if icon_path else ""
|
||||
if hero_path and os.path.isfile(hero_path):
|
||||
shutil.copyfile(hero_path, os.path.join(grid_dir, "%d_hero.png" % raw_appid))
|
||||
logo_path = os.path.join(os.path.dirname(icon_path), "logo.png") if icon_path else ""
|
||||
if logo_path and os.path.isfile(logo_path):
|
||||
shutil.copyfile(logo_path, os.path.join(grid_dir, "%d_logo.png" % raw_appid))
|
||||
except Exception:
|
||||
pass
|
||||
print("%s '%s' (%d shortcut(s) in the library)" % (
|
||||
"added" if created else "updated", name, len(shortcuts)))
|
||||
|
||||
@@ -509,6 +536,15 @@ if [ -f "$srcdir/VERSION" ]; then
|
||||
elif [ -n "$VERSION" ]; then
|
||||
printf '%s\n' "$VERSION" > "$INSTALL_DIR/VERSION"
|
||||
fi
|
||||
for asset in icon.png hero.png capsule.png header.png logo.png; do
|
||||
if [ -f "$srcdir/$asset" ]; then
|
||||
cp "$srcdir/$asset" "$INSTALL_DIR/$asset"
|
||||
elif [ -n "$script_dir" ] && [ -f "$script_dir/$asset" ]; then
|
||||
cp "$script_dir/$asset" "$INSTALL_DIR/$asset"
|
||||
elif [ -f "assets/$asset" ]; then
|
||||
cp "assets/$asset" "$INSTALL_DIR/$asset"
|
||||
fi
|
||||
done
|
||||
ok "$(ls -1 "$INSTALL_DIR/shaders" | wc -l | tr -d ' ') shaders, $(du -h "$INSTALL_DIR/pixelvr" 2>/dev/null | cut -f1) binary"
|
||||
|
||||
missing_libs=""
|
||||
|
||||
@@ -37,12 +37,23 @@ cp "${bin}" "${stage}/PixelVRPlayer/pixelvr"
|
||||
cp "${BUILD_DIR}/shaders/"*.spv "${stage}/PixelVRPlayer/shaders/"
|
||||
cp tools/run.sh "${stage}/PixelVRPlayer/run.sh"
|
||||
cp tools/install.sh "${stage}/PixelVRPlayer/install.sh"
|
||||
if [[ -f "tools/Install Pixel VR Player.desktop" ]]; then
|
||||
cp "tools/Install Pixel VR Player.desktop" "${stage}/PixelVRPlayer/Install Pixel VR Player.desktop"
|
||||
fi
|
||||
for asset in icon.png hero.png capsule.png header.png logo.png; do
|
||||
if [[ -f "assets/${asset}" ]]; then
|
||||
cp "assets/${asset}" "${stage}/PixelVRPlayer/${asset}"
|
||||
fi
|
||||
done
|
||||
if [[ -n "${VERSION}" ]]; then
|
||||
printf '%s\n' "${VERSION}" > "${stage}/PixelVRPlayer/VERSION"
|
||||
fi
|
||||
chmod +x "${stage}/PixelVRPlayer/pixelvr" \
|
||||
"${stage}/PixelVRPlayer/run.sh" \
|
||||
"${stage}/PixelVRPlayer/install.sh"
|
||||
if [[ -f "${stage}/PixelVRPlayer/Install Pixel VR Player.desktop" ]]; then
|
||||
chmod +x "${stage}/PixelVRPlayer/Install Pixel VR Player.desktop"
|
||||
fi
|
||||
|
||||
rm -f "${OUT_TAR}"
|
||||
tar -C "${stage}" -czf "${OUT_TAR}" PixelVRPlayer
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Automated in-headset screenshot capture tool for Valve Steam Frame (SteamOS).
|
||||
Uses OpenVR IVRScreenshots via SteamVR's running compositor to capture
|
||||
both 2D preview and stereo VR framebuffers.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import argparse
|
||||
import subprocess
|
||||
import base64
|
||||
from datetime import datetime
|
||||
|
||||
DEFAULT_SSH_KEY = os.path.expanduser(r"~\AppData\Local\steamos-devkit\steamos-devkit\devkit_rsa")
|
||||
DEFAULT_HOST = "steamos@192.168.178.61"
|
||||
|
||||
CPP_CODE = r"""#include <iostream>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include "/opt/steamvr/tools/hellovr_vulkan_linux/src/openvr/headers/openvr.h"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc < 3) {
|
||||
std::cerr << "Usage: " << argv[0] << " <preview_png> <stereo_png>" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
const char* preview = argv[1];
|
||||
const char* vr_file = argv[2];
|
||||
|
||||
vr::EVRInitError err = vr::VRInitError_None;
|
||||
vr::IVRSystem* vrSys = vr::VR_Init(&err, vr::VRApplication_Overlay);
|
||||
if (!vrSys || err != vr::VRInitError_None) {
|
||||
std::cerr << "VR_Init failed: " << vr::VR_GetVRInitErrorAsEnglishDescription(err) << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
vr::IVRScreenshots* vrScreenshots = (vr::IVRScreenshots*)vr::VR_GetGenericInterface("IVRScreenshots_001", &err);
|
||||
if (!vrScreenshots || err != vr::VRInitError_None) {
|
||||
std::cerr << "GetGenericInterface IVRScreenshots_001 failed: " << err << std::endl;
|
||||
vr::VR_Shutdown();
|
||||
return 1;
|
||||
}
|
||||
|
||||
vr::ScreenshotHandle_t handle = 0;
|
||||
vr::EVRScreenshotError scErr = vrScreenshots->TakeStereoScreenshot(&handle, preview, vr_file);
|
||||
if (scErr != vr::VRScreenshotError_None) {
|
||||
std::cerr << "TakeStereoScreenshot failed with error: " << scErr << std::endl;
|
||||
vr::VR_Shutdown();
|
||||
return 1;
|
||||
}
|
||||
std::cout << "Screenshot requested successfully, handle: " << handle << std::endl;
|
||||
|
||||
// Allow compositor frame loop to flush the write
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1500));
|
||||
|
||||
vr::VR_Shutdown();
|
||||
return 0;
|
||||
}
|
||||
"""
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Capture in-headset screenshots from Valve Steam Frame")
|
||||
parser.add_argument("--host", default=DEFAULT_HOST, help="Steam Frame SSH destination")
|
||||
parser.add_argument("--key", default=DEFAULT_SSH_KEY, help="Path to devkit private key")
|
||||
parser.add_argument("--delay", type=int, default=0, help="Seconds to wait before taking screenshot")
|
||||
parser.add_argument("--out-dir", default="docs/screenshots", help="Directory to save downloaded screenshots")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.delay > 0:
|
||||
print(f"Waiting {args.delay} seconds before capture (put headset on or position UI)...")
|
||||
time.sleep(args.delay)
|
||||
|
||||
b64_cpp = base64.b64encode(CPP_CODE.encode('utf-8')).decode('utf-8')
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
remote_preview = f"/tmp/pixelvr_snap_{timestamp}_preview.png"
|
||||
remote_stereo = f"/tmp/pixelvr_snap_{timestamp}_stereo.png"
|
||||
|
||||
remote_cmd = (
|
||||
f"if [ ! -f /tmp/pixelvr_screenshot_bin ]; then "
|
||||
f" echo '{b64_cpp}' | base64 -d > /tmp/pixelvr_screenshot.cpp && "
|
||||
f" g++ -O2 /tmp/pixelvr_screenshot.cpp -L/opt/steamvr/bin/linuxarm64 -lopenvr_api -Wl,-rpath,/opt/steamvr/bin/linuxarm64 -o /tmp/pixelvr_screenshot_bin ; "
|
||||
f"fi ; "
|
||||
f"/tmp/pixelvr_screenshot_bin {remote_preview} {remote_stereo}"
|
||||
)
|
||||
|
||||
print(f"Triggering screenshot capture on {args.host}...")
|
||||
res = subprocess.run([
|
||||
"ssh", "-i", args.key, "-o", "StrictHostKeyChecking=no", args.host, remote_cmd
|
||||
], capture_output=True, text=True)
|
||||
|
||||
if res.returncode != 0:
|
||||
print("Error capturing screenshot:\n", res.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(res.stdout.strip())
|
||||
|
||||
os.makedirs(args.out_dir, exist_ok=True)
|
||||
local_preview = os.path.join(args.out_dir, f"screenshot_{timestamp}_preview.png")
|
||||
local_stereo = os.path.join(args.out_dir, f"screenshot_{timestamp}_stereo.png")
|
||||
|
||||
print(f"Downloading captured frames to {args.out_dir}...")
|
||||
subprocess.run(["scp", "-i", args.key, "-o", "StrictHostKeyChecking=no", f"{args.host}:{remote_preview}", local_preview], check=True)
|
||||
subprocess.run(["scp", "-i", args.key, "-o", "StrictHostKeyChecking=no", f"{args.host}:{remote_stereo}", local_stereo], check=True)
|
||||
|
||||
print(f"Saved: {local_preview}")
|
||||
print(f"Saved: {local_stereo}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||