Set up the camera
This guide uses a Linux installer layout. The camera renderer is a separate Node.js process. It needs the emulator, a current Volt build containing camera.html, and compatible renderer assets.
Prepare the renderer files
Section titled “Prepare the renderer files”Get the source version that matches your emulator release. From a PlusEMU source checkout, run:
cd CameraRenderernpm cinpx playwright install chromiumOn a Linux host missing Chromium system libraries, install them with npx playwright install-deps chromium. Install the browser as the account that will run the renderer.
Use the deployed Volt directory as CAMERA_PAGE_ROOT. For the Linux installer, confirm this file exists:
ls /var/www/hotel/client/camera.htmlIf it is missing, deploy a compatible Volt build before continuing.
Prepare compatible assets
Section titled “Prepare compatible assets”The renderer’s local asset layout differs from the installer’s public hotel-files layout. Do not point CAMERA_ASSET_ROOT at that directory without preparing the expected files.
For the example below, place compatible assets under /srv/plusemu-camera-assets with this structure:
/srv/plusemu-camera-assets/ furniture/nitro/*.nitro furniture/json/ProductData.json clothes/nitro/*.nitro clothes/json/FigureData.json clothes/json/FigureMap.json effects/nitro/*.nitro effects/json/EffectMap.json gamedata/json/HabboAvatarActions.json pets/*.nitroUse the asset set that matches your client. The current renderer requests .nitro filenames. A hotel asset pack containing only .hab files is not a drop-in local asset source for these defaults.
The example reads furniture data from the emulator through CAMERA_FURNIDATA_URL. Without that variable, the renderer also needs furniture/json/FurnitureData.json under the asset root.
Point CAMERA_IMAGES_ROOT at the directory containing Habbo-Stories/ for camera effects. In an installer layout, the image directory is /var/www/hotel/hotel-files/c_images.
An existing compatible asset service can replace local game assets through CAMERA_ASSET_ORIGIN. This must be an HTTPS origin, such as https://assets.example.com, with no path. It must serve /assets/... and /gamedata/... using the paths requested by the renderer. The renderer still reads camera effect images locally.
Set the shared secret
Section titled “Set the shared secret”Generate a private secret of at least 32 characters. For example:
openssl rand -hex 32Set the following keys inside the existing Camera section of the emulator’s Config/config.json. Keep its existing Effects list.
{ "RendererUrl": "http://127.0.0.1:3921/render", "Bearer": "replace-with-your-private-secret", "OutputDirectory": "/var/www/hotel/emulator/camera", "TimeoutSeconds": 15, "MaxConcurrency": 2}Keep the renderer private. RendererUrl must use HTTP, a private address, and the /render path. PlusEMU rejects redirects and public renderer addresses.
Give the emulator account write access to the output directory. The renderer and web server need read access. Keep the bearer out of client configuration and public files.
Start the renderer
Section titled “Start the renderer”From CameraRenderer, enter the same secret without saving it in shell history:
read -rsp 'Camera secret: ' CAMERA_RENDERER_SECRETexport CAMERA_RENDERER_SECRETStart the renderer with your prepared paths:
CAMERA_PAGE_ROOT=/var/www/hotel/client \CAMERA_ASSET_ROOT=/srv/plusemu-camera-assets \CAMERA_IMAGES_ROOT=/var/www/hotel/hotel-files/c_images \CAMERA_MEDIA_ROOT=/var/www/hotel/emulator/camera \CAMERA_FURNIDATA_URL=http://127.0.0.1:8080/api/gamedata/furnidata \CAMERA_RENDERER_CONFIG=/var/www/hotel/client/configuration/renderer-config.json \CAMERA_UI_CONFIG=/var/www/hotel/client/configuration/ui-config.json \node server.mjsLook for both Trusted camera renderer listening on 127.0.0.1:3921 and Trusted camera page ready. A listening process can still have asset or browser startup failures.
After the foreground check succeeds, run the same process under your service manager with these environment variables and a protected secret file. Restart it after each Volt deployment.
Serve the photos
Section titled “Serve the photos”Add this location inside the hotel’s nginx server block. Adjust the alias if you chose a different output directory.
location /camera/ { alias /var/www/hotel/emulator/camera/; autoindex off; limit_except GET HEAD { deny all; } add_header X-Content-Type-Options nosniff always; add_header Cache-Control "no-cache" always;}Check and reload nginx:
sudo nginx -tsudo systemctl reload nginxThis location serves image files, not the private renderer. The refresh policy also lets room thumbnails change without retaining an old image.
The installer already sets thumbnails.url in Volt’s ui-config.json to /camera/thumbnail/%thumbnail%.png. Check that it still has this value.
Restart PlusEMU to load the camera connection settings:
sudo systemctl restart plusemuCurrent fresh databases already contain camera tables. Updates apply database migrations at startup. Do not replay old SQL update files on a current installation.
Check a photo
Section titled “Check a photo”- Sign in with an account that has
camera.use. - Enter a room and take a photo.
- Apply an available effect and confirm that the preview changes.
- Purchase a wall photograph and place it.
- Capture a room thumbnail with an account that has room management rights.
- Reload the navigator and check the thumbnail.
For a generated photo, request its actual /camera/<guid>.png URL in the browser. A successful renderer preview alone does not confirm public photo delivery.
Troubleshoot the camera
Section titled “Troubleshoot the camera”| Symptom | Check |
|---|---|
| Renderer rejects captures | The emulator bearer and CAMERA_RENDERER_SECRET match, with at least 32 characters |
| Renderer listens but never becomes ready | camera.html, Chromium libraries, renderer file permissions, and the first asset error in its log |
| Missing asset error | The exact requested filename exists in the expected local tree or configured asset origin |
| Camera unavailable for one account | camera.enabled, camera.use, capture cooldowns, and daily quotas |
| Preview works but placed photos fail | The /camera/ web route, output directory permissions, and CAMERA_MEDIA_ROOT |
| Photo purchase fails | The account balance, point currency type 0, and a valid camera.item_id |
| Old photo draft fails | Take a new photo in the current room and session |
| Thumbnail stays unchanged | The thumbnail URL and cache rules on nginx and Cloudflare |
See camera settings for prices and quotas.