vtskit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vtskit/__init__.py +25 -0
- vtskit/errors.py +8 -0
- vtskit/ifo.py +113 -0
- vtskit/lang.py +648 -0
- vtskit/menu.py +171 -0
- vtskit/nav.py +256 -0
- vtskit/ps.py +484 -0
- vtskit/spu.py +262 -0
- vtskit/subtitles.py +126 -0
- vtskit/titleset.py +967 -0
- vtskit/vm.py +482 -0
- vtskit/vmg.py +159 -0
- vtskit-0.1.0.dist-info/METADATA +205 -0
- vtskit-0.1.0.dist-info/RECORD +18 -0
- vtskit-0.1.0.dist-info/WHEEL +5 -0
- vtskit-0.1.0.dist-info/licenses/COPYING +674 -0
- vtskit-0.1.0.dist-info/licenses/COPYING.LESSER +165 -0
- vtskit-0.1.0.dist-info/top_level.txt +1 -0
vtskit/menu.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Menus: a program chain of a menu domain with its buttons.
|
|
2
|
+
|
|
3
|
+
A menu is a VOB like a title's, with a subpicture overlay (vtskit.spu)
|
|
4
|
+
drawn where the buttons light up, and the highlight information (HLI) of
|
|
5
|
+
every navigation pack's PCI (https://dvdnav.mplayerhq.hu/dvdinfo/pci_pkt.html)
|
|
6
|
+
saying where each button is, which button each arrow key leads to, and the
|
|
7
|
+
command a button runs. The colours come from the program chain's palette:
|
|
8
|
+
entry 0 is the highlight (a button selected), entry 1 the selection (a
|
|
9
|
+
button activated), as dvdauthor and spumux arrange them; the overlay is
|
|
10
|
+
invisible until a button is selected. A menu may instead show its buttons
|
|
11
|
+
in a `normal` colour all the time (spumux's `image`): then the palette is
|
|
12
|
+
normal, highlight, selection, and the overlay draws palette 0.
|
|
13
|
+
|
|
14
|
+
A menu without a stream is a program chain of commands alone (its `pre`
|
|
15
|
+
runs and leaves), as dvdauthor writes a `<pgc>` without a `<vob>`.
|
|
16
|
+
|
|
17
|
+
From dvdauthor's output: the HLI is repeated whole in every VOBU
|
|
18
|
+
(HLI_SS 1), its start time is the menu's first picture, its end times are
|
|
19
|
+
0xFFFFFFFF (never), the buttons form one group of the normal kind, every
|
|
20
|
+
button uses colour table 1, and in that table only the pattern code (1) is
|
|
21
|
+
coloured: palette 0 and opaque when selected, palette 1 and opaque when
|
|
22
|
+
activated.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import struct
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
|
|
28
|
+
from . import spu
|
|
29
|
+
from .errors import VtsError
|
|
30
|
+
|
|
31
|
+
YELLOW, RED = (0xD2, 0x92, 0x10), (0x52, 0xF0, 0x5A) # Y, Cr, Cb
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Button:
|
|
36
|
+
"""A button: its rectangle (x0, y0, x1, y1 in the picture, x1 and y1 the
|
|
37
|
+
last column and line + 1), the command it runs (8 bytes), the buttons
|
|
38
|
+
(numbers, from 1) the arrow keys lead to (default: itself), and whether
|
|
39
|
+
it runs as soon as it is selected (`auto`)."""
|
|
40
|
+
rect: tuple
|
|
41
|
+
command: bytes
|
|
42
|
+
up: int = 0
|
|
43
|
+
down: int = 0
|
|
44
|
+
left: int = 0
|
|
45
|
+
right: int = 0
|
|
46
|
+
auto: bool = False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class Menu:
|
|
51
|
+
"""A menu: its program stream (ffmpeg -f dvd, no subpictures), its
|
|
52
|
+
buttons, the overlay of pixel codes lighting up the buttons (code 1
|
|
53
|
+
where a button shows when selected; by default the buttons' rectangles),
|
|
54
|
+
the highlight and selection colours (Y, Cr, Cb, and a contrast 0-15,
|
|
55
|
+
15 if left out), the colour the buttons show in otherwise (None: they
|
|
56
|
+
show nothing), where its cells start (seconds, as a title's chapters; a
|
|
57
|
+
loop point), its commands, and which remote key it answers (`entry`:
|
|
58
|
+
"title" in the VMG; "root", "subpicture", "audio", "angle" or "ptt" in
|
|
59
|
+
a titleset; None for a menu reached by commands). `stream` None: a
|
|
60
|
+
program chain of commands only, without pictures or buttons."""
|
|
61
|
+
stream: str | None
|
|
62
|
+
buttons: list = field(default_factory=list)
|
|
63
|
+
overlay: object = None
|
|
64
|
+
highlight: tuple = YELLOW
|
|
65
|
+
select: tuple = RED
|
|
66
|
+
normal: tuple | None = None
|
|
67
|
+
cells: tuple = (0.0,)
|
|
68
|
+
pre: tuple = ()
|
|
69
|
+
post: tuple = () # none: Exit, as dvdauthor writes a menu without post commands
|
|
70
|
+
entry: str | None = None
|
|
71
|
+
first_button: int = 0 # the button selected when the menu shows (0: the player's choice)
|
|
72
|
+
button_offset: int = 0 # added to a number key's digit to pick a button
|
|
73
|
+
uops: int = 0 # user operations the menu forbids (bit n: vtskit.vm.UOP_*)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
ENTRY_TYPES = {"title": 2, "root": 3, "subpicture": 4, "audio": 5, "angle": 6, "ptt": 7}
|
|
77
|
+
ENTRY_FLAGS = {"title": 0x80, "root": 0x80, "subpicture": 0x40, "audio": 0x20, "angle": 0x10, "ptt": 0x08}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _colour(c, what):
|
|
81
|
+
"""(Y, Cr, Cb), contrast of a colour given as 3 or 4 numbers."""
|
|
82
|
+
if len(c) not in (3, 4) or not all(isinstance(v, int) for v in c):
|
|
83
|
+
raise VtsError(f"the {what} colour is (Y, Cr, Cb) or (Y, Cr, Cb, contrast)")
|
|
84
|
+
contrast = c[3] if len(c) == 4 else 15
|
|
85
|
+
if not 0 <= contrast <= 15:
|
|
86
|
+
raise VtsError(f"the {what} colour's contrast is 0 to 15")
|
|
87
|
+
return tuple(c[:3]), contrast
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def check(menu, width, height):
|
|
91
|
+
if menu.stream is None and menu.buttons:
|
|
92
|
+
raise VtsError("a menu without a stream has no buttons")
|
|
93
|
+
if len(menu.buttons) > 36:
|
|
94
|
+
raise VtsError(f"{len(menu.buttons)} buttons (a menu has at most 36)")
|
|
95
|
+
_colour(menu.highlight, "highlight")
|
|
96
|
+
_colour(menu.select, "selection")
|
|
97
|
+
if menu.normal is not None:
|
|
98
|
+
_colour(menu.normal, "normal")
|
|
99
|
+
if menu.entry is not None and menu.entry not in ENTRY_TYPES:
|
|
100
|
+
raise VtsError(f"{menu.entry!r} is not a menu entry kind")
|
|
101
|
+
for n, b in enumerate(menu.buttons, 1):
|
|
102
|
+
x0, y0, x1, y1 = b.rect
|
|
103
|
+
if not (0 <= x0 < x1 <= width and 0 <= y0 < y1 <= height):
|
|
104
|
+
raise VtsError(f"button {n}: {b.rect} is not inside {width}x{height}")
|
|
105
|
+
if len(b.command) != 8:
|
|
106
|
+
raise VtsError(f"button {n}: a command is 8 bytes")
|
|
107
|
+
for k in (b.up, b.down, b.left, b.right):
|
|
108
|
+
if not 0 <= k <= len(menu.buttons):
|
|
109
|
+
raise VtsError(f"button {n} points at button {k}, which the menu has not")
|
|
110
|
+
if not 0 <= menu.first_button <= len(menu.buttons):
|
|
111
|
+
raise VtsError(f"first button {menu.first_button}: the menu has {len(menu.buttons)}")
|
|
112
|
+
if not 0 <= menu.button_offset <= 255:
|
|
113
|
+
raise VtsError("the button offset is 0 to 255")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def overlay(menu, width, height):
|
|
117
|
+
"""The menu's overlay: as given, or its buttons' rectangles."""
|
|
118
|
+
if menu.overlay is not None:
|
|
119
|
+
if (menu.overlay.width, menu.overlay.height) != (width, height):
|
|
120
|
+
raise VtsError("the overlay is not the picture's size")
|
|
121
|
+
return menu.overlay
|
|
122
|
+
return spu.Overlay.from_rects(width, height, [b.rect for b in menu.buttons])
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def palette(menu):
|
|
126
|
+
"""The program chain's colours: the normal colour if any, then the
|
|
127
|
+
highlight and the selection; none for a menu without buttons."""
|
|
128
|
+
if not menu.buttons:
|
|
129
|
+
return ()
|
|
130
|
+
colours = [_colour(menu.highlight, "highlight")[0], _colour(menu.select, "selection")[0]]
|
|
131
|
+
if menu.normal is not None:
|
|
132
|
+
colours.insert(0, _colour(menu.normal, "normal")[0])
|
|
133
|
+
return tuple(colours)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def unit(menu, width, height):
|
|
137
|
+
"""The menu's subpicture unit: the overlay, shown in the normal colour
|
|
138
|
+
(palette 0, its contrast) or not at all."""
|
|
139
|
+
o = overlay(menu, width, height)
|
|
140
|
+
if menu.normal is None:
|
|
141
|
+
return spu.encode(o)
|
|
142
|
+
return spu.encode(o, colours=(0, 0, 0, 0), contrasts=(0, _colour(menu.normal, "normal")[1], 0, 0))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def hli(menu, s_ptm):
|
|
146
|
+
"""The highlight information of the menu's navigation packs (the PCI
|
|
147
|
+
from 0x60 to 0x316), shown from `s_ptm`, the menu's first picture."""
|
|
148
|
+
buttons = menu.buttons
|
|
149
|
+
out = bytearray(0x316 - 0x60)
|
|
150
|
+
if not buttons:
|
|
151
|
+
return bytes(out)
|
|
152
|
+
struct.pack_into(">HIIIHBBBBBB", out, 0, 1, s_ptm, 0xFFFFFFFF, 0xFFFFFFFF, 0x1000,
|
|
153
|
+
menu.button_offset, len(buttons), len(buttons), 0, menu.first_button, 0)
|
|
154
|
+
# colour table 1: the pattern code, the highlight's palette entry and
|
|
155
|
+
# contrast when selected, the selection's when activated; the other
|
|
156
|
+
# codes stay as they are
|
|
157
|
+
first = 1 if menu.normal is not None else 0
|
|
158
|
+
out[0x16:0x1E] = bytes((0x00, first << 4, 0x00, _colour(menu.highlight, "highlight")[1] << 4,
|
|
159
|
+
0x00, (first + 1) << 4, 0x00, _colour(menu.select, "selection")[1] << 4))
|
|
160
|
+
for n, b in enumerate(buttons):
|
|
161
|
+
x0, y0, x1, y1 = b.rect
|
|
162
|
+
e = 0x2E + 18 * n
|
|
163
|
+
out[e] = 1 << 6 | x0 >> 4
|
|
164
|
+
out[e + 1] = (x0 & 0xF) << 4 | x1 >> 8
|
|
165
|
+
out[e + 2] = x1 & 0xFF
|
|
166
|
+
out[e + 3] = (0x40 if b.auto else 0) | y0 >> 4 # auto action mode 01
|
|
167
|
+
out[e + 4] = (y0 & 0xF) << 4 | y1 >> 8
|
|
168
|
+
out[e + 5] = y1 & 0xFF
|
|
169
|
+
out[e + 6:e + 10] = bytes(k or n + 1 for k in (b.up, b.down, b.left, b.right))
|
|
170
|
+
out[e + 10:e + 18] = b.command
|
|
171
|
+
return bytes(out)
|
vtskit/nav.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Fill the navigation packs of a VOB.
|
|
2
|
+
|
|
3
|
+
Every VOBU starts with a navigation pack: a PCI packet (presentation:
|
|
4
|
+
times, and a menu's buttons) and a DSI packet (search: where this VOBU
|
|
5
|
+
ends, where its reference pictures end, where the VOBUs 0.5 s to 120 s
|
|
6
|
+
away start, where its sound starts). ffmpeg's `-f dvd` leaves both empty;
|
|
7
|
+
`fill` writes them, and no pack moves.
|
|
8
|
+
|
|
9
|
+
The fields are those described at https://dvdnav.mplayerhq.hu/dvdinfo/
|
|
10
|
+
(pci_pkt.html, dsi_pkt.html). Where the description leaves a choice open,
|
|
11
|
+
the choice is the one dvdauthor makes, found by comparing its output
|
|
12
|
+
bytes (see tests/test_nav.py).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import bisect
|
|
16
|
+
import struct
|
|
17
|
+
|
|
18
|
+
from . import ps
|
|
19
|
+
from .errors import VtsError
|
|
20
|
+
|
|
21
|
+
PCI = 0x2D # the PCI data, in the navigation pack
|
|
22
|
+
DSI = 0x407 # the DSI data
|
|
23
|
+
|
|
24
|
+
# the search spans of VOBU_SRI, in half seconds: forward from the farthest,
|
|
25
|
+
# backward from the nearest
|
|
26
|
+
SPANS = (240, 120, 60, 20, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)
|
|
27
|
+
|
|
28
|
+
COARSE = (20, 60, 120, 240)
|
|
29
|
+
NONE = 0x3FFFFFFF # no VOBU for this span (or no next / previous VOBU)
|
|
30
|
+
NO_VIDEO = 0xBFFFFFFF # no next / previous VOBU with video
|
|
31
|
+
VALID = 0x80000000
|
|
32
|
+
BETWEEN = 0x40000000 # VOBUs lie between this one and the nearer span's
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Vobu:
|
|
36
|
+
"""One VOBU of a stream: its sectors (nav pack first, last), times and
|
|
37
|
+
what the DSI points at."""
|
|
38
|
+
__slots__ = ("start", "end", "scr", "s_ptm", "e_ptm", "video", "refs", "cell")
|
|
39
|
+
|
|
40
|
+
def __init__(self, start, end, scr):
|
|
41
|
+
self.start, self.end, self.scr = start, end, scr
|
|
42
|
+
self.s_ptm = self.e_ptm = None
|
|
43
|
+
self.video = False
|
|
44
|
+
self.refs = []
|
|
45
|
+
self.cell = None # (vob id, cell id, index of its first VOBU, of its last)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def vobus(stream):
|
|
49
|
+
"""The VOBUs of a scanned stream (`ps.scan`), with their times and
|
|
50
|
+
reference pictures."""
|
|
51
|
+
if not stream.navs or stream.navs[0][0] != 0:
|
|
52
|
+
raise VtsError("the stream does not start with a navigation pack")
|
|
53
|
+
starts = [s for s, _ in stream.navs]
|
|
54
|
+
out = [Vobu(s, e - 1, scr) for (s, scr), e in zip(stream.navs, starts[1:] + [stream.sectors])]
|
|
55
|
+
k = 0
|
|
56
|
+
for p in stream.pictures:
|
|
57
|
+
while k + 1 < len(out) and p.start >= out[k + 1].start:
|
|
58
|
+
k += 1
|
|
59
|
+
v = out[k]
|
|
60
|
+
v.video = True
|
|
61
|
+
if p.pts is not None and (v.s_ptm is None or p.pts < v.s_ptm):
|
|
62
|
+
v.s_ptm = p.pts
|
|
63
|
+
if p.type in "IP" and len(v.refs) < 3:
|
|
64
|
+
v.refs.append(p.end - v.start)
|
|
65
|
+
if any(v.s_ptm is None for v in out):
|
|
66
|
+
raise VtsError("a VOBU without video: a stream of sound alone is not a DVD title (dvdauthor "
|
|
67
|
+
"refuses it too; a still is one picture, with the sound after it)")
|
|
68
|
+
for a, b in zip(out, out[1:]):
|
|
69
|
+
a.e_ptm = b.s_ptm
|
|
70
|
+
out[-1].e_ptm = stream.end
|
|
71
|
+
return out
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _bcd(n):
|
|
75
|
+
return (n // 10) << 4 | n % 10
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def elapsed(ticks, frame):
|
|
79
|
+
"""A time as the PGC and navigation packs give it: BCD hh:mm:ss:ff, the
|
|
80
|
+
top two bits of ff saying 30 (11) or 25 (01) frames a second."""
|
|
81
|
+
fps, flag = (25, 0x40) if frame == 3600 else (30, 0xC0)
|
|
82
|
+
frames = int(round(ticks / frame))
|
|
83
|
+
s, f = divmod(frames, fps)
|
|
84
|
+
m, s = divmod(s, 60)
|
|
85
|
+
h, m = divmod(m, 60)
|
|
86
|
+
return bytes((_bcd(h), _bcd(m), _bcd(s), _bcd(f) | flag))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _search(vs, times, i, first, last, pos):
|
|
90
|
+
"""VOBU_SRI of VOBU i, its cell being VOBUs first..last (`times`: the
|
|
91
|
+
start of each VOBU, `pos`: the sector of each VOBU on the disc)."""
|
|
92
|
+
v = vs[i]
|
|
93
|
+
end = vs[last].e_ptm
|
|
94
|
+
|
|
95
|
+
def find(t):
|
|
96
|
+
if t < times[first] or t >= end:
|
|
97
|
+
return None
|
|
98
|
+
return bisect.bisect_right(times, t, first, last + 1) - 1
|
|
99
|
+
|
|
100
|
+
def entries(sign):
|
|
101
|
+
# (the coarse spans, 10 s and up, also say whether VOBUs lie between
|
|
102
|
+
# their target and the nearer span's, or past it when they have none)
|
|
103
|
+
out, nearer = [], i
|
|
104
|
+
for span in sorted(SPANS):
|
|
105
|
+
j = find(v.s_ptm + sign * span * 45000)
|
|
106
|
+
coarse = span in COARSE and nearer is not None
|
|
107
|
+
if j is None:
|
|
108
|
+
beyond = first <= nearer + sign <= last if coarse else False
|
|
109
|
+
out.append(NONE | (BETWEEN if beyond else 0))
|
|
110
|
+
else:
|
|
111
|
+
val = VALID | abs(pos[j] - pos[i])
|
|
112
|
+
if coarse and abs(j - nearer) > 1:
|
|
113
|
+
val |= BETWEEN
|
|
114
|
+
out.append(val)
|
|
115
|
+
nearer = j
|
|
116
|
+
return out # nearest first
|
|
117
|
+
|
|
118
|
+
fwd = entries(1)[::-1]
|
|
119
|
+
bwd = entries(-1)
|
|
120
|
+
nxt = VALID | (pos[i + 1] - pos[i]) if i < last else NONE
|
|
121
|
+
prv = VALID | (pos[i] - pos[i - 1]) if i > first else NONE
|
|
122
|
+
nvwv = next((VALID | (pos[j] - pos[i]) for j in range(i + 1, last + 1) if vs[j].video),
|
|
123
|
+
NO_VIDEO)
|
|
124
|
+
pvwv = next((VALID | (pos[i] - pos[j]) for j in range(i - 1, first - 1, -1)
|
|
125
|
+
if vs[j].video), NO_VIDEO)
|
|
126
|
+
return [nvwv] + fwd + [nxt, prv] + bwd + [pvwv]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _audio_sync(stream, vs, pos, final):
|
|
130
|
+
"""a_synca of each VOBU: per sound stream (numbered in the order the
|
|
131
|
+
streams first appear), the offset to the stream's last packet stamped
|
|
132
|
+
at or before the VOBU's start (or its first, if none is)."""
|
|
133
|
+
if len(stream.audio) > 8:
|
|
134
|
+
raise VtsError(f"{len(stream.audio)} sound streams (a DVD title has at most 8)")
|
|
135
|
+
out = [[0] * 8 for _ in vs]
|
|
136
|
+
for n, packets in enumerate(stream.audio.values()):
|
|
137
|
+
stamped = [(s, p) for s, p, _ in packets if p is not None]
|
|
138
|
+
if not stamped:
|
|
139
|
+
continue
|
|
140
|
+
k = 0
|
|
141
|
+
for i, (row, v) in enumerate(zip(out, vs)):
|
|
142
|
+
while k + 1 < len(stamped) and stamped[k + 1][1] <= v.s_ptm:
|
|
143
|
+
k += 1
|
|
144
|
+
off = final(stamped[k][0]) - pos[i]
|
|
145
|
+
row[n] = off if off >= 0 else 0x8000 | -off
|
|
146
|
+
return out
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _subpicture_sync(stream, vs, pos, covered):
|
|
150
|
+
"""sp_synca of each VOBU: per subpicture stream (0-31), where the unit
|
|
151
|
+
on screen during the VOBU is: in this VOBU, or nothing on screen
|
|
152
|
+
(7fffffff), or the offset to the VOBU it starts in. The unit on screen
|
|
153
|
+
is the last one shown before the VOBU ends and not gone before it
|
|
154
|
+
starts; a unit from before the VOBU's cell is not pointed at (a
|
|
155
|
+
player entering the cell has not read it), as dvdauthor writes them."""
|
|
156
|
+
starts = [v.start for v in vs]
|
|
157
|
+
cell_start = {}
|
|
158
|
+
for i, cell in enumerate(covered):
|
|
159
|
+
cell_start.setdefault(cell, vs[i].start)
|
|
160
|
+
out = [[0] * 32 for _ in vs]
|
|
161
|
+
for sub, units in stream.subpictures.items():
|
|
162
|
+
n = sub - 0x20
|
|
163
|
+
for i, (row, v) in enumerate(zip(out, vs)):
|
|
164
|
+
val = 0x7FFFFFFF
|
|
165
|
+
for sector, shown, until in reversed(units):
|
|
166
|
+
if sector < cell_start[covered[i]]:
|
|
167
|
+
break
|
|
168
|
+
if shown < v.e_ptm and (until is None or until > v.s_ptm):
|
|
169
|
+
home = pos[bisect.bisect_right(starts, sector) - 1]
|
|
170
|
+
if home > pos[i]:
|
|
171
|
+
val = home - pos[i]
|
|
172
|
+
elif home < pos[i]:
|
|
173
|
+
val = 0x80000000 | (pos[i] - home)
|
|
174
|
+
break
|
|
175
|
+
row[n] = val
|
|
176
|
+
return out
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
HLI = 0x60 # the highlight information in the PCI data, to 0x316
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def fill(data, cells, lbn=0, uops=0, stream=None, hli=None, layout=None, ilvu=None, nsml=None):
|
|
183
|
+
"""Fill the navigation packs of a VOB in place (a bytearray or a
|
|
184
|
+
writable mmap).
|
|
185
|
+
|
|
186
|
+
`cells`: [(vob id, cell id, first VOBU index, last VOBU index)], every
|
|
187
|
+
VOBU in one cell. `lbn`: the sector of the stream's first pack in the
|
|
188
|
+
titleset's VOBs, when the VOB lies whole there; `layout`: the sector of
|
|
189
|
+
each VOBU on the disc instead, when the VOB is interleaved with others.
|
|
190
|
+
`stream`: the stream already scanned, if it is. `hli`: {vob id: the
|
|
191
|
+
highlight information (buttons) of its menu, 0x2B6 bytes}, written into
|
|
192
|
+
every navigation pack of that VOB. `ilvu`: {VOBU index: (flags, end,
|
|
193
|
+
next start, next size, [(angle's next unit, size)] * 9)}, the SML_PBI
|
|
194
|
+
and SML_AGLI of a VOBU in or before an interleaved block. `nsml`: {VOBU
|
|
195
|
+
index: [angle's VOBU] * 9}, the PCI's NSML_AGLI. Returns the VOBUs."""
|
|
196
|
+
stream = stream or ps.scan(data)
|
|
197
|
+
vs = vobus(stream)
|
|
198
|
+
pos = list(layout) if layout is not None else [lbn + v.start for v in vs]
|
|
199
|
+
if len(pos) != len(vs):
|
|
200
|
+
raise VtsError("the layout does not give every VOBU a sector")
|
|
201
|
+
starts = [v.start for v in vs]
|
|
202
|
+
|
|
203
|
+
def final(sector):
|
|
204
|
+
k = bisect.bisect_right(starts, sector) - 1
|
|
205
|
+
return pos[k] + sector - vs[k].start
|
|
206
|
+
ilvu = ilvu or {}
|
|
207
|
+
nsml = nsml or {}
|
|
208
|
+
covered = [None] * len(vs)
|
|
209
|
+
for cell in cells:
|
|
210
|
+
vob_id, cell_id, first, last = cell
|
|
211
|
+
for j in range(first, last + 1):
|
|
212
|
+
covered[j] = cell
|
|
213
|
+
if None in covered:
|
|
214
|
+
raise VtsError("a VOBU is in no cell")
|
|
215
|
+
vob_s, vob_e = {}, {}
|
|
216
|
+
for v, cell in zip(vs, covered):
|
|
217
|
+
v.cell = cell
|
|
218
|
+
vob_s.setdefault(cell[0], v.s_ptm)
|
|
219
|
+
vob_e[cell[0]] = v.e_ptm
|
|
220
|
+
times = [v.s_ptm for v in vs]
|
|
221
|
+
sync = _audio_sync(stream, vs, pos, final)
|
|
222
|
+
spu = _subpicture_sync(stream, vs, pos, covered)
|
|
223
|
+
for i, v in enumerate(vs):
|
|
224
|
+
vob_id, cell_id, first, last = v.cell
|
|
225
|
+
base = v.start * ps.SECTOR
|
|
226
|
+
if data[base + 0x26:base + 0x2D] != b"\x00\x00\x01\xbf\x03\xd4\x00" or \
|
|
227
|
+
data[base + 0x400:base + 0x407] != b"\x00\x00\x01\xbf\x03\xfa\x01":
|
|
228
|
+
raise VtsError(f"sector {v.start}: not a navigation pack of PCI and DSI")
|
|
229
|
+
eltm = elapsed(v.s_ptm - vs[first].s_ptm, stream.frame)
|
|
230
|
+
pci = bytearray(0x3D4 - 1)
|
|
231
|
+
struct.pack_into(">IIIIIII", pci, 0, pos[i], 0, uops,
|
|
232
|
+
v.s_ptm, v.e_ptm, 0, int.from_bytes(eltm, "big"))
|
|
233
|
+
if i in nsml:
|
|
234
|
+
struct.pack_into(">9I", pci, 0x3C, *nsml[i])
|
|
235
|
+
if hli and vob_id in hli:
|
|
236
|
+
if len(hli[vob_id]) != 0x316 - HLI:
|
|
237
|
+
raise VtsError("the highlight information is 0x2B6 bytes")
|
|
238
|
+
pci[HLI:0x316] = hli[vob_id]
|
|
239
|
+
dsi = bytearray(0x3FA - 1)
|
|
240
|
+
# (fewer than three reference pictures: the last one's end again,
|
|
241
|
+
# as dvdauthor writes it)
|
|
242
|
+
refs = v.refs + [v.refs[-1] if v.refs else 0] * (3 - len(v.refs))
|
|
243
|
+
struct.pack_into(">IIIIIIHxB4s", dsi, 0, v.scr & 0xFFFFFFFF, pos[i],
|
|
244
|
+
v.end - v.start, *refs, vob_id, cell_id, eltm)
|
|
245
|
+
if i in ilvu:
|
|
246
|
+
flags, ea, sa, sz, agli = ilvu[i]
|
|
247
|
+
struct.pack_into(">HIIH", dsi, 0x20, flags, ea, sa, sz)
|
|
248
|
+
for n, (dsta, size) in enumerate(agli):
|
|
249
|
+
struct.pack_into(">IH", dsi, 0xB4 + 6 * n, dsta, size)
|
|
250
|
+
struct.pack_into(">II", dsi, 0x2C, vob_s[vob_id], vob_e[vob_id])
|
|
251
|
+
struct.pack_into(">42I", dsi, 0xEA, *_search(vs, times, i, first, last, pos))
|
|
252
|
+
struct.pack_into(">8H", dsi, 0x192, *sync[i])
|
|
253
|
+
struct.pack_into(">32I", dsi, 0x1A2, *spu[i])
|
|
254
|
+
data[base + PCI:base + PCI + len(pci)] = pci
|
|
255
|
+
data[base + DSI:base + DSI + len(dsi)] = dsi
|
|
256
|
+
return vs
|