No description
  • C 95.9%
  • Makefile 2.3%
  • Shell 1.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Phillippe Pelzer 26e106fb77 CI: remove redundant ci.yml; keep only release.yml
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.
2026-06-09 22:34:57 +02:00
.forgejo CI: remove redundant ci.yml; keep only release.yml 2026-06-09 22:34:57 +02:00
docs CI: remove redundant ci.yml; keep only release.yml 2026-06-09 22:34:57 +02:00
examples Add FFmpeg integration: bluray: playback for menu targets 2026-06-09 20:31:10 +02:00
src Clearer diagnostics for AACS-encrypted / BD-J discs 2026-06-09 22:25:42 +02:00
third_party
.gitignore Add Forgejo CI: Linux + Windows release builds on tag 2026-06-09 21:21:06 +02:00
Makefile CI: silence benign warnings; make release upload diagnostic 2026-06-09 22:14:57 +02:00
README.en.md Add docs/FFMPEG_INTEGRATION.md and link it from both READMEs 2026-06-09 20:59:46 +02:00
README.md Add docs/FFMPEG_INTEGRATION.md and link it from both READMEs 2026-06-09 20:59:46 +02:00

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_PAGE submenus).
  • 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)

  1. Openbd_open(), then bd_get_disc_info() for validation and BD-J detection.
  2. Navigation — the overlay callback is registered before bd_play(); then a bd_read_ext() read loop handles STILL/STILL_TIME events (bd_read_skip_still) so the HDMV Interactive Graphics (IG) menu actually draws. The top menu is forced with bd_menu_call() if needed.
  3. 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 via bd_get_sound_effect(). If the IG parse fails, the tool falls back to the overlay callback (drawn DRAW regions → individual buttons).
  4. 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.264 high, YUV420P, + AAC, GLOBAL_HEADER + +faststart; decoders and encoders flushed at EOF) and then removed.
  5. Playermenu.json + index.html + player.js are 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 .mpls number: playPlaylist targets are .mpls numbers (use directly). playTitle (JUMP_TITLE) targets are title numbers; translate via BLURAY_TITLE_INFO.playlist before passing to -playlist if 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 via bd_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_PAGE and immediate PLAY_PL targets; links via JUMP_TITLE/JUMP_OBJECT or register/conditional commands are left as action.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-seconds adjustable) — ample for a menu loop.
  • Audio requires FFmpeg ≥ 5.1 (AVChannelLayout API). With older FFmpeg: build with make 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/empty menu.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.