⎇
server
shared
webclient
.dockerignore55 B
.gitignore59 B
biome.json616 B
compose.yaml348 B
Containerfile393 B
LICENSE33.7 KB
README.md8.3 KB
READMERaw

Pico Pixel Player icon

Pico Pixel Player is a lightweight, self-hostable media player with an ElysiaJS backend and SolidJS frontend. It lets you play your audio and video files directly from your server with a clean, responsive UI.

Features

  • Lightweight: Minimal dependencies, fast performance
  • Easy Setup: One JSON config file for users and libraries
  • Responsive UI: Works seamlessly on desktop and mobile
  • Offline Capable: Progressive Web App (PWA) support and local caching of files
  • Transcoding: Optionally transcode files at a selected bitrate and format to your client
  • Video playback: Responsive inline video, theater/fullscreen modes, chapters, and text subtitles
  • Directory Listing: Browse files in their original directory structure
  • Multiple Users and Libraries: Several logins, each seeing only the libraries assigned to it

Screenshot

screenshot

Setup

  1. Clone the repository:
git clone <repository-url>
cd pico-pixel-player
  1. Generate a password hash for each user. The password is read from stdin, so it never lands in your shell history or in ps:
echo -n 'your-password' | bun run --cwd server src/index.ts --hash-password
  1. Write a config.json next to compose.yaml. The keys of roots are the names shown as top-level folders in the UI; the values are the paths inside the container, so they must match the mounts in compose.yaml:
{
  "roots": {
    "music": "/mnt/music",
    "videos": "/mnt/videos"
  },
  "users": [
    { "name": "alice", "passwordHash": "$argon2id$...", "roots": ["music", "videos"] },
    { "name": "bob", "passwordHash": "$argon2id$...", "roots": ["music"] }
  ]
}
  1. Start the service:
docker-compose up  # or podman-compose up

Runtime Dependencies

If you choose to not run this via the include Containerfile, you need to ensure these dependencies are installed:

  • bun: Used for building, also the backend is using some bun-specific APIs
  • tar: To bundle playlists to a single file when downloading them
  • ffmpeg: For transcoding audio and video
  • avifenc: For transcoding cover art
  • mediainfo: For probing media file metadata

Configuration

You can configure the backend with the following environment variables:

  • COVER_REGEX: JS-compatible regex to match filenames in a directory to find a matching cover art. Searches the directory structure upwards. Set to an empty string to disable cover detection.
  • EXCLUDE_EXTENSION: comma-separated list of file extensions to ignore completely, e.g. txt,log,nfo. If unset, uses a safe set of text files common in downloaded music archives. Useful because some files can get mis-scanned, e.g. some CD-scan .log files get scanned as mp1
  • SCAN_CONCURRENCY: maximum number of parallel mediainfo processes while scanning uncached folders. Defaults to 8; invalid or non-positive values fall back to the default.
  • CONFIG: path to the JSON file holding the roots and users. Defaults to ./config.json. The server refuses to start without a valid one. Basic assets are not protected, so the UI itself will load fine even when not logged in, so when you get signed out for whatever reason (e.g. by the server restarting), you can still access the UI and play already synced files without problems.
    • Passwords are stored as argon2 hashes produced by --hash-password, which reads the password from stdin. Auth tokens are kept in memory, so a server restart signs everyone out.
    • Roots are virtual top-level folders: a path is <root name>/<path inside that root>, and requesting a root a user does not have returns 403. Root directories must exist at startup, and symlinks that leave their root are skipped when listing and refused when read.

Non-Goals

  • Metadata-Based Views: No support for filtering by genre, artist, etc. Use other projects for this
  • Server-side user state: Playlists, synced files and options live in the browser, so they are per-device rather than per-account and do not follow a user to another machine. Finer-grained permissions (read-only, per-folder) are not planned.

FAQ

  • What do the "Streaming mode" options do?
    • Each transcoding profile (video and music) picks how its transcode is delivered to the player.
    • mse feeds the transcode into a SourceBuffer via Media Source Extensions. This is the only mode that can seek inside a transcode, because a seek simply restarts ffmpeg at an offset. In exchange it only works for containers and codecs that have an MSE byte stream format, which the options menu greys out the rest based on.
    • progressive hands the transcode straight to the media element as it is produced. It plays whatever the browser can decode - notably video in webm, which MSE cannot handle - but cannot seek.
    • buffered makes the backend transcode the whole file before sending anything. By default the backend sends the transcoded media as it progresses, which uses "chunked transfer", i.e. without the usual content-length of normal HTTP requests. Some browser (safari cough cough) seem to be very selective about the sources of <audio> elements, and choke on sources that are transferred via this method. This mode improves compatibility with those browsers, at the cost of a significant startup delay. It does not enable seeking - the transcode endpoint serves no range requests either way.
  • Why does an MKV file stop or fail when transcoding is disabled?
    • No-transcoding serves the original file directly, so playback and seeking depend entirely on browser support. Browsers support WebM more consistently than arbitrary Matroska/MKV files; an MKV may start playing and still fail when seeking. Use transcoding or remux the file to a browser-friendly container such as MP4. Remuxing only changes the container and does not fix unsupported codecs.
  • Why is AC-3 audio missing?
    • AC-3 decoding is not consistently available in browsers. With transcoding enabled, the server re-encodes audio to the selected browser-compatible codec. Disabling transcoding leaves the original AC-3 stream, even if the video itself plays.
  • Why are bitmap subtitles such as PGS not shown, and why can FFmpeg not convert them to WebVTT?
    • PGS subtitles contain timed bitmap images rather than text. FFmpeg can copy or convert bitmap subtitles to other bitmap formats, but its WebVTT encoder accepts text subtitles only; converting PGS to text requires OCR. Supporting them would require OCR, custom image subtitle rendering, or burning them into a re-encoded video, none of which is currently implemented.
  • The playback stops automatically at some point on my mobile device when I put the browser in the background or turning my screen off
    • In my testing at least mobile firefox behaves this way, with no apparent solution. It seems to stop after changing the sources of the audio element at some point. Either use a browser that doesn't behave like this (Chrome on Android seems to work), or wrap the PWA in a native app (e.g. https://www.pwabuilder.com/)
  • I'm getting "Quota exceeded" errors when trying to sync files or
  • My synced files randomly don't work, are missing
    • By default websites aren't granted infinite storage or have persisten data. The frontend requests these permissions by calling navigator.storage.persist() on startup, which causes some browsers to show a permissions popup (e.g. firefox), while others just seem to ignore this request and use a heuristic like interaction amount and bookmarking instead. So if you have the error described above you either are running out of actual disk storage, or you need to check what heuristics your browser uses and trigger them.

Known issues

  • Some files (especially mp3) have weird metadata issues (e.g. a padding gap between the ID3 tag and the first frame sync) that stop the file-type library from identifying them. The server now falls back to mediainfo for MIME detection in that case, so these files list and stream normally without any intervention. If a file still fails to be listed or played, running ffmpeg -i INPUTFILE -c copy OUTPUTFILE on it to rewrite the container usually fixes it

Attributions