[libbluray-devel] [Git][videolan/libbluray][master] 3 commits: Skip the rest of a clip once PCR has passed the play item OUT time
Petri Hintukainen (@hpi)
gitlab at videolan.org
Mon Oct 12 20:16:05 UTC 2026
Petri Hintukainen pushed to branch master at VideoLAN / libbluray
Commits:
955a0e56 by Daniel Oberlin at 2026-10-09T17:34:53-04:00
Skip the rest of a clip once PCR has passed the play item OUT time
A play item may reference only the beginning of a much longer clip.
m2ts_filter already wipes ES packets with PTS after OUT time, but the
clip was still delivered up to its last packet (the EP map gives no
earlier end when the video stream stops before OUT time). PCR packets
are passed through, so an application pacing playback on PCR waits for
the whole length of the clip.
Seen on Star Trek: The Original Series (season 1, disc 1): playlist
00017.mpls plays 5 seconds (IN 27000000, OUT 27225225) of clip 00173,
whose video ends at 604.9 s but whose audio and PCR run to 719 s. With
VLC the 5 second warning card stayed on screen for about two minutes.
Track PCR in the filter and report when it crosses OUT time. All
remaining ES packets then have PTS > OUT, so the reader skips to the end
of the clip. Only a continuous crossing (previous PCR <= OUT, step below
one second) is accepted, so STC discontinuities inside a clip keep the
old behaviour.
- - - - -
99145eab by Daniel Oberlin at 2026-10-09T17:34:53-04:00
BD-J: do not initialize "present" applications when loading a title
Applications with control code PRESENT were init()ed as soon as the
title's BDJO was loaded, leaving their AppProxy in PAUSED state. They
are only listed in the applications database and should stay NOT_LOADED
until another application launches them.
This breaks discs where a launcher Xlet starts the real application
and first checks its state. Seen on "Bond 50th Anniversary" (Fox): the
autostart Xlet com.macrovision.bdplus.Handshake looks up the Xlet named
by its "-BD+XLET:" parameter and calls AppProxy.start() only if
getState() == NOT_LOADED; otherwise it gives up ("Application state
wasn't NOT_LOADED. Can't load it."), and the disc stays on a black
screen.
Leave the proxy of a PRESENT application untouched. AppProxy.start()
already loads and initializes a NOT_LOADED application.
Tested by starting 115 Blu-ray discs (95 of them with BD-J titles) to their
menu with VLC before and after the change; no disc regressed.
- - - - -
448f8960 by Daniel Oberlin at 2026-10-09T17:34:53-04:00
BD-J: use JAR as unsigned when its signature can not be verified
The JVM refuses to load any class from a JAR whose signature block can
not be parsed or verified (SecurityException "cannot verify signature
block file META-INF/SIG-BD00" from the JAR verifier). The Xlet is never
loaded, and a disc whose menu is a BD-J application shows only its
background.
Seen on "Yes - Fragile" (Steven Wilson remix, Blu-ray audio disc) with
both OpenJDK 8 and 17.
Such an application is not authenticated, but it is still an unsigned
application. After a JAR is copied to the cache, check that the JVM
accepts its signature; if not, rewrite the cached copy without the
signature files.
- - - - -
4 changed files:
- src/libbluray/bdj/java/org/videolan/BDJLoader.java
- src/libbluray/bdj/java/org/videolan/VFSCache.java
- src/libbluray/bluray.c
- src/libbluray/decoders/m2ts_filter.c
Changes:
=====================================
src/libbluray/bdj/java/org/videolan/BDJLoader.java
=====================================
@@ -293,8 +293,11 @@ public class BDJLoader {
logger.info("Autostart xlet " + i + ": " + appTable[i].getInitialClass());
proxys[i].start();
} else if (code == AppEntry.PRESENT) {
- logger.info("Init xlet " + i + ": " + appTable[i].getInitialClass());
- proxys[i].init();
+ /* "Present" applications are listed in the applications database but are not
+ * loaded until something launches them. Leave the proxy in its current state
+ * (NOT_LOADED for a new application): applications that launch another one
+ * through AppProxy.start() may first check that it has not been loaded yet. */
+ logger.info("Present xlet " + i + ": " + appTable[i].getInitialClass());
} else {
logger.info("Unsupported xlet code (" +code+") xlet " + i + ": " + appTable[i].getInitialClass());
}
=====================================
src/libbluray/bdj/java/org/videolan/VFSCache.java
=====================================
@@ -27,6 +27,11 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.security.AccessController;
import java.security.PrivilegedAction;
+import java.util.Enumeration;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
import java.io.BDFileSystem;
import org.videolan.bdjo.AppCache;
@@ -160,6 +165,108 @@ class VFSCache {
Libbluray.cacheBdRomFile(relPath, dstPath);
logger.info("cached " + relPath);
+
+ if (!isJarVerifiable(dstPath)) {
+ logger.error("Signature of " + relPath + " can not be verified. Using it as unsigned JAR.");
+ removeJarSignature(dstPath);
+ }
+ }
+
+ /*
+ * Check that JVM accepts the JAR signature.
+ *
+ * JVM refuses to load any class from a JAR with a broken signature block
+ * (SecurityException from the JAR verifier). Such application is not
+ * authenticated, but it is still an (unsigned) application.
+ */
+ private static boolean isJarVerifiable(String path) {
+ JarFile jar = null;
+ try {
+ jar = new JarFile(path, true);
+ Enumeration entries = jar.entries();
+ while (entries.hasMoreElements()) {
+ JarEntry entry = (JarEntry)entries.nextElement();
+ if (!entry.isDirectory() && !entry.getName().startsWith("META-INF/")) {
+ /* opening first entry initializes the verifier and parses signature block */
+ jar.getInputStream(entry).close();
+ break;
+ }
+ }
+ } catch (SecurityException e) {
+ return false;
+ } catch (IOException e) {
+ /* not a signature problem */
+ } finally {
+ if (jar != null) {
+ try {
+ jar.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ return true;
+ }
+
+ private static boolean isSignatureEntry(String name) {
+ name = name.toUpperCase();
+ if (!name.startsWith("META-INF/") || name.indexOf('/', 9) >= 0) {
+ return false;
+ }
+ return name.startsWith("META-INF/SIG-") || name.endsWith(".SF") ||
+ name.endsWith(".RSA") || name.endsWith(".DSA") || name.endsWith(".EC");
+ }
+
+ /* Rewrite cached JAR without signature files */
+ private void removeJarSignature(String path) {
+ String tmpPath = path + ".unsigned";
+ JarFile jar = null;
+ ZipOutputStream out = null;
+ boolean ok = false;
+ byte[] buffer = new byte[64*1024];
+
+ try {
+ jar = new JarFile(path, false);
+ out = new ZipOutputStream(new FileOutputStream(tmpPath));
+ Enumeration entries = jar.entries();
+ while (entries.hasMoreElements()) {
+ JarEntry entry = (JarEntry)entries.nextElement();
+ if (isSignatureEntry(entry.getName())) {
+ continue;
+ }
+ out.putNextEntry(new ZipEntry(entry.getName()));
+ if (!entry.isDirectory()) {
+ InputStream in = jar.getInputStream(entry);
+ try {
+ int length;
+ while ((length = in.read(buffer)) > 0) {
+ out.write(buffer, 0, length);
+ }
+ } finally {
+ in.close();
+ }
+ }
+ out.closeEntry();
+ }
+ ok = true;
+ } catch (IOException e) {
+ logger.error("Error removing signature from " + path + ": " + e);
+ } finally {
+ try {
+ if (out != null)
+ out.close();
+ if (jar != null)
+ jar.close();
+ } catch (IOException e) {
+ ok = false;
+ }
+ }
+
+ File tmp = new File(tmpPath);
+ File dst = new File(path);
+ if (!ok || !dst.delete() || !tmp.renameTo(dst)) {
+ logger.error("Error replacing " + path);
+ tmp.delete();
+ }
}
private void copyJarDir(String name, String[] files) {
=====================================
src/libbluray/bluray.c
=====================================
@@ -86,6 +86,7 @@ typedef struct {
/* */
uint8_t eof_hit;
+ uint8_t out_hit; /* PCR passed play item OUT time: rest of the clip can be skipped */
uint8_t encrypted_block_cnt;
uint8_t seek_flag; /* used to fine-tune first read after seek */
@@ -661,6 +662,7 @@ static int _open_m2ts(BLURAY *bd, BD_STREAM *st)
st->clip_pos = (uint64_t)st->clip->start_pkt * 192;
st->clip_block_pos = (st->clip_pos / 6144) * 6144;
st->eof_hit = 0;
+ st->out_hit = 0;
st->encrypted_block_cnt = 0;
if (st->fp) {
@@ -740,6 +742,7 @@ static int _validate_unit(BLURAY *bd, BD_STREAM *st, uint8_t *buf)
}
st->eof_hit = 0;
+ st->out_hit = 0;
st->encrypted_block_cnt = 0;
return 1;
}
@@ -794,6 +797,8 @@ static int _read_block(BLURAY *bd, BD_STREAM *st, uint8_t *buf)
if (result < 0) {
m2ts_filter_close(&st->m2ts_filter);
BD_DEBUG(DBG_BLURAY | DBG_CRIT, "m2ts filter error\n");
+ } else if (result > 0) {
+ st->out_hit = 1;
}
}
@@ -2062,6 +2067,16 @@ static int _bd_read(BLURAY *bd, unsigned char *buf, int len)
}
}
}
+ if (st->out_hit && st->int_buf_off == 6144 && clip_pkt < st->clip->end_pkt) {
+ /* everything left in this clip is after play item OUT time */
+ uint64_t skip = (uint64_t)(st->clip->end_pkt - clip_pkt) * 192;
+ BD_DEBUG(DBG_BLURAY | DBG_STREAM, "OUT time reached, skipping %u packets at end of clip %s\n",
+ st->clip->end_pkt - clip_pkt, st->clip->name);
+ st->clip_pos += skip;
+ bd->s_pos += skip;
+ st->out_hit = 0;
+ clip_pkt = st->clip->end_pkt;
+ }
if (st->int_buf_off == 6144 || clip_pkt >= st->clip->end_pkt) {
// Do we need to get the next clip?
=====================================
src/libbluray/decoders/m2ts_filter.c
=====================================
@@ -52,6 +52,9 @@ struct m2ts_filter_s
int64_t out_pts;
uint32_t pat_packets; /* how many packets to search for PAT (seeked pat_packets packets before the actual seek point) */
uint8_t pat_seen;
+
+ int64_t last_pcr; /* previous PCR base (90kHz), -1 if none seen since open/seek */
+ uint8_t out_reached; /* PCR has advanced past out_pts: nothing left to present in this clip */
};
M2TS_FILTER *m2ts_filter_init(int64_t in_pts, int64_t out_pts,
@@ -66,6 +69,7 @@ M2TS_FILTER *m2ts_filter_init(int64_t in_pts, int64_t out_pts,
p->in_pts = in_pts;
p->out_pts = out_pts;
+ p->last_pcr = -1;
p->wipe_pid = calloc(num_audio + num_video + num_ig + num_pg + 1, sizeof(uint16_t));
p->pass_pid = calloc(num_audio + num_video + num_ig + num_pg + 1, sizeof(uint16_t));
if (!p->pass_pid || !p->wipe_pid) {
@@ -199,6 +203,46 @@ void m2ts_filter_seek(M2TS_FILTER *p, uint32_t pat_packets, int64_t in_pts)
p->in_pts = in_pts;
p->pat_seen = 0;
p->pat_packets = pat_packets;
+ p->last_pcr = -1;
+ p->out_reached = 0;
+}
+
+/*
+ * Detect the PCR running past the OUT time.
+ *
+ * Some clips are much longer than the play item that references them (ex. a 5 second
+ * warning card backed by a 2 minute clip). ES packets after OUT time are wiped, but the
+ * clip would still be delivered - and paced by the application - up to its last packet.
+ * Once PCR has passed OUT time every remaining ES packet has PTS > OUT, so the rest of
+ * the clip can be skipped.
+ * Only a continuous crossing is accepted, to ignore STC discontinuities inside the clip.
+ */
+static void _check_pcr(M2TS_FILTER *p, const uint8_t *buf)
+{
+ const uint8_t *ts = buf + 4; /* TS packet, after TP_extra_header */
+
+ unsigned af_exists = ts[3] & 0x20;
+ unsigned af_length = ts[4];
+ unsigned pcr_flag = ts[5] & 0x10;
+
+ if (!af_exists || af_length < 7 || !pcr_flag) {
+ return;
+ }
+
+ /* program_clock_reference_base: 33 bits, 90 kHz. The extension is not needed. */
+ const uint8_t *base = ts + 6;
+ int64_t pcr = ((int64_t)base[0] << 25) |
+ ((int64_t)base[1] << 17) |
+ ((int64_t)base[2] << 9) |
+ ((int64_t)base[3] << 1) |
+ ((int64_t)base[4] >> 7);
+
+ if (p->last_pcr >= 0 && p->last_pcr <= p->out_pts &&
+ pcr > p->out_pts && pcr - p->last_pcr < 90000) {
+ M2TS_TRACE("PCR %" PRId64 " passed OUT timestamp %" PRId64 "\n", pcr, p->out_pts);
+ p->out_reached = 1;
+ }
+ p->last_pcr = pcr;
}
static int _filter_es_pts(M2TS_FILTER *p, const uint8_t *buf, uint16_t pid)
@@ -289,6 +333,9 @@ int m2ts_filter(M2TS_FILTER *p, uint8_t *buf)
if (pid < HDMV_PID_VIDEO) {
/* pass PMT, PCR, SIT */
/*M2TS_TRACE("NOT Wiping pid 0x%04x (< 0x1011)\n", pid);*/
+ if (pid == HDMV_PID_PCR && p->out_pts >= 0) {
+ _check_pcr(p, buf);
+ }
continue;
}
#if 0
@@ -316,5 +363,9 @@ int m2ts_filter(M2TS_FILTER *p, uint8_t *buf)
/*result = !!(p->after ? p->wipe_pid[0] : p->pass_pid[0]);*/
+ if (p->out_reached) {
+ result = 1;
+ }
+
return result;
}
View it on GitLab: https://code.videolan.org/videolan/libbluray/-/compare/a24f4fad4d62893de647abc8671397747b2359dd...448f896091634ab84776a3d062f0e1826e13ac6c
--
View it on GitLab: https://code.videolan.org/videolan/libbluray/-/compare/a24f4fad4d62893de647abc8671397747b2359dd...448f896091634ab84776a3d062f0e1826e13ac6c
You're receiving this email because of your account on code.videolan.org. Manage all notifications: https://code.videolan.org/-/profile/notifications | Help: https://code.videolan.org/help
More information about the libbluray-devel
mailing list