diff --git a/README.md b/README.md
index f1c603f..6780e1d 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/scripts/run-mpv.sh b/scripts/run-mpv.sh
new file mode 100644
index 0000000..53c66ba
--- /dev/null
+++ b/scripts/run-mpv.sh
@@ -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"
diff --git a/scripts/run-vlc.sh b/scripts/run-vlc.sh
index e6f79e7..fb0ee2f 100755
--- a/scripts/run-vlc.sh
+++ b/scripts/run-vlc.sh
@@ -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"
diff --git a/src/org/homebrew/AdjustmentsMenu.java b/src/org/homebrew/AdjustmentsMenu.java
index 1e94bc7..7dc7e26 100644
--- a/src/org/homebrew/AdjustmentsMenu.java
+++ b/src/org/homebrew/AdjustmentsMenu.java
@@ -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) {
diff --git a/src/org/homebrew/MyXlet.java b/src/org/homebrew/MyXlet.java
index 711d371..1f4c209 100644
--- a/src/org/homebrew/MyXlet.java
+++ b/src/org/homebrew/MyXlet.java
@@ -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();
}
diff --git a/vendor/bdj-sdk/samples/helloworld/.gitignore b/vendor/bdj-sdk/samples/helloworld/.gitignore
new file mode 100644
index 0000000..deb367f
--- /dev/null
+++ b/vendor/bdj-sdk/samples/helloworld/.gitignore
@@ -0,0 +1,6 @@
+*~
+*.class
+discdir
+META-INF
+*.iso
+
diff --git a/vendor/bdj-sdk/samples/helloworld/Makefile b/vendor/bdj-sdk/samples/helloworld/Makefile
new file mode 100644
index 0000000..1c85513
--- /dev/null
+++ b/vendor/bdj-sdk/samples/helloworld/Makefile
@@ -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
+# .
+
+
+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 $<
diff --git a/vendor/bdj-sdk/samples/helloworld/src/org/homebrew/LoggingUI.java b/vendor/bdj-sdk/samples/helloworld/src/org/homebrew/LoggingUI.java
new file mode 100644
index 0000000..df8be4b
--- /dev/null
+++ b/vendor/bdj-sdk/samples/helloworld/src/org/homebrew/LoggingUI.java
@@ -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 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 0) {
+ log(sb.toString());
+ }
+ }
+}
+
diff --git a/vendor/bdj-sdk/samples/helloworld/src/org/homebrew/MyXlet.java b/vendor/bdj-sdk/samples/helloworld/src/org/homebrew/MyXlet.java
new file mode 100644
index 0000000..9f97b20
--- /dev/null
+++ b/vendor/bdj-sdk/samples/helloworld/src/org/homebrew/MyXlet.java
@@ -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!");
+ }
+}
+
+
+
diff --git a/vendor/bdj-sdk/samples/helloworld/src/org/homebrew/bluray.MyXlet.perm b/vendor/bdj-sdk/samples/helloworld/src/org/homebrew/bluray.MyXlet.perm
new file mode 100644
index 0000000..b225fb3
--- /dev/null
+++ b/vendor/bdj-sdk/samples/helloworld/src/org/homebrew/bluray.MyXlet.perm
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+ *
+
+
+
\ No newline at end of file