Play feature under the menu; add mpv path; simplify VLC launch

Root causes found this session (all player-side, disc is valid -- bd_info
validates it and mpv plays the ISO's feature directly):

- Plain 'vlc <iso>' can't run ANY BD-J: system Java is 26, and libbluray's BD-J
  needs a JVM where System.setSecurityManager() works (<=21). run-vlc.sh pins
  mise Temurin 8, which is why it gets a menu at all.
- The BD-J VFS-cache 'error creating /tmp/libbluray-bdj-cache' flood is
  libbluray's SecurityManager.canWrite() denying the cache root while an Xlet
  context is active. Fixed by pointing TMPDIR at LIBBLURAY_CACHE_ROOT (buda
  root), which canWrite() allows unconditionally.
- Menu->playback failed with 'blurayReleaseVout: subpicture channel exists':
  VLC botches the background->playlist transition for an idle BD-J menu. Now the
  Xlet starts the feature immediately and draws the menu on top, so VLC stays in
  normal playing mode; MAIN FEATURE just hides the menu, any key brings it back,
  and the feature loops.

Also: drop the redundant tools/vlc-plugins swap (system VLC already ships the
bluray plugin), and add scripts/run-mpv.sh to play the feature with no BD-J/JVM
at all. Vendored the SDK helloworld sample for reference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
happy 2026-08-25 16:25:42 -04:00
parent 48c172cb80
commit 4c270b744f
10 changed files with 362 additions and 25 deletions

View file

@ -30,20 +30,23 @@ build/adjustments.iso
```
`make run` previews the disc by pointing VLC's Blu-ray access module at the
`build/disc` BDMV **folder**, not the ISO:
`build/disc` BDMV **folder** (a real player reads the same tree from the ISO):
```sh
vlc --bluray-menu bluray:///absolute/path/to/build/disc
```
Why the folder and not the ISO: when libbluray opens a UDF `.iso`, its BD-J
runtime must copy `BDMV/JAR/00000.jar` out of the image into a temp cache dir
before the JVM can load it. On this setup that write is refused by libbluray's
BD-J `SecurityManager`, so the Xlet class is never found and VLC silently falls
back to playing the raw playlist with no menu. Reading the folder lets
libbluray load the JAR directly, so the menu Xlet runs. The ISO is still the
artifact you burn/distribute; real players read the JAR from the disc
filesystem directly and are unaffected.
BD-J cache note: to load the menu Xlet, libbluray copies the disc's JAR and
fonts into a VFS cache under `java.io.tmpdir` (default
`/tmp/libbluray-bdj-cache`). Its BD-J `SecurityManager.canWrite()` allows writes
under the buda/persistent roots unconditionally, but only allows the VFS cache
root when no Xlet context is active on the calling thread -- and that extraction
frequently runs while a context *is* active, so the write is denied and you see
a flood of `error creating .../libbluray-bdj-cache` followed by the Xlet failing
to load (no menu). `scripts/run-vlc.sh` fixes this by pointing `TMPDIR` at the
same directory it gives libbluray as `LIBBLURAY_CACHE_ROOT` (the buda root), so
the cache path is always writable. This is a libbluray/VLC runtime quirk, not a
defect in the authored disc.
The initial menu is a Java Xlet overlay with a few bonus-feature placeholders
and a hidden menu stub. The actual generated video is a short ffmpeg SMPTE bars

29
scripts/run-mpv.sh Normal file
View file

@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
# Play the disc's feature straight from the ISO (or a BDMV folder) with mpv.
#
# mpv links libbluray and plays Blu-ray *titles/playlists* directly. It does NOT
# run BD-J -- there is no menu, no Xlet, no Java involved. This is purely "open
# the disc and watch the movie", and unlike the VLC/BD-J path it Just Works with
# no JVM or cache setup. Use scripts/run-vlc.sh if you want the BD-J menu.
#
# Pass a path to override the default (an .iso image or a BDMV folder).
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TARGET="${1:-$ROOT_DIR/build/adjustments.iso}"
if [[ ! -e "$TARGET" ]]; then
echo "Missing disc target: $TARGET" >&2
echo "Run: make" >&2
exit 1
fi
if [[ -d "$TARGET" ]]; then
DEVICE="$(cd "$TARGET" && pwd)"
else
DEVICE="$(cd "$(dirname "$TARGET")" && pwd)/$(basename "$TARGET")"
fi
# bd://longest plays the main title; --bluray-device points at the image/folder.
exec mpv --bluray-device="$DEVICE" "bd://longest"

View file

@ -4,15 +4,9 @@ set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
MISE_BIN="${MISE_BIN:-$HOME/.local/bin/mise}"
# Default to the on-disk BDMV folder, not the ISO.
#
# When libbluray opens a UDF *image* (.iso), its BD-J stack must first copy
# BDMV/JAR/00000.jar out of the image into a temp cache dir. On this setup that
# write is refused by libbluray's BD-J SecurityManager (usingUdf=true, no disc
# root), so the JAR never mounts, the Xlet class is never found, and VLC falls
# back to playing the raw playlist with no menu. Opening the *folder* lets
# libbluray read the JAR directly, so the Xlet (menu) loads. Pass an explicit
# path to override (folder or .iso).
# Default to the on-disk BDMV folder; pass an explicit path to override (a
# folder or an .iso). The folder is just the simpler preview target -- a real
# player reads the JAR from the ISO's disc filesystem directly.
TARGET="${1:-$ROOT_DIR/build/disc}"
if [[ -d "$TARGET" && ! -d "$TARGET/BDMV" ]]; then
@ -43,14 +37,34 @@ if [[ -n "$JDK8_HOME" ]]; then
export LD_LIBRARY_PATH="$JDK8_HOME/jre/lib/amd64/server:${LD_LIBRARY_PATH:-}"
fi
LOCAL_VLC_PLUGINS="$ROOT_DIR/tools/vlc-plugins/usr/lib/vlc/plugins"
if [[ -d "$LOCAL_VLC_PLUGINS" ]]; then
export VLC_PLUGIN_PATH="$LOCAL_VLC_PLUGINS:/usr/lib/vlc/plugins${VLC_PLUGIN_PATH:+:$VLC_PLUGIN_PATH}"
fi
# Note: no VLC_PLUGIN_PATH juggling. This system's VLC already ships the bluray
# access plugin (/usr/lib/vlc/plugins/access/liblibbluray_plugin.so); the old
# tools/vlc-plugins copy was only needed for split-package installs that lack it.
# Give libbluray writable BD-J storage roots, and -- the important part -- put
# the BD-J VFS cache *under* the cache (BUDA) root.
#
# libbluray copies the disc's JAR and fonts into a VFS cache under java.io.tmpdir
# (default /tmp/libbluray-bdj-cache). Its BD-J SecurityManager.canWrite() checks
# roots in this order: budaRoot -> persistentRoot -> (if an Xlet context is
# active: DENY) -> cacheRoot. That extraction often runs while an Xlet context is
# live on the thread, so the write is denied before the cacheRoot allowance is
# ever reached -- the flaky "error creating .../libbluray-bdj-cache" /
# "failed to create temporary cache directory" cascade that stops the Xlet (menu)
# from loading. budaRoot/persistentRoot are allowed unconditionally, so pointing
# TMPDIR (== java.io.tmpdir == the VFS cache's base) at the buda root makes the
# cache path start with budaRoot and always pass the write check.
BDJ_RUN_DIR="$ROOT_DIR/build/bdj-runtime"
export LIBBLURAY_CACHE_ROOT="$BDJ_RUN_DIR/cache"
export LIBBLURAY_PERSISTENT_ROOT="$BDJ_RUN_DIR/persistent"
export TMPDIR="$LIBBLURAY_CACHE_ROOT"
mkdir -p "$LIBBLURAY_CACHE_ROOT" "$LIBBLURAY_PERSISTENT_ROOT"
# start each preview from a clean VFS cache
rm -rf "$LIBBLURAY_CACHE_ROOT/libbluray-bdj-cache"
if [[ -d "$TARGET" ]]; then
ABS_TARGET="$(cd "$TARGET" && pwd)"
else
ABS_TARGET="$(cd "$(dirname "$TARGET")" && pwd)/$(basename "$TARGET")"
fi
exec vlc --reset-plugins-cache --bluray-menu "bluray://$ABS_TARGET"
exec vlc --bluray-menu "bluray://$ABS_TARGET"

View file

@ -7,8 +7,15 @@ import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.media.ControllerEvent;
import javax.media.ControllerListener;
import javax.media.EndOfMediaEvent;
import javax.media.Manager;
import javax.media.MediaLocator;
import javax.media.Player;
import javax.media.Time;
public class AdjustmentsMenu extends Container implements KeyListener {
public class AdjustmentsMenu extends Container implements KeyListener, ControllerListener {
private static final String[] MAIN_ITEMS = {
"MAIN FEATURE",
"BONUS: WAITING ROOM OUTTAKES",
@ -30,6 +37,8 @@ public class AdjustmentsMenu extends Container implements KeyListener {
private boolean hidden;
private int codeProgress;
private String status = "BD-J scaffold loaded";
private Player featurePlayer;
private boolean menuShown = true;
public AdjustmentsMenu() {
setFocusable(true);
@ -111,7 +120,11 @@ public class AdjustmentsMenu extends Container implements KeyListener {
selected = 0;
status = "Returned to approved materials";
} else if ("MAIN FEATURE".equals(item)) {
status = "Main feature is playlist 00000";
// The feature is already playing behind the menu; just hide the
// menu to reveal it. Press the menu key (or ENTER) to bring it back.
status = "Playing main feature -- press ENTER for menu";
setMenuVisible(false);
return;
} else if ("SETTINGS".equals(item)) {
status = "Settings stub selected";
} else {
@ -121,6 +134,47 @@ public class AdjustmentsMenu extends Container implements KeyListener {
repaint();
}
// Called once by the Xlet at startup. Start the feature playing straight
// away and leave it running for the whole session. This keeps VLC in normal
// "playing" mode with a live playlist behind the menu, so selecting an item
// never triggers the background->playlist transition that VLC botches
// ("blurayReleaseVout: subpicture channel exists", no video). The menu is
// just drawn on top and toggled with setMenuVisible().
public void startPlayback() {
if (featurePlayer != null) {
return;
}
try {
// libbluray's "bd" protocol; PLAYLIST:00000 -> BDMV/PLAYLIST/00000.mpls.
featurePlayer = Manager.createPlayer(new MediaLocator("bd://PLAYLIST:00000"));
featurePlayer.addControllerListener(this);
featurePlayer.start();
} catch (Exception e) {
featurePlayer = null;
status = "Playback failed: " + e.getMessage();
}
repaint();
}
// Toggle only this menu Container, not the whole HScene, so the scene (and
// its key listener) stays alive while the video shows underneath.
private void setMenuVisible(boolean visible) {
menuShown = visible;
setVisible(visible);
if (visible) {
requestFocus();
}
repaint();
}
public void controllerUpdate(ControllerEvent event) {
// Loop the feature so there is always a live playlist behind the menu.
if (event instanceof EndOfMediaEvent && featurePlayer != null) {
featurePlayer.setMediaTime(new Time(0));
featurePlayer.start();
}
}
private void unlockHidden() {
hidden = true;
selected = 0;
@ -131,6 +185,14 @@ public class AdjustmentsMenu extends Container implements KeyListener {
public void keyPressed(KeyEvent e) {
int code = e.getKeyCode();
// When the menu is hidden (feature playing full screen), any key brings
// the menu back over the still-running video.
if (!menuShown) {
setMenuVisible(true);
return;
}
String[] items = hidden ? HIDDEN_ITEMS : MAIN_ITEMS;
if (code == KeyEvent.VK_UP) {

View file

@ -29,6 +29,9 @@ public class MyXlet extends Thread implements Xlet {
scene.setVisible(true);
menu.requestFocus();
scene.requestFocus();
// Start the feature immediately so VLC always has a live playlist behind
// the menu (avoids the background->playlist transition it botches).
menu.startPlayback();
start();
}

View file

@ -0,0 +1,6 @@
*~
*.class
discdir
META-INF
*.iso

View file

@ -0,0 +1,80 @@
# Copyright (C) 2022 John Törnblom
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; see the file COPYING. If not see
# <http://www.gnu.org/licenses/>.
DISC_LABEL := helloworld
#
# Host tools
#
MAKEFILE_DIR := $(dir $(realpath $(lastword $(MAKEFILE_LIST))))
BDJSDK_HOME ?= $(MAKEFILE_DIR)/../../
BDSIGNER := $(BDJSDK_HOME)/host/bin/bdsigner
MAKEFS := $(BDJSDK_HOME)/host/bin/makefs
JAVA8_HOME ?= $(BDJSDK_HOME)/host/jdk8
JAVAC := $(JAVA8_HOME)/bin/javac
JAR := $(JAVA8_HOME)/bin/jar
export JAVA8_HOME
#
# Compilation artifacts
#
CLASSPATH := $(BDJSDK_HOME)/target/lib/enhanced-stubs.zip
SOURCES := $(wildcard src/org/homebrew/*.java)
JFLAGS := -Xlint:-options -source 1.3 -target 1.3
#
# Disc files
#
TMPL_DIRS := $(shell find $(BDJSDK_HOME)/resources/AVCHD/ -type d)
TMPL_FILES := $(shell find $(BDJSDK_HOME)/resources/AVCHD/ -type f)
DISC_DIRS := $(patsubst $(BDJSDK_HOME)/resources/AVCHD%,discdir%,$(TMPL_DIRS)) \
discdir/BDMV/JAR
DISC_FILES := $(patsubst $(BDJSDK_HOME)/resources/AVCHD%,discdir%,$(TMPL_FILES)) \
discdir/BDMV/JAR/00000.jar
all: $(DISC_LABEL).iso
discdir:
mkdir -p $(DISC_DIRS)
discdir/BDMV/JAR/00000.jar: discdir $(SOURCES)
$(JAVAC) $(JFLAGS) -cp $(CLASSPATH) $(SOURCES)
$(JAR) cf $@ -C src/ .
$(BDSIGNER) -keystore $(BDJSDK_HOME)/resources/sig.ks $@
discdir/%: discdir
cp $(BDJSDK_HOME)/resources/AVCHD/$* $@
$(DISC_LABEL).iso: $(DISC_FILES)
$(MAKEFS) -m 16m -t udf -o T=bdre,v=2.50,L=$(DISC_LABEL) $@ discdir
clean:
rm -rf META-INF $(DISC_LABEL).iso discdir src/org/homebrew/*.class
test: $(DISC_LABEL).iso
vlc $<

View file

@ -0,0 +1,82 @@
package org.homebrew;
import java.awt.Color;
import java.awt.Container;
import java.awt.Font;
import java.awt.Graphics;
import java.io.StringWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
public class LoggingUI extends Container {
private final Font font = new Font(null, Font.PLAIN, 14);
private final Color fontColor = new Color(240, 240, 240);
private final Color bgColor = new Color(5, 5, 5);
private final ArrayList rows = new ArrayList();
private static LoggingUI instance = null;
private LoggingUI() {
}
public static LoggingUI getInstance() {
if(instance == null) {
instance = new LoggingUI();
}
return instance;
}
public void paint(Graphics g) {
g.setFont(font);
g.setColor(bgColor);
g.fillRect(20, 20, getWidth() - 40, getHeight() - 40);
g.setColor(fontColor);
for(int i=0; i<rows.size(); i++) {
String row = (String)rows.get(i);
int len = g.getFontMetrics().stringWidth(row);
g.drawString(row, 30, 40 + (i*15));
}
}
public void log(String msg) {
rows.add(msg);
if(rows.size() > 45) {
rows.remove(0);
}
repaint();
}
public void log(Throwable t) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
StringBuffer sb = new StringBuffer();
t.printStackTrace(pw);
String st = sw.toString();
for(int i=0; i<st.length(); i++) {
char ch = st.charAt(i);
switch(ch) {
case '\t':
sb.append(" ");
break;
case '\n':
log(sb.toString());
sb = new StringBuffer();
break;
default:
sb.append(ch);
break;
}
}
if(sb.length() > 0) {
log(sb.toString());
}
}
}

View file

@ -0,0 +1,46 @@
package org.homebrew;
import java.awt.BorderLayout;
import javax.tv.xlet.Xlet;
import javax.tv.xlet.XletContext;
import org.havi.ui.HScene;
import org.havi.ui.HSceneFactory;
public class MyXlet extends Thread implements Xlet {
private HScene scene;
private LoggingUI logUI;
public void initXlet(XletContext context) {
logUI = LoggingUI.getInstance();
logUI.setSize(1280, 720);
scene = HSceneFactory.getInstance().getDefaultHScene();
scene.add(logUI, BorderLayout.CENTER);
scene.validate();
scene.repaint();
}
public void startXlet() {
logUI.setVisible(true);
scene.setVisible(true);
start();
}
public void pauseXlet() {
logUI.setVisible(false);
}
public void destroyXlet(boolean unconditional) {
interrupt();
logUI.setVisible(false);
scene.remove(logUI);
scene = null;
}
public void run() {
logUI.log("Hello, World!");
}
}

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<n:permissionrequestfile xmlns:n="urn:BDA:bdmv;PRF" orgid="0x56789abc" appid="0x4000">
<file value="true"></file>
<applifecyclecontrol value="true"></applifecyclecontrol>
<servicesel value="true"></servicesel>
<userpreferences read="true" write="true"></userpreferences>
<bd-vfs value="true"></bd-vfs>
<network>
<host action="connect,resolve,listen">*</host>
</network>
<bd-bindingunitarea value="true" />
</n:permissionrequestfile>