⎇
server
shared
webclient
.dockerignore55 B
.gitignore52 B
biome.json616 B
compose.yaml181 B
Containerfile341 B
LICENSE33.7 KB
README.md6.0 KB
READMERaw

Pico Pixel Player icon

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

Features

  • Lightweight: Minimal dependencies, fast performance
  • Easy Setup: Configure with just a .env file
  • 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
  • Directory Listing: Browse files in their original directory structure

Screenshot

screenshot

Setup

  1. Clone the repository:
git clone <repository-url>
cd pico-pixel-player
  1. Write/adjust these settings in a file called .env:
HOST_PORT=1234
MUSIC_ROOT=/path/to/your/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
  • 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.
  • AUTH: Same format as the string used for basic auth (username and password joined by a colon : and encoded to base64). If set, the server APIs return 401 unless the user is signed in with the given login details. 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.

Non-Goals

  • Metadata-Based Views: No support for filtering by genre, artist, etc. Use other projects for this
  • Multiple users: Right now everything besides the actual file hosting is stored on the server. When syncing is implemented, it will only be single storage. Permission systems are also not planned. If distinct permissions/syncs are absolutely required, hosting multiple instances is an option

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.
  • 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