- C 95.9%
- Makefile 2.3%
- Shell 1.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
ci.yml duplicated the Linux build that release.yml already does. Only the tag-driven release workflow remains; plain pushes no longer trigger a workflow. |
||
| .forgejo | ||
| docs | ||
| examples | ||
| src | ||
| third_party | ||
| .gitignore | ||
| Makefile | ||
| README.en.md | ||
| README.md | ||
English · Nederlands
bluray_menu_converter
A command-line tool that reads a Blu-ray disc or ISO and converts only the menus into an interactive HTML5/JavaScript player.
It parses the HDMV Interactive Graphics (IG) stream directly and produces:
- Multiple screens / submenus — every menu page (incl.
SET_BUTTON_PAGEsubmenus). - Button states — the real normal / selected / activated graphics per button.
- Button linkage — clicking navigates to the right submenu; title/film actions are recognized and passed through (but not followed).
- Sound effects — menu navigation/click sounds, played on focus/activate.
- Motion background — for a motion menu, the menu background video (via FFmpeg).
Falls back to a simple button overview (overlay callback) if the IG parse yields nothing.
The feature film is never touched. The menu background video is captured from the stream libbluray plays while the menu is on screen — that is, by definition, the menu background, so film content can never be converted by accident.
BD-J menus are not supported (the tool warns). Only the first/HDMV menu is processed — usually the main menu.
Project layout
bluray-menu-converter/
├── src/ source code (C)
├── third_party/ vendored libs (stb_image_write.h)
├── Makefile build via pkg-config
├── README.en.md (English) · README.md (Nederlands)
| File | Purpose |
|---|---|
src/main.c |
CLI, opens the disc, BD-J check, navigation, IG-parse orchestration, fallback. |
src/hdmv_ig.c/.h |
Direct IG-stream parser: ICS/PDS/ODS → screens + buttons with 3 states. |
src/hdmv_cmd.c/.h |
Decodes HDMV navigation commands → actions (navigate/playTitle/external). |
src/tsdemux.c/.h |
Demux 192-byte M2TS → IG PID → PES elementary stream. |
src/sound.c/.h |
Menu sound effects (bd_get_sound_effect) → WAV. |
src/rle_decode.c/.h |
Shared RLE+palette → RGBA8888 decoder + PNG export (contains the stb impl). |
src/submenu_extract.c/.h |
Cross-playlist: extract in-mux submenu IGs (bd_open_file_dec). |
src/menu_merge.c/.h |
Merge top menu + submenu compositions → one model, global ids, link resolution. |
src/playgen.c/.h |
Generate play.sh/play.bat (FFmpeg bluray: playback per button). |
src/extract.c/.h |
Overlay callback (fallback buttons) + FFmpeg transcoding to MP4. |
src/html_gen.c/.h |
Generates menu.json, index.html and the multi-screen player.js. |
third_party/stb_image_write.h |
Vendored single-header PNG encoder (public domain, Sean Barrett). |
Makefile |
Build via pkg-config. |
Dependencies
- libbluray ≥ 1.2 (
libbluray-dev) - FFmpeg ≥ 5.1 dev libs:
libavformat,libavcodec,libavutil,libswscale,libswresample(the last one only for audio — see video-only build) - pkg-config, gcc or clang
- To read menu video from a disc: read permissions and (for commercial discs)
libaacs+ a valid key database.
Installation
Debian/Ubuntu
sudo apt-get install build-essential pkg-config libbluray-dev \
libavformat-dev libavcodec-dev libavutil-dev libswscale-dev libswresample-dev
macOS (Homebrew)
brew install libbluray ffmpeg pkg-config
Windows (MSYS2 / MinGW-w64) — open the MSYS2 MinGW64 shell:
pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-pkg-config \
mingw-w64-x86_64-libbluray mingw-w64-x86_64-ffmpeg make
Building
make # normal build (video + AAC audio)
make NO_AUDIO=1 # video-only (no libswresample; works with older FFmpeg)
make check # syntax check only (no link)
make clean
The Makefile gathers all compiler/linker flags via pkg-config. Result: the
bluray_menu_converter binary.
Usage
./bluray_menu_converter [options] <bluray-path-or-iso>
| Option | Meaning |
|---|---|
-o, --output DIR |
Output directory (default output). |
--no-video |
Buttons + menu graphics only; no background video. |
--menu-video PATH |
Advanced: use an explicit MENU clip instead of auto-capture. Never point at the feature film. |
--demo |
Generate sample buttons without a disc (test the player standalone). |
--max-seconds N |
Maximum capture/navigation time (default 20). |
-v, --verbose |
Verbose logging. |
-h, --help |
Help. |
By default the menu background video is captured automatically from the menu itself —
this also works for ISOs (libbluray reads the stream; no loose .m2ts needed).
Examples
# Mounted disc or ISO — menu + (if present) motion menu background:
./bluray_menu_converter /media/bluray_disc
./bluray_menu_converter film.iso
# Buttons + menu graphics only, no video:
./bluray_menu_converter --no-video /media/bluray_disc
# View/test the player without a disc:
./bluray_menu_converter --demo
# Serve the result:
cd output && python3 -m http.server 8000
# open http://localhost:8000
Sample output:
Scanning Blu-ray: /media/bluray_disc
Disc: MY_MOVIE
Navigating HDMV menu (incl. menu background)...
Menu parsed: 2 screen(s) from the IG stream.
Transcoding menu background to MP4 (H.264/AAC)... Video OK
Generating HTML/JS...
Done! 2 screen(s), 7 button(s), + sounds, + background video.
Output structure
output/
├── index.html # page + inline CSS
├── player.js # loads menu.json, multi-screen SPA
├── menu.json # screens, buttons (3 states), neighbors, actions, sounds
├── streams/
│ └── menu_background.mp4 # motion menu background (H.264/AAC, +faststart)
├── images/
│ ├── scr0_btn1_n.png # button states: _n normal, _s selected, _a activated
│ ├── scr0_btn1_s.png … # (fallback path: btn_N.png + menu_overlay.png)
└── snd/
└── snd_00.wav … # menu sound effects
menu.json (abbreviated):
{
"menu": { "width": 1920, "height": 1080, "startScreen": 0,
"video": "streams/menu_background.mp4" },
"sounds": [ "snd/snd_00.wav", "snd/snd_01.wav" ],
"screens": [
{ "id": 0, "defaultFocus": 1, "buttons": [
{ "id": 1, "x": 760, "y": 820, "w": 400, "h": 90,
"images": { "normal": "images/scr0_btn1_n.png",
"selected": "images/scr0_btn1_s.png",
"activated": "images/scr0_btn1_a.png" },
"neighbors": { "up": -1, "down": 2, "left": -1, "right": -1 },
"sound": { "selected": 0, "activated": 1 },
"action": { "type": "navigate", "page": 1, "target": 0, "op": "SET_BUTTON_PAGE" } }
] }
]
}
The player scales buttons via a design box (percentage coordinates in an
aspect-locked overlay) and swaps each button's normal/selected/activated image.
Navigation: mouse, arrow keys (button neighbors, with a spatial fallback),
Enter/Space (activate), Esc/Backspace (back) and [/] (browse all
extracted screens, incl. submenus). navigate actions switch screens; title/film
actions raise a toast. Override the action handling with your own handler:
window.onMenuAction = function (btn, action) { /* action.type, action.target, btn.id */ };
How it works (brief)
- Open —
bd_open(), thenbd_get_disc_info()for validation and BD-J detection. - Navigation — the overlay callback is registered before
bd_play(); then abd_read_ext()read loop handlesSTILL/STILL_TIMEevents (bd_read_skip_still) so the HDMV Interactive Graphics (IG) menu actually draws. The top menu is forced withbd_menu_call()if needed. - IG-stream parse (deep extraction) — the IG PID is demuxed from the captured
menu stream (192-byte M2TS → PES) and the Interactive Graphics segments (ICS/PDS/ODS)
are parsed directly. This yields every page (screen), each button's
normal/selected/activated graphics (ODS → RGBA via the shared decoder, BT.709/601
→ PNG), the neighbors, sound refs and the decoded navigation commands →
actions (
SET_BUTTON_PAGE= submenu; title/playlist actions are recognized but not followed). Sound effects are dumped to WAV viabd_get_sound_effect(). If the IG parse fails, the tool falls back to the overlay callback (drawn DRAW regions → individual buttons). - Menu background video — during step 2 the A/V stream libbluray plays under the
menu is captured (from the first IG draw) to a temporary
.m2ts— by definition the menu background, so never the feature film. A still menu yields no video. The recording is transcoded with FFmpeg to web MP4 (H.264high, YUV420P, + AAC,GLOBAL_HEADER++faststart; decoders and encoders flushed at EOF) and then removed. - Player —
menu.json+index.html+player.jsare generated.
FFmpeg integration (playing a menu target)
Full guide: docs/FFMPEG_INTEGRATION.md — building FFmpeg with libbluray, the
bluray:protocol, the action→command mapping, the title→mpls translation, scripts, the libavformat C example, and transcoding.
The player handles navigate actions itself (in-player screen switches). For
buttons that play a title/playlist, the tool also writes output/play.sh
and output/play.bat that run the right command via FFmpeg's bluray: input
protocol (FFmpeg built with --enable-gpl --enable-libbluray):
./output/play.sh # list the playable buttons
./output/play.sh play /mnt/disc # play the "Play" button's target
Action → command mapping:
action.type |
FFmpeg command |
|---|---|
navigate |
none — in-player screen switch |
playPlaylist target=N |
ffplay -playlist N "bluray:<disc>" (N = .mpls number) |
playTitle target=T |
translate title T → mpls, then -playlist <mpls> |
The bluray: protocol takes playlist, angle, chapter as input options
(before the URL/-i); point it at the folder/mount/ISO that contains BDMV.
Encrypted retail discs need libaacs + a key DB. To drive playback from the web
player, override window.onMenuAction(btn, action); for a programmatic C example
see examples/play_bd.c:
gcc examples/play_bd.c -o play_bd `pkg-config --cflags --libs libavformat libavcodec libavutil`
./play_bd /mnt/disc 1 # open .mpls playlist 1
Title index vs
.mplsnumber:playPlaylisttargets are.mplsnumbers (use directly).playTitle(JUMP_TITLE) targets are title numbers; translate viaBLURAY_TITLE_INFO.playlistbefore passing to-playlistif playback is wrong.
Limitations & honest caveats
- BD-J menus (Java) are not supported; the tool warns and stops if no HDMV menu is available.
- Submenus: pages within the same IG composition (
SET_BUTTON_PAGE) and submenus that live in a separate playlist (with an in-mux IG stream) are both extracted as screens and merged into one model. Every extracted screen is reachable — directly via a resolved button link, or by browsing with[/]. Not extractable: sub-path IG submenus (their sub-clip name isn't exposed by the public API) — those are reported and skipped. (Extraction reads the clip directly viabd_open_file_dec; the feature film is never opened.) - Dynamic HDMV-VM: HDMV navigation can be register-based or conditional (the menu VM
computes the target at runtime). Static extraction cannot execute this. Button→submenu
links are auto-resolved for
SET_BUTTON_PAGEand immediatePLAY_PLtargets; links viaJUMP_TITLE/JUMP_OBJECTor register/conditional commands are left asaction.type:"playTitle"/"external"(the target submenu, if extracted, is still reachable with[/]). Inherently approximate. - Button graphics: with IG parse the real button definitions (position + 3 states)
come from the stream. If the IG parse fails (e.g. a menu drawn purely as a composite),
the tool falls back to the drawn overlay regions (one state per button) or, for a
full-plane composite, to
menu_overlay.png. Otherwise use--demo. - Menu background video comes from the stream the menu itself plays — never the
feature film. A still (non-motion) menu yields no video; the player falls back to
menu_overlay.png/the buttons. To force a specific clip, use--menu-video <path.m2ts>(advanced; give a menu clip, not the film). By default max. 20 s is recorded (--max-secondsadjustable) — ample for a menu loop. - Audio requires FFmpeg ≥ 5.1 (
AVChannelLayoutAPI). With older FFmpeg: build withmake NO_AUDIO=1(video-only). Blu-ray audio (AC3/DTS/TrueHD/LPCM) is re-encoded to AAC, not stream-copied. - Windows: use the MSYS2/MinGW toolchain (Makefile + pkg-config). Plain MSVC is not supported by this build.
Verification status
The multi-screen HTML5/JS player has been tested in a real browser (12/12): building
- switching screens, button-state swapping (normal/selected/activated), neighbor and
spatial navigation, submenu linkage (
navigate), title/film actions via toast, back/Esc history, and graceful handling of a missing/emptymenu.json.
The C tool (incl. the IG bitstream parser) is written against the verified libbluray/FFmpeg APIs and the BD-ROM HDMV spec, and adversarially reviewed (verdict: compile-ready, IG parse sound). However: this development environment has no libbluray/FFmpeg and no Blu-ray, so the C has not been compiled or run on real IG data here. End-to-end validation requires a Linux/MSYS2 environment with the libraries and a real disc/ISO. On the very first real run: visually check one decoded button PNG (an off-by-one in the RLE/EOL would show as diagonal shearing).
License
Project code: free to use/modify. stb_image_write.h is public domain (see the header).
libbluray and FFmpeg fall under their own licenses (LGPL/GPL) — respect those when
distributing.