One file drop-in media player web app for using video and audio files served using basic HTTP directory listing
See the codeSingle-file audio + video player web app for media libraries served from basic HTTP directory listings (NGINX/Apache/IIS/etc). Try it live on GitHub Pages: pseudosavant.github.io/player.html/player.html.
It can be used as:
.srt / .vtt)..m3u playlists and play .m3u playlists from the web (when CORS allows).player.html.json, and optionally host a static manifest.json for PWA install metadata.player.html.json) with sensible precedence and export support.player.html.json + manifest.json)player.html is designed to be a drop-in audio and video player that does not require any configuration or other files.
./dist/player.html or ./dist/player.min.html into a folder that is served over HTTP with directory listing enabled.player.html in your browser.player.html treats the directory listing HTML as an API for enumerating files/folders, so it works with simple web servers (see Supported web servers).
Serve player.html from https:// (or http://localhost) and use your browser's "Install" option to install it as an app.
Once installed, it can be launched like a local media player. Some platforms also support opening local media files directly into the app (PWA file handlers).
If you are customizing a hosted deployment, the usual pairing is:
player.html.json for player behavior and default library locationmanifest.json for install-time app metadata such as name, icons, colors, start_url, and scopedist/player.html and dist/player.min.html) with zero runtime dependencies.MP4, M4V, MOV, MKV, WEBM, OGG, etc)..srt and .vtt).MP3, WAV, AAC, M4A, MKA, OGG, etc).sessionStorage for the session..m3u playlists.manifest.json if present).player.html in the same library root, folder, search, playlist, media item, timestamp, and subtitle selection.#dr= share-state value.showDirectoryPicker() where supported; browsers without folder access fall back to opening one local media file with <input type="file">.title, description) plus favicon and PWA manifest support.If you want to generate simple links into your own player.html instance, append #url=<media-url> instead of constructing the full encoded #dr= state value.
Example:
https://example.com/player.html#url=https://example.com/videos/example.webm
For this repository served locally:
http://localhost:8080/dist/player.html#url=http://localhost:8080/videos/webm-big-buck-bunny_trailer.webm
The player loads and attempts to play the media URL, then best-effort browses the containing folder so the file browser is populated. Folder browsing requires same-origin access or CORS support. If the media URL contains characters such as # or &, URL-encode the media URL before appending it.
saturation: 0).Upload) or by config (settings.poster-image).player.html.json) and exported from the UI. PWA install metadata can also be exported as manifest.json. See Configuration and PWA manifest (player.html.json + manifest.json).Use player.html with your own look and branding:
settings.poster-image)? in the app to see the list).CTRL+V to play the URL currently in your clipboard.Shift+S, or use File metadata modal -> Actions -> Take screenshot.app.options.cloud).Open the playlist panel using the playlist button next to the file sources. From there you can:
.m3u playlists from a URL (requires CORS) or from a local file.m3u fileNotes:
.m3u files can include relative URLs; they are resolved against the playlist URL (or the current page for local files).* Be careful with concurrency. Increasing the setting above 1 does make it generate thumbnails much faster. But it is very easy for HTTP requests for generating thumbnails to saturate a connection enough that the main video gets starved for bandwidth. Especially if you browse into a folder with many dozens of videos in it. Thumbnails are generated lazily as tiles scroll into view to reduce bandwidth.
** Animated thumbnails can consume a lot of data. The experience may degrade on slower network connections
player.html can use server-side thumbnails for any video that has one available. It will fall back to generating thumbnails in the browser otherwise. The server-side thumbnail files must follow the common filename convention of using the video file name, with the extension replaced with an image extension, in the same folder as the video. Note: The image files must be shown by your web server's directory browsing (may require mime-type adjustments on some servers) feature to show up in player.html.
myMedia.mp4myMedia.jpgThis repo includes prerender-video-thumbnails.py, which recursively generates missing thumbnails for all video files under a root folder. It never overwrites existing thumbnails.
What it does:
mp4, mkv, webm, mov, etc).ffmpeg.Requires ffmpeg on your PATH.
Run it from the repo root:
uv run prerender-video-thumbnails.py [ROOT]
Examples:
uv run prerender-video-thumbnails.py
uv run prerender-video-thumbnails.py "C:\Media\Videos" --ext webp --seek 2.5 --max-dim 640
Options:
ROOT - Root folder to scan (default: current directory)--ext - Thumbnail extension to create: jpg, jpeg, png, gif, webp (default: jpg)--seek - Seek time in seconds for the thumbnail frame (default: 5.0)--max-dim - Maximum output dimension in pixels for the largest side (default: 512)This repo includes prerender-audio-thumbnails.py, which recursively creates missing folder.jpg sidecar files for audio folders. It scans each folder for embedded artwork and uses the first one it finds.
What it does:
folder.jpg, it skips the folder.folder.jpg.Requires ffmpeg on your PATH.
Run it from the repo root:
uv run prerender-audio-thumbnails.py [ROOT]
Examples:
uv run prerender-audio-thumbnails.py
uv run prerender-audio-thumbnails.py "C:\Media\Audio" --debug
Options:
ROOT - Root folder to scan (default: current directory)--debug - Print ffmpeg debug outputplayer.html.json + manifest.json)player.html can load a configuration file named player.html.json from the same folder as player.html.
You can rename player.html (for example to index.html) and still keep the config filename as player.html.json.
This lets you host the player at a folder root while starting in a sub-folder media library.
player.html also generates a manifest in-memory by default for PWA installs. If you host a real manifest.json next to player.html, the page will use that instead. This is useful when you want to control install metadata such as:
start_urlscopeIn practice:
player.html.json for player behavior and defaultsmanifest.json for install-time PWA metadata and brandingPriority order (highest to lowest):
localStorage (setting-* keys)player.html.jsonsrc/js/config.jsplayer.html.json uses the same shape as app.options. You can create one by:
Export settings fileplayer.html.json next to player.htmlYou can create a starter manifest.json by:
Export manifest.jsonmanifest.json next to player.htmlNotes:
manifest.json is present, it is preferred over the generated inline manifest.manifest.json is not present, player.html falls back to its generated inline manifest automatically.player.html.json is the right place to set startLocation and other player defaults.manifest.json is the right place to set install/app metadata. It is intentionally separate from shareable URL hash state.manifest.json is a starter. For a hosted deployment, you will often want to replace name, short_name, colors, and icon URLs with your own values.{
"settings": {
"hue": 323,
"saturation": 100,
"auto-subtitles": false,
"subtitle-font": "sans",
"subtitle-size": "100%",
"subtitle-position": "author",
"subtitle-color": "#ffffff",
"subtitle-background": "#000000",
"subtitle-shadow": false,
"wake-lock": true,
"watch-history": true,
"thumbnailing": true,
"animate": true,
"search-depth": 3
},
"thumbnails": {
"timestamps": [0.005, 0.01, 0.015],
"size": 320,
"mime": { "type": "image/webp", "quality": 0.2 },
"cache": true,
"resizeQuality": "high",
"concurrency": 1
},
"volumeExponent": 1.8
}
manifest.json{
"short_name": "player.html",
"name": "player.html",
"description": "Drop player.html in a folder of video and audio files and play them on the web",
"background_color": "#FFFFFF",
"theme_color": "#ff0099",
"color": "#ff0099",
"icons": [
{
"src": "./icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"display": "standalone",
"start_url": "./player.html",
"scope": "./",
"file_handlers": [
{
"action": "./player.html",
"accept": {
"audio/*": [".mp3", ".wav", ".aac", ".m4a", ".mka", ".ogg"],
"video/*": [".mp4", ".mov", ".webm", ".mkv"]
}
}
]
}
User-facing display and subtitle defaults (most commonly customized):
settings.hue: Theme hue (number)settings.saturation: Theme saturation from 0 to 100 (use 0 for grayscale themes)settings.hue and settings.saturation together.settings.auto-subtitles: Auto-load matching subtitle file (true/false)settings.subtitle-font: sans, serif, mono, casualsettings.subtitle-size: Percentage string. Presets in UI: 50%, 75%, 100%, 150%, 200%. Arbitrary percentages are accepted (clamped to 25%-400%).settings.subtitle-position: author, 90, 75, 60, 35, 20settings.subtitle-color: Hex color (for example #ffffff)settings.subtitle-background: Hex color (for example #000000)settings.subtitle-shadow: true uses transparent cue backgrounds with offset text shadow based on settings.subtitle-background; false uses solid cue backgroundsettings.poster-image: Optional custom player poster image URL. Supports relative URL, absolute URL, or data URI.settings.wake-lock: Prevent screen sleep while media is playing, when supported (true/false)settings.watch-history: Remember recent playback positions and watched state in IndexedDB (true/false)settings.blur: Enable UI blur effects (true/false)settings.transitions: Enable UI transitions (true/false)settings.thumbnailing: Enable video thumbnail generation (true/false)settings.animate: Enable animated thumbnails (true/false)settings.playlist-depth: Folder depth used when adding folders to playlists (1 to 3)settings.search-depth: Client-side search depth (0 disables search and hides the search UI, 1 indexes the current folder only, higher values recurse deeper up to 6)Poster image behavior:
localStorage value (setting-poster-image) from uploaded custom posterplayer.html.json value (settings.poster-image)1920x1080 and encoded as WebP when supported (PNG fallback).player.html.json includes settings.poster-image (including uploaded data URI values).Thumbnail timing and generation:
thumbnails.timestamps: Relative capture positions for generated thumbnails (0.0 to 1.0)thumbnails.size: Max thumbnail width in pxthumbnails.mime.type: Output image mime type (for example image/webp)thumbnails.mime.quality: Output quality (0.0 to 1.0)thumbnails.cache: Enable thumbnail cache in IndexedDB (true/false). Generated video thumbnails are stored as image Blobs and materialized as temporary object URLs when tiles render.thumbnails.resizeQuality: Canvas resize quality (low, medium, high)thumbnails.concurrency: Concurrent video thumbnail generation jobsAudio thumbnail options:
audioThumbnails.concurrency: Concurrent cover-art lookupsaudioThumbnails.sidecarConcurrency: Concurrent sidecar checks per audio filePlayback and UI timing:
startLocation: Optional initial folder/media URL. Relative paths resolve from the page URL.
index.html + player.html.json deployments where media lives in a sub-folder like videos/.startLocation values to root-relative paths so they stay portable across host/domain/port changes.volumeExponent: Volume curve exponentupdateRate.timeupdate: UI update throttle for media timeupdate events (ms)updateRate.trickHover: UI update throttle for trick-hover previews (ms)Cloud source integration:
cloud.onedrive.clientIdcloud.gdrive.developerKeycloud.gdrive.clientIdcloud.gdrive.appIdDiagnostics:
debug: Enable non-error console logging (true/false)Compatibility notes:
subtitles.* keys are still accepted on load for compatibility, but settings.* is the canonical configuration surface for user-facing defaults.Use startLocation to control the initial folder or media URL opened by the player.
index.html at root, media in videos/ sub-folder:/index.html
/player.html.json
/videos/ (directory browsing enabled)
player.html.json:
{
"startLocation": "videos/"
}
{
"startLocation": "https://media.example.com/videos/"
}
Requirements for cross-server startLocation:
player.html can parse.If you have an idea for player.html, please open a GitHub issue and label it as an enhancement.
Feature requests are preferred over pull requests.
The more detail you include, the easier it is to implement quickly and correctly. Helpful details include:
A useful quality check before posting:
Does this feature request for player.html have enough detail to clearly implement it? What other details should I consider adding?The thumbnail scripts require ffmpeg to be available on your PATH.
Windows (winget):
winget install --id=Gyan.FFmpeg --exact
macOS (Homebrew):
brew install ffmpeg
Linux (package manager examples):
sudo apt install ffmpeg
If your source is already H.264 video + AC3 audio, you can often remux to MP4 for broad playback compatibility while keeping the original quality.
Remuxing (-c copy) is a container change, not a re-encode. For copied streams it is lossless and typically much faster than transcoding.
This command:
faststart) for better web playbackffmpeg -i "input.mkv" -map 0 -c copy -map_chapters 0 -movflags +faststart "output.mp4"
If your source audio is something less broadly supported in browsers/devices (for example DTS or EAC3), you can keep the original H.264 video stream and only transcode audio to AC3:
This keeps video remuxed/lossless (-c:v copy) and only re-encodes audio.
ffmpeg -i "input.mkv" -map 0 -c:v copy -c:a ac3 -b:a 640k -map_chapters 0 -movflags +faststart "output.mp4"
src/player.html is the dev template (it references src/styles.css, src/js/*, and src/svg/*).dist/player.html and dist/player.min.html are generated output; do not edit them by hand.The build inlines CSS, JS, SVGs, and assets into portable single-file outputs.
uv run build.py
By default this writes:
dist/player.html (unminified)dist/player.min.html (minified)For a dev-only build that skips minification:
uv run build.py --dev
For continuous rebuilds while editing:
uv run build.py --watch
You can combine watch mode with dev mode:
uv run build.py --watch --dev
The latest version of these browsers is supported:
Modern browser/device media stacks have improved significantly. In practice, many current platforms now handle 5.1 AC3 / Dolby Digital playback out of the box.
Real-world compatibility can still vary by OS version, hardware decoder, browser version, and how audio is routed, but MP4 + H.264 + AC3 has worked well across iPhone/iOS Safari, Chrome/Edge, Roku devices, and Xbox in testing.
The latest version of these web servers (others may work as well):
autoindex on)mod_autoindex)Directory Browsing)player.html uses folder.api to consume HTTP directory listings like an APIplayer.html uses video-thumbnail.js to render thumbnails from video file URLsplayer.html uses audio-thumbnail.js to render thumbnails from audio file URLs© 2026 Paul Ellis
866 followers · starred Nov 2025
62 followers · starred Oct 2026
One file drop-in media player web app for using video and audio files served using basic HTTP directory listing
See the codeSingle-file audio + video player web app for media libraries served from basic HTTP directory listings (NGINX/Apache/IIS/etc). Try it live on GitHub Pages: pseudosavant.github.io/player.html/player.html.
It can be used as:
.srt / .vtt)..m3u playlists and play .m3u playlists from the web (when CORS allows).player.html.json, and optionally host a static manifest.json for PWA install metadata.player.html.json) with sensible precedence and export support.player.html.json + manifest.json)player.html is designed to be a drop-in audio and video player that does not require any configuration or other files.
./dist/player.html or ./dist/player.min.html into a folder that is served over HTTP with directory listing enabled.player.html in your browser.player.html treats the directory listing HTML as an API for enumerating files/folders, so it works with simple web servers (see Supported web servers).
Serve player.html from https:// (or http://localhost) and use your browser's "Install" option to install it as an app.
Once installed, it can be launched like a local media player. Some platforms also support opening local media files directly into the app (PWA file handlers).
If you are customizing a hosted deployment, the usual pairing is:
player.html.json for player behavior and default library locationmanifest.json for install-time app metadata such as name, icons, colors, start_url, and scopedist/player.html and dist/player.min.html) with zero runtime dependencies.MP4, M4V, MOV, MKV, WEBM, OGG, etc)..srt and .vtt).MP3, WAV, AAC, M4A, MKA, OGG, etc).sessionStorage for the session..m3u playlists.manifest.json if present).player.html in the same library root, folder, search, playlist, media item, timestamp, and subtitle selection.#dr= share-state value.showDirectoryPicker() where supported; browsers without folder access fall back to opening one local media file with <input type="file">.title, description) plus favicon and PWA manifest support.If you want to generate simple links into your own player.html instance, append #url=<media-url> instead of constructing the full encoded #dr= state value.
Example:
https://example.com/player.html#url=https://example.com/videos/example.webm
For this repository served locally:
http://localhost:8080/dist/player.html#url=http://localhost:8080/videos/webm-big-buck-bunny_trailer.webm
The player loads and attempts to play the media URL, then best-effort browses the containing folder so the file browser is populated. Folder browsing requires same-origin access or CORS support. If the media URL contains characters such as # or &, URL-encode the media URL before appending it.
saturation: 0).Upload) or by config (settings.poster-image).player.html.json) and exported from the UI. PWA install metadata can also be exported as manifest.json. See Configuration and PWA manifest (player.html.json + manifest.json).Use player.html with your own look and branding:
settings.poster-image)? in the app to see the list).CTRL+V to play the URL currently in your clipboard.Shift+S, or use File metadata modal -> Actions -> Take screenshot.app.options.cloud).Open the playlist panel using the playlist button next to the file sources. From there you can:
.m3u playlists from a URL (requires CORS) or from a local file.m3u fileNotes:
.m3u files can include relative URLs; they are resolved against the playlist URL (or the current page for local files).* Be careful with concurrency. Increasing the setting above 1 does make it generate thumbnails much faster. But it is very easy for HTTP requests for generating thumbnails to saturate a connection enough that the main video gets starved for bandwidth. Especially if you browse into a folder with many dozens of videos in it. Thumbnails are generated lazily as tiles scroll into view to reduce bandwidth.
** Animated thumbnails can consume a lot of data. The experience may degrade on slower network connections
player.html can use server-side thumbnails for any video that has one available. It will fall back to generating thumbnails in the browser otherwise. The server-side thumbnail files must follow the common filename convention of using the video file name, with the extension replaced with an image extension, in the same folder as the video. Note: The image files must be shown by your web server's directory browsing (may require mime-type adjustments on some servers) feature to show up in player.html.
myMedia.mp4myMedia.jpgThis repo includes prerender-video-thumbnails.py, which recursively generates missing thumbnails for all video files under a root folder. It never overwrites existing thumbnails.
What it does:
mp4, mkv, webm, mov, etc).ffmpeg.Requires ffmpeg on your PATH.
Run it from the repo root:
uv run prerender-video-thumbnails.py [ROOT]
Examples:
uv run prerender-video-thumbnails.py
uv run prerender-video-thumbnails.py "C:\Media\Videos" --ext webp --seek 2.5 --max-dim 640
Options:
ROOT - Root folder to scan (default: current directory)--ext - Thumbnail extension to create: jpg, jpeg, png, gif, webp (default: jpg)--seek - Seek time in seconds for the thumbnail frame (default: 5.0)--max-dim - Maximum output dimension in pixels for the largest side (default: 512)This repo includes prerender-audio-thumbnails.py, which recursively creates missing folder.jpg sidecar files for audio folders. It scans each folder for embedded artwork and uses the first one it finds.
What it does:
folder.jpg, it skips the folder.folder.jpg.Requires ffmpeg on your PATH.
Run it from the repo root:
uv run prerender-audio-thumbnails.py [ROOT]
Examples:
uv run prerender-audio-thumbnails.py
uv run prerender-audio-thumbnails.py "C:\Media\Audio" --debug
Options:
ROOT - Root folder to scan (default: current directory)--debug - Print ffmpeg debug outputplayer.html.json + manifest.json)player.html can load a configuration file named player.html.json from the same folder as player.html.
You can rename player.html (for example to index.html) and still keep the config filename as player.html.json.
This lets you host the player at a folder root while starting in a sub-folder media library.
player.html also generates a manifest in-memory by default for PWA installs. If you host a real manifest.json next to player.html, the page will use that instead. This is useful when you want to control install metadata such as:
start_urlscopeIn practice:
player.html.json for player behavior and defaultsmanifest.json for install-time PWA metadata and brandingPriority order (highest to lowest):
localStorage (setting-* keys)player.html.jsonsrc/js/config.jsplayer.html.json uses the same shape as app.options. You can create one by:
Export settings fileplayer.html.json next to player.htmlYou can create a starter manifest.json by:
Export manifest.jsonmanifest.json next to player.htmlNotes:
manifest.json is present, it is preferred over the generated inline manifest.manifest.json is not present, player.html falls back to its generated inline manifest automatically.player.html.json is the right place to set startLocation and other player defaults.manifest.json is the right place to set install/app metadata. It is intentionally separate from shareable URL hash state.manifest.json is a starter. For a hosted deployment, you will often want to replace name, short_name, colors, and icon URLs with your own values.{
"settings": {
"hue": 323,
"saturation": 100,
"auto-subtitles": false,
"subtitle-font": "sans",
"subtitle-size": "100%",
"subtitle-position": "author",
"subtitle-color": "#ffffff",
"subtitle-background": "#000000",
"subtitle-shadow": false,
"wake-lock": true,
"watch-history": true,
"thumbnailing": true,
"animate": true,
"search-depth": 3
},
"thumbnails": {
"timestamps": [0.005, 0.01, 0.015],
"size": 320,
"mime": { "type": "image/webp", "quality": 0.2 },
"cache": true,
"resizeQuality": "high",
"concurrency": 1
},
"volumeExponent": 1.8
}
manifest.json{
"short_name": "player.html",
"name": "player.html",
"description": "Drop player.html in a folder of video and audio files and play them on the web",
"background_color": "#FFFFFF",
"theme_color": "#ff0099",
"color": "#ff0099",
"icons": [
{
"src": "./icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"display": "standalone",
"start_url": "./player.html",
"scope": "./",
"file_handlers": [
{
"action": "./player.html",
"accept": {
"audio/*": [".mp3", ".wav", ".aac", ".m4a", ".mka", ".ogg"],
"video/*": [".mp4", ".mov", ".webm", ".mkv"]
}
}
]
}
User-facing display and subtitle defaults (most commonly customized):
settings.hue: Theme hue (number)settings.saturation: Theme saturation from 0 to 100 (use 0 for grayscale themes)settings.hue and settings.saturation together.settings.auto-subtitles: Auto-load matching subtitle file (true/false)settings.subtitle-font: sans, serif, mono, casualsettings.subtitle-size: Percentage string. Presets in UI: 50%, 75%, 100%, 150%, 200%. Arbitrary percentages are accepted (clamped to 25%-400%).settings.subtitle-position: author, 90, 75, 60, 35, 20settings.subtitle-color: Hex color (for example #ffffff)settings.subtitle-background: Hex color (for example #000000)settings.subtitle-shadow: true uses transparent cue backgrounds with offset text shadow based on settings.subtitle-background; false uses solid cue backgroundsettings.poster-image: Optional custom player poster image URL. Supports relative URL, absolute URL, or data URI.settings.wake-lock: Prevent screen sleep while media is playing, when supported (true/false)settings.watch-history: Remember recent playback positions and watched state in IndexedDB (true/false)settings.blur: Enable UI blur effects (true/false)settings.transitions: Enable UI transitions (true/false)settings.thumbnailing: Enable video thumbnail generation (true/false)settings.animate: Enable animated thumbnails (true/false)settings.playlist-depth: Folder depth used when adding folders to playlists (1 to 3)settings.search-depth: Client-side search depth (0 disables search and hides the search UI, 1 indexes the current folder only, higher values recurse deeper up to 6)Poster image behavior:
localStorage value (setting-poster-image) from uploaded custom posterplayer.html.json value (settings.poster-image)1920x1080 and encoded as WebP when supported (PNG fallback).player.html.json includes settings.poster-image (including uploaded data URI values).Thumbnail timing and generation:
thumbnails.timestamps: Relative capture positions for generated thumbnails (0.0 to 1.0)thumbnails.size: Max thumbnail width in pxthumbnails.mime.type: Output image mime type (for example image/webp)thumbnails.mime.quality: Output quality (0.0 to 1.0)thumbnails.cache: Enable thumbnail cache in IndexedDB (true/false). Generated video thumbnails are stored as image Blobs and materialized as temporary object URLs when tiles render.thumbnails.resizeQuality: Canvas resize quality (low, medium, high)thumbnails.concurrency: Concurrent video thumbnail generation jobsAudio thumbnail options:
audioThumbnails.concurrency: Concurrent cover-art lookupsaudioThumbnails.sidecarConcurrency: Concurrent sidecar checks per audio filePlayback and UI timing:
startLocation: Optional initial folder/media URL. Relative paths resolve from the page URL.
index.html + player.html.json deployments where media lives in a sub-folder like videos/.startLocation values to root-relative paths so they stay portable across host/domain/port changes.volumeExponent: Volume curve exponentupdateRate.timeupdate: UI update throttle for media timeupdate events (ms)updateRate.trickHover: UI update throttle for trick-hover previews (ms)Cloud source integration:
cloud.onedrive.clientIdcloud.gdrive.developerKeycloud.gdrive.clientIdcloud.gdrive.appIdDiagnostics:
debug: Enable non-error console logging (true/false)Compatibility notes:
subtitles.* keys are still accepted on load for compatibility, but settings.* is the canonical configuration surface for user-facing defaults.Use startLocation to control the initial folder or media URL opened by the player.
index.html at root, media in videos/ sub-folder:/index.html
/player.html.json
/videos/ (directory browsing enabled)
player.html.json:
{
"startLocation": "videos/"
}
{
"startLocation": "https://media.example.com/videos/"
}
Requirements for cross-server startLocation:
player.html can parse.If you have an idea for player.html, please open a GitHub issue and label it as an enhancement.
Feature requests are preferred over pull requests.
The more detail you include, the easier it is to implement quickly and correctly. Helpful details include:
A useful quality check before posting:
Does this feature request for player.html have enough detail to clearly implement it? What other details should I consider adding?The thumbnail scripts require ffmpeg to be available on your PATH.
Windows (winget):
winget install --id=Gyan.FFmpeg --exact
macOS (Homebrew):
brew install ffmpeg
Linux (package manager examples):
sudo apt install ffmpeg
If your source is already H.264 video + AC3 audio, you can often remux to MP4 for broad playback compatibility while keeping the original quality.
Remuxing (-c copy) is a container change, not a re-encode. For copied streams it is lossless and typically much faster than transcoding.
This command:
faststart) for better web playbackffmpeg -i "input.mkv" -map 0 -c copy -map_chapters 0 -movflags +faststart "output.mp4"
If your source audio is something less broadly supported in browsers/devices (for example DTS or EAC3), you can keep the original H.264 video stream and only transcode audio to AC3:
This keeps video remuxed/lossless (-c:v copy) and only re-encodes audio.
ffmpeg -i "input.mkv" -map 0 -c:v copy -c:a ac3 -b:a 640k -map_chapters 0 -movflags +faststart "output.mp4"
src/player.html is the dev template (it references src/styles.css, src/js/*, and src/svg/*).dist/player.html and dist/player.min.html are generated output; do not edit them by hand.The build inlines CSS, JS, SVGs, and assets into portable single-file outputs.
uv run build.py
By default this writes:
dist/player.html (unminified)dist/player.min.html (minified)For a dev-only build that skips minification:
uv run build.py --dev
For continuous rebuilds while editing:
uv run build.py --watch
You can combine watch mode with dev mode:
uv run build.py --watch --dev
The latest version of these browsers is supported:
Modern browser/device media stacks have improved significantly. In practice, many current platforms now handle 5.1 AC3 / Dolby Digital playback out of the box.
Real-world compatibility can still vary by OS version, hardware decoder, browser version, and how audio is routed, but MP4 + H.264 + AC3 has worked well across iPhone/iOS Safari, Chrome/Edge, Roku devices, and Xbox in testing.
The latest version of these web servers (others may work as well):
autoindex on)mod_autoindex)Directory Browsing)player.html uses folder.api to consume HTTP directory listings like an APIplayer.html uses video-thumbnail.js to render thumbnails from video file URLsplayer.html uses audio-thumbnail.js to render thumbnails from audio file URLs© 2026 Paul Ellis
866 followers · starred Nov 2025
62 followers · starred Oct 2026