vtskit 0.1.1__tar.gz → 0.3.0__tar.gz
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-0.1.1/vtskit.egg-info → vtskit-0.3.0}/PKG-INFO +21 -6
- {vtskit-0.1.1 → vtskit-0.3.0}/README.md +20 -5
- {vtskit-0.1.1 → vtskit-0.3.0}/pyproject.toml +1 -1
- {vtskit-0.1.1 → vtskit-0.3.0}/tests/test_lang.py +12 -1
- vtskit-0.3.0/tests/test_title_buttons.py +247 -0
- {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/__init__.py +1 -1
- {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/lang.py +9 -7
- {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/menu.py +103 -13
- {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/nav.py +11 -5
- {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/titleset.py +92 -8
- {vtskit-0.1.1 → vtskit-0.3.0/vtskit.egg-info}/PKG-INFO +21 -6
- {vtskit-0.1.1 → vtskit-0.3.0}/vtskit.egg-info/SOURCES.txt +1 -0
- {vtskit-0.1.1 → vtskit-0.3.0}/COPYING +0 -0
- {vtskit-0.1.1 → vtskit-0.3.0}/COPYING.LESSER +0 -0
- {vtskit-0.1.1 → vtskit-0.3.0}/setup.cfg +0 -0
- {vtskit-0.1.1 → vtskit-0.3.0}/tests/test_angles.py +0 -0
- {vtskit-0.1.1 → vtskit-0.3.0}/tests/test_ifo.py +0 -0
- {vtskit-0.1.1 → vtskit-0.3.0}/tests/test_menu.py +0 -0
- {vtskit-0.1.1 → vtskit-0.3.0}/tests/test_nav.py +0 -0
- {vtskit-0.1.1 → vtskit-0.3.0}/tests/test_subtitles.py +0 -0
- {vtskit-0.1.1 → vtskit-0.3.0}/tests/test_titleset.py +0 -0
- {vtskit-0.1.1 → vtskit-0.3.0}/tests/test_vm.py +0 -0
- {vtskit-0.1.1 → vtskit-0.3.0}/tests/test_vmg.py +0 -0
- {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/errors.py +0 -0
- {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/ifo.py +0 -0
- {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/ps.py +0 -0
- {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/spu.py +0 -0
- {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/subtitles.py +0 -0
- {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/vm.py +0 -0
- {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/vmg.py +0 -0
- {vtskit-0.1.1 → vtskit-0.3.0}/vtskit.egg-info/dependency_links.txt +0 -0
- {vtskit-0.1.1 → vtskit-0.3.0}/vtskit.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vtskit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: DVD-Video titlesets in pure Python: VOBs, navigation packs and IFO files
|
|
5
5
|
Author: kajisho5
|
|
6
6
|
License-Expression: LGPL-3.0-or-later
|
|
@@ -141,7 +141,18 @@ oncemore writing its discs with vtskit.
|
|
|
141
141
|
VOBU of the same moment (NSML_AGLI). Only one of the two is filled, as
|
|
142
142
|
players take NSML_AGLI whenever it is there. A stream whose interleaved
|
|
143
143
|
units start with open groups of pictures (B-pictures leaning on the group
|
|
144
|
-
before, another angle's after a switch) is refused.
|
|
144
|
+
before, another angle's after a switch) is refused. A title may carry
|
|
145
|
+
buttons over stretches of it (`vtskit.menu.TitleButtons`: from a start
|
|
146
|
+
to an end, the buttons, the palette entries that draw them): their
|
|
147
|
+
overlay is a forced subpicture added with `vtskit.subtitles`, and the
|
|
148
|
+
highlight information goes only into the VOBUs that play during the
|
|
149
|
+
stretch (where two stretches meet inside a VOBU, the one covering more
|
|
150
|
+
of it), with the stretch's start and end as its start time, end time
|
|
151
|
+
and the end of the selection (dvdinfo's hli_s_ptm, hli_e_ptm,
|
|
152
|
+
btn_sl_e_ptm). dvdauthor gives a title's buttons the whole title (every
|
|
153
|
+
VOBU, never ending), which is what buttons without an end write. As
|
|
154
|
+
dvdauthor does, a title with buttons starts with its first button
|
|
155
|
+
highlighted (SetHL_BTNN 1 before its own pre commands).
|
|
145
156
|
* `vtskit.subtitles` adds subtitles to a title's program stream, as spumux
|
|
146
157
|
does: each cue a picture of up to four colours placed on the screen and
|
|
147
158
|
shown for a while, as subpicture packs a little before their time.
|
|
@@ -149,8 +160,8 @@ oncemore writing its discs with vtskit.
|
|
|
149
160
|
`if (g5 != 0) { button = g5; }`, `call menu entry root;`) for the domain
|
|
150
161
|
a text runs in, to the same commands dvdauthor writes for it: a tool
|
|
151
162
|
that already speaks the language, as oncemore does, keeps its text. A
|
|
152
|
-
button of several commands
|
|
153
|
-
dispatcher, and a jump to a chapter of another titleset goes through pad
|
|
163
|
+
button of several commands, a menu's or a title's, runs them from the
|
|
164
|
+
program chain's post through a dispatcher, and a jump to a chapter of another titleset goes through pad
|
|
154
165
|
program chains, as dvdauthor's "jumppads" do.
|
|
155
166
|
* `vtskit.vmg` writes the video manager: VIDEO_TS.IFO and its BUP, with
|
|
156
167
|
the first-play commands, the table of titles and the titlesets'
|
|
@@ -182,7 +193,9 @@ dvdauthor's (its buttons made with spumux), and a menu's overlay in a
|
|
|
182
193
|
normal colour is spumux's unit byte for byte; subtitles muxed by vtskit are
|
|
183
194
|
spumux's units, placed where spumux places them, and the title written
|
|
184
195
|
from that stream (4:3, and 16:9 with and without pan-scan or letterbox) is
|
|
185
|
-
dvdauthor's
|
|
196
|
+
dvdauthor's; a title with buttons over the whole of it (one of them of
|
|
197
|
+
several commands) is dvdauthor's VOB and IFO byte for byte, but for the
|
|
198
|
+
palette. A VIDEO_TS written by vtskit alone plays
|
|
186
199
|
its titles and chapters in order under libdvdnav, its commands jumping
|
|
187
200
|
between titles, its menus' buttons lead where they say, and its jumps into
|
|
188
201
|
another titleset's chapters land; a title with an angle block plays and
|
|
@@ -190,7 +203,9 @@ switches angles under libdvdnav, every pointer of its navigation packs
|
|
|
190
203
|
landing on the unit it names, and the pictures libdvdnav hands a player
|
|
191
204
|
through a switch, decoded with ffmpeg, are every picture once, in order,
|
|
192
205
|
the new angle from the end of a unit (dvdauthor writes no angles, so a
|
|
193
|
-
set-top player is the check still owed)
|
|
206
|
+
set-top player is the check still owed); a title's buttons over a stretch
|
|
207
|
+
are offered by libdvdnav in that stretch's VOBUs alone, and run their
|
|
208
|
+
commands
|
|
194
209
|
(`python -m unittest discover -s tests -t .`; a test is skipped when a tool
|
|
195
210
|
it needs is missing). oncemore's switch to vtskit (step 4) is merged:
|
|
196
211
|
[oncemore#90](https://github.com/kajisho5/oncemore/pull/90) and
|
|
@@ -124,7 +124,18 @@ oncemore writing its discs with vtskit.
|
|
|
124
124
|
VOBU of the same moment (NSML_AGLI). Only one of the two is filled, as
|
|
125
125
|
players take NSML_AGLI whenever it is there. A stream whose interleaved
|
|
126
126
|
units start with open groups of pictures (B-pictures leaning on the group
|
|
127
|
-
before, another angle's after a switch) is refused.
|
|
127
|
+
before, another angle's after a switch) is refused. A title may carry
|
|
128
|
+
buttons over stretches of it (`vtskit.menu.TitleButtons`: from a start
|
|
129
|
+
to an end, the buttons, the palette entries that draw them): their
|
|
130
|
+
overlay is a forced subpicture added with `vtskit.subtitles`, and the
|
|
131
|
+
highlight information goes only into the VOBUs that play during the
|
|
132
|
+
stretch (where two stretches meet inside a VOBU, the one covering more
|
|
133
|
+
of it), with the stretch's start and end as its start time, end time
|
|
134
|
+
and the end of the selection (dvdinfo's hli_s_ptm, hli_e_ptm,
|
|
135
|
+
btn_sl_e_ptm). dvdauthor gives a title's buttons the whole title (every
|
|
136
|
+
VOBU, never ending), which is what buttons without an end write. As
|
|
137
|
+
dvdauthor does, a title with buttons starts with its first button
|
|
138
|
+
highlighted (SetHL_BTNN 1 before its own pre commands).
|
|
128
139
|
* `vtskit.subtitles` adds subtitles to a title's program stream, as spumux
|
|
129
140
|
does: each cue a picture of up to four colours placed on the screen and
|
|
130
141
|
shown for a while, as subpicture packs a little before their time.
|
|
@@ -132,8 +143,8 @@ oncemore writing its discs with vtskit.
|
|
|
132
143
|
`if (g5 != 0) { button = g5; }`, `call menu entry root;`) for the domain
|
|
133
144
|
a text runs in, to the same commands dvdauthor writes for it: a tool
|
|
134
145
|
that already speaks the language, as oncemore does, keeps its text. A
|
|
135
|
-
button of several commands
|
|
136
|
-
dispatcher, and a jump to a chapter of another titleset goes through pad
|
|
146
|
+
button of several commands, a menu's or a title's, runs them from the
|
|
147
|
+
program chain's post through a dispatcher, and a jump to a chapter of another titleset goes through pad
|
|
137
148
|
program chains, as dvdauthor's "jumppads" do.
|
|
138
149
|
* `vtskit.vmg` writes the video manager: VIDEO_TS.IFO and its BUP, with
|
|
139
150
|
the first-play commands, the table of titles and the titlesets'
|
|
@@ -165,7 +176,9 @@ dvdauthor's (its buttons made with spumux), and a menu's overlay in a
|
|
|
165
176
|
normal colour is spumux's unit byte for byte; subtitles muxed by vtskit are
|
|
166
177
|
spumux's units, placed where spumux places them, and the title written
|
|
167
178
|
from that stream (4:3, and 16:9 with and without pan-scan or letterbox) is
|
|
168
|
-
dvdauthor's
|
|
179
|
+
dvdauthor's; a title with buttons over the whole of it (one of them of
|
|
180
|
+
several commands) is dvdauthor's VOB and IFO byte for byte, but for the
|
|
181
|
+
palette. A VIDEO_TS written by vtskit alone plays
|
|
169
182
|
its titles and chapters in order under libdvdnav, its commands jumping
|
|
170
183
|
between titles, its menus' buttons lead where they say, and its jumps into
|
|
171
184
|
another titleset's chapters land; a title with an angle block plays and
|
|
@@ -173,7 +186,9 @@ switches angles under libdvdnav, every pointer of its navigation packs
|
|
|
173
186
|
landing on the unit it names, and the pictures libdvdnav hands a player
|
|
174
187
|
through a switch, decoded with ffmpeg, are every picture once, in order,
|
|
175
188
|
the new angle from the end of a unit (dvdauthor writes no angles, so a
|
|
176
|
-
set-top player is the check still owed)
|
|
189
|
+
set-top player is the check still owed); a title's buttons over a stretch
|
|
190
|
+
are offered by libdvdnav in that stretch's VOBUs alone, and run their
|
|
191
|
+
commands
|
|
177
192
|
(`python -m unittest discover -s tests -t .`; a test is skipped when a tool
|
|
178
193
|
it needs is missing). oncemore's switch to vtskit (step 4) is merged:
|
|
179
194
|
[oncemore#90](https://github.com/kajisho5/oncemore/pull/90) and
|
|
@@ -138,6 +138,17 @@ class TestForms(unittest.TestCase):
|
|
|
138
138
|
self.assertEqual(hexes(self.d.compile("g1 = 0; exit;", "vts", 1)), ["7100000100000000", "3001000000000000"])
|
|
139
139
|
self.assertEqual(hexes(self.d.compile("call menu entry root resume 3;", "vts", 1)), ["3008000003830000"])
|
|
140
140
|
|
|
141
|
+
def test_title_buttons(self):
|
|
142
|
+
# dvdauthor's bytes for the same buttons in a title's <pgc>, with
|
|
143
|
+
# <post>jump title 2;</post>: the same dispatcher as a menu's
|
|
144
|
+
buttons, post = self.d.menu(["g1 = 2; g2 = 3; jump title 2 chapter 1;", "g3 = 4; g4 = 5; jump title 2;"],
|
|
145
|
+
"jump title 2;", "vts", 1)
|
|
146
|
+
self.assertEqual(hexes(buttons), ["7101000f0001000d", "7101000f0002000d"])
|
|
147
|
+
self.assertEqual(hexes(post), ["6100000e000f0000", "7100000f00000000", "00a1000e00010006",
|
|
148
|
+
"00a1000e00020009", "3003000000020000", "7100000100020000",
|
|
149
|
+
"7100000200030000", "3005000100020000", "7100000300040000",
|
|
150
|
+
"7100000400050000", "3003000000020000"])
|
|
151
|
+
|
|
141
152
|
def test_dead_code_after_a_jump(self):
|
|
142
153
|
self.assertEqual(hexes(self.d.compile("g1 = 0; resume; g1 = 0; jump title 1 chapter 2; "
|
|
143
154
|
"subtitle = 65; audio = 1;", "vtsm", 1)),
|
|
@@ -191,7 +202,7 @@ class TestForms(unittest.TestCase):
|
|
|
191
202
|
with self.assertRaises(VtsError):
|
|
192
203
|
self.d.menu(["g1 = 1; g2 = 2;"], "g3 = 3;", "vtsm", 1) # post that does not leave
|
|
193
204
|
with self.assertRaises(VtsError):
|
|
194
|
-
self.d.menu(["exit;"], "exit;", "
|
|
205
|
+
self.d.menu(["exit;"], "exit;", "fp") # no buttons in the first play
|
|
195
206
|
|
|
196
207
|
|
|
197
208
|
def author(folder, menu_n, menu_h, a, b):
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Buttons over a stretch of a title (vtskit.menu.TitleButtons): the VOB
|
|
2
|
+
checked byte for byte against dvdauthor's for buttons over the whole
|
|
3
|
+
title, the highlight information only in the VOBUs of a stretch, and
|
|
4
|
+
libdvdnav offering the buttons during the stretch alone and running their
|
|
5
|
+
commands."""
|
|
6
|
+
|
|
7
|
+
import ctypes
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import struct
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
import unittest
|
|
15
|
+
|
|
16
|
+
from vtskit import VtsError, lang, menu, ps, spu, subtitles, titleset, vm, vmg
|
|
17
|
+
from vtskit.menu import Button, TitleButtons
|
|
18
|
+
from vtskit.titleset import Audio, Title
|
|
19
|
+
|
|
20
|
+
from .test_menu import RECTS, png
|
|
21
|
+
from .test_titleset import encode
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from . import dvdnav_sim
|
|
25
|
+
LIBDVDNAV = dvdnav_sim.load_library()
|
|
26
|
+
except Exception: # pragma: no cover
|
|
27
|
+
LIBDVDNAV = None
|
|
28
|
+
|
|
29
|
+
TOOLS_OK = bool(shutil.which("ffmpeg") and shutil.which("dvdauthor") and shutil.which("spumux"))
|
|
30
|
+
ENV = {**os.environ, "VIDEO_FORMAT": "NTSC"}
|
|
31
|
+
YELLOW, RED = (0xD2, 0x92, 0x10), (0x52, 0xF0, 0x5A)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def read(path):
|
|
35
|
+
with open(path, "rb") as f:
|
|
36
|
+
return f.read()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def buttons(start, end, first=(vm.jump_vts_ptt(1, 1), vm.exit())):
|
|
40
|
+
return TitleButtons(start, end, [Button(RECTS[0], first[0].encode(), up=2, down=2),
|
|
41
|
+
Button(RECTS[1], first[1].encode(), up=1, down=1)],
|
|
42
|
+
highlight=(0, 15), select=(1, 15))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def navs(vob):
|
|
46
|
+
"""(sector, VOBU s_ptm, the PCI's highlight information) of every navigation pack."""
|
|
47
|
+
out = []
|
|
48
|
+
for pk in ps.iter_packs(vob):
|
|
49
|
+
if pk.nav:
|
|
50
|
+
o = pk.sector * ps.SECTOR + 0x2D
|
|
51
|
+
out.append((pk.sector, struct.unpack_from(">I", vob, o + 0x0C)[0], vob[o + 0x60:o + 0x316]))
|
|
52
|
+
return out
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TestFields(unittest.TestCase):
|
|
56
|
+
def test_hli(self):
|
|
57
|
+
h = buttons(1.0, 2.0).hli(1000, 2000)
|
|
58
|
+
self.assertEqual(struct.unpack_from(">HIIIHBBBBBB", h, 0),
|
|
59
|
+
(1, 1000, 2000, 2000, 0x1000, 0, 2, 2, 0, 0, 0))
|
|
60
|
+
self.assertEqual(h[0x16:0x1E].hex(), "000000f0001000f0") # dvdauthor's for a title
|
|
61
|
+
self.assertEqual(h[0x2E:0x34].hex(), "46412c06408c") # colour table 1
|
|
62
|
+
|
|
63
|
+
def test_cue(self):
|
|
64
|
+
c = buttons(1.0, 3.5).cue(720, 480)
|
|
65
|
+
self.assertEqual((c.start, c.end, c.x, c.y, c.forced), (1.0, 3.5, 100, 100, True))
|
|
66
|
+
self.assertEqual((c.picture.width, c.picture.height), (200, 140))
|
|
67
|
+
self.assertEqual((c.colours, c.contrasts), ((0, 0, 0, 0), (0, 0, 0, 0)))
|
|
68
|
+
area, pixels, cmds = spu.decode(subtitles.unit(c))
|
|
69
|
+
self.assertEqual(area, (100, 299, 100, 239))
|
|
70
|
+
self.assertEqual(cmds[0], 0x00) # forced start
|
|
71
|
+
self.assertEqual(sum(1 for v in pixels.values() if v == 1), 2 * 200 * 40)
|
|
72
|
+
shown = TitleButtons(0, None, buttons(0, 1).buttons, (0, 15), (1, 15), normal=(2, 9))
|
|
73
|
+
c = shown.cue(720, 480, length=8)
|
|
74
|
+
self.assertEqual((c.end, c.colours, c.contrasts), (8, (0, 2, 0, 0), (0, 9, 0, 0)))
|
|
75
|
+
with self.assertRaisesRegex(VtsError, "length"):
|
|
76
|
+
shown.cue(720, 480)
|
|
77
|
+
|
|
78
|
+
def test_refused(self):
|
|
79
|
+
bad = [TitleButtons(2.0, 1.0, buttons(0, 1).buttons, (0, 15), (1, 15)),
|
|
80
|
+
TitleButtons(0, 1, buttons(0, 1).buttons, (16, 15), (1, 15)),
|
|
81
|
+
TitleButtons(0, 1, buttons(0, 1).buttons, (0, 15), (1,)),
|
|
82
|
+
TitleButtons(0, 1, [], (0, 15), (1, 15)),
|
|
83
|
+
TitleButtons(0, 1, [Button((0, 0, 800, 10), bytes(8))], (0, 15), (1, 15)),
|
|
84
|
+
TitleButtons(0, 1, buttons(0, 1).buttons, (0, 15), (1, 15), first_button=3)]
|
|
85
|
+
for tb in bad:
|
|
86
|
+
with self.assertRaises(VtsError, msg=tb):
|
|
87
|
+
menu.check_title_buttons(tb, 720, 480, "t")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@unittest.skipUnless(TOOLS_OK, "needs ffmpeg, spumux and dvdauthor")
|
|
91
|
+
class TestTitleButtons(unittest.TestCase):
|
|
92
|
+
@classmethod
|
|
93
|
+
def setUpClass(cls):
|
|
94
|
+
cls.tmp = t = tempfile.mkdtemp(prefix="vtskit-tbtn-")
|
|
95
|
+
cls.plain = os.path.join(t, "plain.mpg")
|
|
96
|
+
encode(cls.plain, seconds=10)
|
|
97
|
+
cls.short = os.path.join(t, "short.mpg")
|
|
98
|
+
encode(cls.short, seconds=2)
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def tearDownClass(cls):
|
|
102
|
+
shutil.rmtree(cls.tmp, ignore_errors=True)
|
|
103
|
+
|
|
104
|
+
def test_whole_title_as_dvdauthor(self):
|
|
105
|
+
# spumux's buttons over the whole title, then dvdauthor; vtskit
|
|
106
|
+
# given the same stream and buttons without an end writes the same
|
|
107
|
+
# VOB (dvdauthor writes the HLI in every VOBU, never ending) and IFO
|
|
108
|
+
t = self.tmp
|
|
109
|
+
png(f"{t}/hl.png", 720, 480, RECTS, (255, 255, 0))
|
|
110
|
+
png(f"{t}/sel.png", 720, 480, RECTS, (255, 0, 0))
|
|
111
|
+
with open(f"{t}/spu.xml", "w") as f:
|
|
112
|
+
f.write('<subpictures format="NTSC"><stream><spu start="00:00:00.00" end="00:00:10.00" '
|
|
113
|
+
f'highlight="{t}/hl.png" select="{t}/sel.png">'
|
|
114
|
+
'<button name="b1" x0="100" y0="100" x1="300" y1="140" up="b2" down="b2"/>'
|
|
115
|
+
'<button name="b2" x0="100" y0="200" x1="300" y1="240" up="b1" down="b1"/>'
|
|
116
|
+
'</spu></stream></subpictures>')
|
|
117
|
+
sp = f"{t}/buttons.mpg"
|
|
118
|
+
with open(self.plain, "rb") as src, open(sp, "wb") as dst:
|
|
119
|
+
subprocess.run(["spumux", "-s", "0", f"{t}/spu.xml"], stdin=src, stdout=dst, check=True,
|
|
120
|
+
stderr=subprocess.DEVNULL, env=ENV)
|
|
121
|
+
texts = ["g1 = 2; g2 = 3; jump title 2 chapter 1;", "exit;"]
|
|
122
|
+
with open(f"{t}/dvd.xml", "w") as f:
|
|
123
|
+
f.write(f'<dvdauthor dest="{t}/dvd"><vmgm/><titleset><titles>'
|
|
124
|
+
'<video format="ntsc" aspect="4:3"/><audio format="ac3" channels="2" lang="en"/>'
|
|
125
|
+
f'<pgc><vob file="{sp}"/><button name="b1">{texts[0]}</button>'
|
|
126
|
+
f'<button name="b2">{texts[1]}</button><post>jump title 2;</post></pgc>'
|
|
127
|
+
f'<pgc><vob file="{self.short}"/></pgc></titles></titleset></dvdauthor>')
|
|
128
|
+
subprocess.run(["dvdauthor", "-x", f"{t}/dvd.xml"], check=True, capture_output=True, env=ENV)
|
|
129
|
+
# vtskit: the same buttons compiled with vtskit.lang (a button of
|
|
130
|
+
# several commands runs them from the post, through a dispatcher)
|
|
131
|
+
disc = lang.Disc({(1, 1): 1, (1, 2): 2}, titlesets={1: lang.Menus()})
|
|
132
|
+
commands, post = disc.menu(texts, "jump title 2;", "vts", 1)
|
|
133
|
+
tb = buttons(0, None)
|
|
134
|
+
for b, c in zip(tb.buttons, commands):
|
|
135
|
+
b.command = c
|
|
136
|
+
mine = f"{t}/mine-whole"
|
|
137
|
+
titleset.write(mine, 1, [Title(sp, audio=(Audio("en", 2),), post=tuple(post), buttons=(tb,)),
|
|
138
|
+
Title(self.short, audio=(Audio("en", 2),))])
|
|
139
|
+
theirs = f"{t}/dvd/VIDEO_TS"
|
|
140
|
+
a, b = read(f"{mine}/VTS_01_1.VOB"), read(f"{theirs}/VTS_01_1.VOB")
|
|
141
|
+
self.assertEqual([n[2] for n in navs(a)], [n[2] for n in navs(b)])
|
|
142
|
+
self.assertEqual(a, b)
|
|
143
|
+
# the IFO: the same, the title's pre (SetHL_BTNN 1) and post
|
|
144
|
+
# included, but for the palette (spumux's colours)
|
|
145
|
+
a, b = read(f"{mine}/VTS_01_0.IFO"), read(f"{theirs}/VTS_01_0.IFO")
|
|
146
|
+
self.assertEqual(len(a), len(b))
|
|
147
|
+
pgc = struct.unpack_from(">I", a, 0xCC)[0] * 2048 + 16
|
|
148
|
+
self.assertEqual(a[:pgc + 0xA4], b[:pgc + 0xA4])
|
|
149
|
+
self.assertEqual(a[pgc + 0xE4:], b[pgc + 0xE4:])
|
|
150
|
+
|
|
151
|
+
def stretch_disc(self, start, end):
|
|
152
|
+
"""Title 1 (10 s) with buttons from `start` to `end` (button 1:
|
|
153
|
+
title 2, button 2: exit), title 2 short."""
|
|
154
|
+
t = self.tmp
|
|
155
|
+
tb = buttons(start, end, (vm.jump_vts_tt(2), vm.exit()))
|
|
156
|
+
mpg = f"{t}/stretch-{start}-{end}.mpg"
|
|
157
|
+
with open(mpg, "wb") as f:
|
|
158
|
+
f.write(subtitles.mux(read(self.plain), {0: [tb.cue(720, 480)]}))
|
|
159
|
+
out = f"{t}/disc-{start}-{end}"
|
|
160
|
+
ts = titleset.write(out, 1, [Title(mpg, audio=(Audio("en", 2),), palette=(YELLOW, RED),
|
|
161
|
+
buttons=(tb,)),
|
|
162
|
+
Title(self.short, audio=(Audio("en", 2),))])
|
|
163
|
+
vmg.write(out, [ts])
|
|
164
|
+
return out
|
|
165
|
+
|
|
166
|
+
def test_hli_in_the_stretch_only(self):
|
|
167
|
+
out = self.stretch_disc(3.0, 6.0)
|
|
168
|
+
packs = navs(read(f"{out}/VTS_01_1.VOB"))
|
|
169
|
+
packs = packs[:next(k for k in range(1, len(packs)) if packs[k][1] < packs[k - 1][1])] # title 1's
|
|
170
|
+
s0 = packs[0][1]
|
|
171
|
+
ends = [p[1] for p in packs[1:]] + [None]
|
|
172
|
+
lit = 0
|
|
173
|
+
for (sector, s_ptm, h), e_ptm in zip(packs, ends):
|
|
174
|
+
during = (e_ptm is None or e_ptm > s0 + 270000) and s_ptm < s0 + 540000
|
|
175
|
+
if during:
|
|
176
|
+
lit += 1
|
|
177
|
+
self.assertEqual(struct.unpack_from(">HIII", h, 0),
|
|
178
|
+
(1, s0 + 270000, s0 + 540000, s0 + 540000), sector)
|
|
179
|
+
self.assertEqual(h[17], 2) # btn_ns
|
|
180
|
+
else:
|
|
181
|
+
self.assertEqual(h, bytes(len(h)), sector) # hli_ss 0, nothing
|
|
182
|
+
self.assertGreater(lit, 4)
|
|
183
|
+
self.assertLess(lit, len(packs) - 4)
|
|
184
|
+
|
|
185
|
+
def test_refused_in_a_title(self):
|
|
186
|
+
def write(*tbs, stream=None):
|
|
187
|
+
titleset.write(os.path.join(self.tmp, "refused"), 1,
|
|
188
|
+
[Title(stream or self.plain, audio=(Audio("en", 2),), buttons=tbs)])
|
|
189
|
+
with self.assertRaisesRegex(VtsError, "overlap"):
|
|
190
|
+
write(buttons(1, 4), buttons(3, 5))
|
|
191
|
+
with self.assertRaisesRegex(VtsError, "overlap"):
|
|
192
|
+
write(buttons(1, None), buttons(3, 5))
|
|
193
|
+
with self.assertRaisesRegex(VtsError, "one set of buttons"):
|
|
194
|
+
write(buttons(1, 3.0), buttons(3.05, 3.2), buttons(3.25, 5)) # in the VOBU from 3.0 s
|
|
195
|
+
with self.assertRaisesRegex(VtsError, "after the title ends"):
|
|
196
|
+
write(buttons(30, 40))
|
|
197
|
+
|
|
198
|
+
def test_stretches_meeting_in_a_vobu(self):
|
|
199
|
+
# the VOBUs start every 0.6006 s; where two stretches meet inside a
|
|
200
|
+
# VOBU, it carries the one covering more of it
|
|
201
|
+
def starts(*tbs):
|
|
202
|
+
"""{VOBU start (s, rounded): its HLI's start from the title's start}"""
|
|
203
|
+
out = os.path.join(self.tmp, "meeting")
|
|
204
|
+
titleset.write(out, 1, [Title(self.plain, audio=(Audio("en", 2),), buttons=tbs)])
|
|
205
|
+
packs = navs(read(f"{out}/VTS_01_1.VOB"))
|
|
206
|
+
s0 = packs[0][1]
|
|
207
|
+
return {round((s - s0) / 90000, 1): struct.unpack_from(">I", h, 2)[0] - s0 if h[1] else None
|
|
208
|
+
for _, s, h in packs}
|
|
209
|
+
got = starts(buttons(1, 2.8), buttons(2.9, 5))
|
|
210
|
+
self.assertEqual((got[1.8], got[2.4], got[3.0]), (90000, 90000, 261000))
|
|
211
|
+
# buttons that change a frame before or after a VOBU's start (as a
|
|
212
|
+
# film title's frames fall): each VOBU still carries its own
|
|
213
|
+
for edge in (3.003 - 3003 / 90000, 3.003 + 3003 / 90000):
|
|
214
|
+
got = starts(buttons(1, edge), buttons(edge, 5))
|
|
215
|
+
self.assertEqual((got[2.4], got[3.0]), (90000, int(round(edge * 90000))), edge)
|
|
216
|
+
|
|
217
|
+
@unittest.skipUnless(LIBDVDNAV, "needs libdvdnav")
|
|
218
|
+
def test_under_libdvdnav(self):
|
|
219
|
+
out = self.stretch_disc(3.0, 6.0)
|
|
220
|
+
with dvdnav_sim.DvdSim(out, LIBDVDNAV) as d:
|
|
221
|
+
self.assertEqual(d.wait_title(), (1, 1))
|
|
222
|
+
seen = [] # (VOBU start, buttons) through title 1
|
|
223
|
+
e = dvdnav_sim.EV_NAV_PACKET
|
|
224
|
+
while not d.stopped: # (title 1 ends with Exit)
|
|
225
|
+
if e == dvdnav_sim.EV_NAV_PACKET and d.domain() == "title":
|
|
226
|
+
self.assertEqual(d.title_part()[0], 1)
|
|
227
|
+
raw = ctypes.string_at(LIBDVDNAV.dvdnav_get_current_nav_pci(d.h), 16)
|
|
228
|
+
seen.append((int.from_bytes(raw[12:16], sys.byteorder), d.button_count()))
|
|
229
|
+
e = d.step()
|
|
230
|
+
s0 = seen[0][0]
|
|
231
|
+
starts = [s for s, _ in seen]
|
|
232
|
+
ends = starts[1:] + [None]
|
|
233
|
+
expect = [2 if (e is None or e > s0 + 270000) and s < s0 + 540000 else 0
|
|
234
|
+
for s, e in zip(starts, ends)]
|
|
235
|
+
self.assertEqual([n for _, n in seen], expect)
|
|
236
|
+
self.assertIn(0, expect[:2])
|
|
237
|
+
# the first button, pressed while the buttons are there: title 2
|
|
238
|
+
with dvdnav_sim.DvdSim(out, LIBDVDNAV) as d:
|
|
239
|
+
d.wait_title()
|
|
240
|
+
d.run_until(lambda e: e == dvdnav_sim.EV_NAV_PACKET and d.button_count() > 0)
|
|
241
|
+
self.assertEqual(d.title_part(), (1, 1))
|
|
242
|
+
d.activate(1)
|
|
243
|
+
self.assertEqual(d.wait_title(), (2, 1))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
if __name__ == "__main__":
|
|
247
|
+
unittest.main()
|
|
@@ -25,10 +25,10 @@ authored):
|
|
|
25
25
|
* `audio = 1; subtitle = 65;` next to each other are one SetSTN; `g1 = 0;
|
|
26
26
|
jump menu 2;` is one command (vtskit.vm folds a Set and a Link), and
|
|
27
27
|
what follows an unconditional jump in a block is left out;
|
|
28
|
-
* a menu's button runs one command: a button whose text
|
|
29
|
-
sets GPRM 15 to its number and links to the end of the
|
|
30
|
-
whose post commands start with a dispatcher (`g14 = g15;
|
|
31
|
-
Goto per such button) and end with each button's code;
|
|
28
|
+
* a menu's or a title's button runs one command: a button whose text
|
|
29
|
+
compiles to more sets GPRM 15 to its number and links to the end of the
|
|
30
|
+
program chain, whose post commands start with a dispatcher (`g14 = g15;
|
|
31
|
+
g15 = 0;` and a Goto per such button) and end with each button's code;
|
|
32
32
|
* a jump into another titleset (dvdauthor's "jumppad") cannot be one
|
|
33
33
|
command: from the video manager a jump to a chapter sets GPRM 15 to a
|
|
34
34
|
code naming the title and chapter and jumps to the titleset's chapter
|
|
@@ -572,10 +572,12 @@ class Disc:
|
|
|
572
572
|
def menu(self, buttons, post, domain="vtsm", vts=0):
|
|
573
573
|
"""A menu's buttons and post commands: ([one command a button],
|
|
574
574
|
post commands). A button whose text takes several commands runs
|
|
575
|
-
them from the post, through a dispatcher, as dvdauthor arranges.
|
|
575
|
+
them from the post, through a dispatcher, as dvdauthor arranges.
|
|
576
|
+
A title's buttons (domain "vts") are compiled the same way, as
|
|
577
|
+
dvdauthor compiles them."""
|
|
576
578
|
self._check_domain(domain)
|
|
577
|
-
if domain not in ("vtsm", "vmgm"):
|
|
578
|
-
raise VtsError("buttons belong to menus")
|
|
579
|
+
if domain not in ("vtsm", "vmgm", "vts"):
|
|
580
|
+
raise VtsError("buttons belong to menus and titles")
|
|
579
581
|
commands, deferred = [], []
|
|
580
582
|
for n, text in enumerate(buttons, 1):
|
|
581
583
|
items = self.instructions(text, domain, vts)
|
|
@@ -14,6 +14,17 @@ normal, highlight, selection, and the overlay draws palette 0.
|
|
|
14
14
|
A menu without a stream is a program chain of commands alone (its `pre`
|
|
15
15
|
runs and leaves), as dvdauthor writes a `<pgc>` without a `<vob>`.
|
|
16
16
|
|
|
17
|
+
A title may carry buttons too, over a stretch of it (TitleButtons): their
|
|
18
|
+
overlay is a forced subpicture shown for the stretch (`TitleButtons.cue`,
|
|
19
|
+
added to the title's stream with vtskit.subtitles.mux), and the HLI is in
|
|
20
|
+
the navigation packs of the VOBUs that play during the stretch, with the
|
|
21
|
+
stretch's start and end as its start time (hli_s_ptm), end time (hli_e_ptm)
|
|
22
|
+
and the end of the user's selection (btn_sl_e_ptm: "ignore user after
|
|
23
|
+
this", dvdinfo); the other VOBUs have none (hli_ss 0: "no highlight
|
|
24
|
+
information for this VOBU"). dvdauthor gives a title's buttons the whole
|
|
25
|
+
title instead: the HLI in every VOBU, from the first picture, never ending,
|
|
26
|
+
which is what a stretch without an end writes.
|
|
27
|
+
|
|
17
28
|
From dvdauthor's output: the HLI is repeated whole in every VOBU
|
|
18
29
|
(HLI_SS 1), its start time is the menu's first picture, its end times are
|
|
19
30
|
0xFFFFFFFF (never), the buttons form one group of the normal kind, every
|
|
@@ -25,7 +36,7 @@ activated.
|
|
|
25
36
|
import struct
|
|
26
37
|
from dataclasses import dataclass, field
|
|
27
38
|
|
|
28
|
-
from . import spu
|
|
39
|
+
from . import spu, subtitles
|
|
29
40
|
from .errors import VtsError
|
|
30
41
|
|
|
31
42
|
YELLOW, RED = (0xD2, 0x92, 0x10), (0x52, 0xF0, 0x5A) # Y, Cr, Cb
|
|
@@ -73,6 +84,69 @@ class Menu:
|
|
|
73
84
|
uops: int = 0 # user operations the menu forbids (bit n: vtskit.vm.UOP_*)
|
|
74
85
|
|
|
75
86
|
|
|
87
|
+
@dataclass
|
|
88
|
+
class TitleButtons:
|
|
89
|
+
"""Buttons over a stretch of a title: from `start` to `end` (seconds
|
|
90
|
+
from the title's first picture; `end` None: to the end, the buttons
|
|
91
|
+
never taken off), the buttons, the entries of the title's palette
|
|
92
|
+
(0-15) and contrasts (0-15) drawing a button selected (`highlight`) and
|
|
93
|
+
activated (`select`) as (entry, contrast), the entry and contrast the
|
|
94
|
+
buttons show in otherwise (`normal`; None: nothing), the overlay (code
|
|
95
|
+
1 where a button lights up; by default the buttons' rectangles), the
|
|
96
|
+
button selected when they show (0: the player's choice) and the number
|
|
97
|
+
added to a number key's digit."""
|
|
98
|
+
start: float
|
|
99
|
+
end: float | None
|
|
100
|
+
buttons: list
|
|
101
|
+
highlight: tuple
|
|
102
|
+
select: tuple
|
|
103
|
+
normal: tuple | None = None
|
|
104
|
+
overlay: object = None
|
|
105
|
+
first_button: int = 0
|
|
106
|
+
button_offset: int = 0
|
|
107
|
+
|
|
108
|
+
def cue(self, width, height, length=None):
|
|
109
|
+
"""The overlay as a subtitle cue for vtskit.subtitles.mux: forced
|
|
110
|
+
(shown with subtitles off), from `start` to `end` (or `length`
|
|
111
|
+
seconds, the title's, for buttons without an end), transparent or
|
|
112
|
+
in the normal colour."""
|
|
113
|
+
end = self.end if self.end is not None else length
|
|
114
|
+
if end is None:
|
|
115
|
+
raise VtsError("buttons without an end: give the title's length")
|
|
116
|
+
o = self.overlay if self.overlay is not None else \
|
|
117
|
+
spu.Overlay.from_rects(width, height, [b.rect for b in self.buttons])
|
|
118
|
+
if (o.width, o.height) != (width, height):
|
|
119
|
+
raise VtsError("the overlay is not the picture's size")
|
|
120
|
+
box = o.bounds()
|
|
121
|
+
if box is None:
|
|
122
|
+
raise VtsError("the overlay is blank")
|
|
123
|
+
x0, y0, x1, y1 = box
|
|
124
|
+
crop = b"".join(o.pixels[y * width + x0:y * width + x1 + 1] for y in range(y0, y1 + 1))
|
|
125
|
+
entry, contrast = self.normal if self.normal is not None else (0, 0)
|
|
126
|
+
return subtitles.Cue(self.start, end, spu.Overlay(x1 - x0 + 1, y1 - y0 + 1, crop), x0, y0,
|
|
127
|
+
colours=(0, entry, 0, 0), contrasts=(0, contrast, 0, 0), forced=True)
|
|
128
|
+
|
|
129
|
+
def hli(self, s_ptm, e_ptm):
|
|
130
|
+
"""The highlight information shown from `s_ptm` to `e_ptm` (the
|
|
131
|
+
stretch's start and end in the VOB's time)."""
|
|
132
|
+
return _hli(self.buttons, s_ptm, e_ptm, self.button_offset, self.first_button,
|
|
133
|
+
self.highlight + self.select)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def check_title_buttons(tb, width, height, what):
|
|
137
|
+
"""Refuse buttons a title cannot carry."""
|
|
138
|
+
for name, pair in (("highlight", tb.highlight), ("selection", tb.select), ("normal", tb.normal)):
|
|
139
|
+
if pair is None and name == "normal":
|
|
140
|
+
continue
|
|
141
|
+
if len(pair) != 2 or not all(isinstance(v, int) and 0 <= v <= 15 for v in pair):
|
|
142
|
+
raise VtsError(f"{what}: the {name} is (palette entry 0-15, contrast 0-15)")
|
|
143
|
+
if tb.start < 0 or tb.end is not None and tb.end <= tb.start:
|
|
144
|
+
raise VtsError(f"{what}: buttons from {tb.start} to {tb.end} s")
|
|
145
|
+
if not tb.buttons:
|
|
146
|
+
raise VtsError(f"{what}: no buttons")
|
|
147
|
+
_check_buttons(tb.buttons, tb.first_button, tb.button_offset, width, height)
|
|
148
|
+
|
|
149
|
+
|
|
76
150
|
ENTRY_TYPES = {"title": 2, "root": 3, "subpicture": 4, "audio": 5, "angle": 6, "ptt": 7}
|
|
77
151
|
ENTRY_FLAGS = {"title": 0x80, "root": 0x80, "subpicture": 0x40, "audio": 0x20, "angle": 0x10, "ptt": 0x08}
|
|
78
152
|
|
|
@@ -98,18 +172,24 @@ def check(menu, width, height):
|
|
|
98
172
|
_colour(menu.normal, "normal")
|
|
99
173
|
if menu.entry is not None and menu.entry not in ENTRY_TYPES:
|
|
100
174
|
raise VtsError(f"{menu.entry!r} is not a menu entry kind")
|
|
101
|
-
|
|
175
|
+
_check_buttons(menu.buttons, menu.first_button, menu.button_offset, width, height)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _check_buttons(buttons, first_button, button_offset, width, height):
|
|
179
|
+
if len(buttons) > 36:
|
|
180
|
+
raise VtsError(f"{len(buttons)} buttons (at most 36)")
|
|
181
|
+
for n, b in enumerate(buttons, 1):
|
|
102
182
|
x0, y0, x1, y1 = b.rect
|
|
103
183
|
if not (0 <= x0 < x1 <= width and 0 <= y0 < y1 <= height):
|
|
104
184
|
raise VtsError(f"button {n}: {b.rect} is not inside {width}x{height}")
|
|
105
185
|
if len(b.command) != 8:
|
|
106
186
|
raise VtsError(f"button {n}: a command is 8 bytes")
|
|
107
187
|
for k in (b.up, b.down, b.left, b.right):
|
|
108
|
-
if not 0 <= k <= len(
|
|
109
|
-
raise VtsError(f"button {n} points at button {k}, which
|
|
110
|
-
if not 0 <=
|
|
111
|
-
raise VtsError(f"first button {
|
|
112
|
-
if not 0 <=
|
|
188
|
+
if not 0 <= k <= len(buttons):
|
|
189
|
+
raise VtsError(f"button {n} points at button {k}, which is not there")
|
|
190
|
+
if not 0 <= first_button <= len(buttons):
|
|
191
|
+
raise VtsError(f"first button {first_button}: there are {len(buttons)}")
|
|
192
|
+
if not 0 <= button_offset <= 255:
|
|
113
193
|
raise VtsError("the button offset is 0 to 255")
|
|
114
194
|
|
|
115
195
|
|
|
@@ -145,18 +225,28 @@ def unit(menu, width, height):
|
|
|
145
225
|
def hli(menu, s_ptm):
|
|
146
226
|
"""The highlight information of the menu's navigation packs (the PCI
|
|
147
227
|
from 0x60 to 0x316), shown from `s_ptm`, the menu's first picture."""
|
|
148
|
-
|
|
228
|
+
first = 1 if menu.normal is not None else 0
|
|
229
|
+
return _hli(menu.buttons, s_ptm, 0xFFFFFFFF, menu.button_offset, menu.first_button,
|
|
230
|
+
(first, _colour(menu.highlight, "highlight")[1],
|
|
231
|
+
first + 1, _colour(menu.select, "selection")[1]))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _hli(buttons, s_ptm, e_ptm, button_offset, first_button, colours):
|
|
235
|
+
"""The highlight information (PCI 0x60 to 0x316): the buttons, shown
|
|
236
|
+
from `s_ptm` to `e_ptm` (hli_s_ptm, hli_e_ptm; the user's selection
|
|
237
|
+
ends with them, btn_sl_e_ptm), drawn selected and activated with the
|
|
238
|
+
palette entries and contrasts `colours` (entry, contrast, entry,
|
|
239
|
+
contrast)."""
|
|
149
240
|
out = bytearray(0x316 - 0x60)
|
|
150
241
|
if not buttons:
|
|
151
242
|
return bytes(out)
|
|
152
|
-
struct.pack_into(">HIIIHBBBBBB", out, 0, 1, s_ptm,
|
|
153
|
-
|
|
243
|
+
struct.pack_into(">HIIIHBBBBBB", out, 0, 1, s_ptm, e_ptm, e_ptm, 0x1000,
|
|
244
|
+
button_offset, len(buttons), len(buttons), 0, first_button, 0)
|
|
154
245
|
# colour table 1: the pattern code, the highlight's palette entry and
|
|
155
246
|
# contrast when selected, the selection's when activated; the other
|
|
156
247
|
# codes stay as they are
|
|
157
|
-
|
|
158
|
-
out[0x16:0x1E] = bytes((0x00,
|
|
159
|
-
0x00, (first + 1) << 4, 0x00, _colour(menu.select, "selection")[1] << 4))
|
|
248
|
+
hl, hl_c, sel, sel_c = colours
|
|
249
|
+
out[0x16:0x1E] = bytes((0x00, hl << 4, 0x00, hl_c << 4, 0x00, sel << 4, 0x00, sel_c << 4))
|
|
160
250
|
for n, b in enumerate(buttons):
|
|
161
251
|
x0, y0, x1, y1 = b.rect
|
|
162
252
|
e = 0x2E + 18 * n
|
|
@@ -179,7 +179,8 @@ def _subpicture_sync(stream, vs, pos, covered):
|
|
|
179
179
|
HLI = 0x60 # the highlight information in the PCI data, to 0x316
|
|
180
180
|
|
|
181
181
|
|
|
182
|
-
def fill(data, cells, lbn=0, uops=0, stream=None, hli=None, layout=None, ilvu=None, nsml=None
|
|
182
|
+
def fill(data, cells, lbn=0, uops=0, stream=None, hli=None, layout=None, ilvu=None, nsml=None,
|
|
183
|
+
highlights=None):
|
|
183
184
|
"""Fill the navigation packs of a VOB in place (a bytearray or a
|
|
184
185
|
writable mmap).
|
|
185
186
|
|
|
@@ -192,7 +193,9 @@ def fill(data, cells, lbn=0, uops=0, stream=None, hli=None, layout=None, ilvu=No
|
|
|
192
193
|
every navigation pack of that VOB. `ilvu`: {VOBU index: (flags, end,
|
|
193
194
|
next start, next size, [(angle's next unit, size)] * 9)}, the SML_PBI
|
|
194
195
|
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.
|
|
196
|
+
index: [angle's VOBU] * 9}, the PCI's NSML_AGLI. `highlights`: {VOBU
|
|
197
|
+
index: highlight information}, buttons in some VOBUs only (a title's).
|
|
198
|
+
Returns the VOBUs."""
|
|
196
199
|
stream = stream or ps.scan(data)
|
|
197
200
|
vs = vobus(stream)
|
|
198
201
|
pos = list(layout) if layout is not None else [lbn + v.start for v in vs]
|
|
@@ -232,10 +235,13 @@ def fill(data, cells, lbn=0, uops=0, stream=None, hli=None, layout=None, ilvu=No
|
|
|
232
235
|
v.s_ptm, v.e_ptm, 0, int.from_bytes(eltm, "big"))
|
|
233
236
|
if i in nsml:
|
|
234
237
|
struct.pack_into(">9I", pci, 0x3C, *nsml[i])
|
|
235
|
-
|
|
236
|
-
|
|
238
|
+
h = highlights.get(i) if highlights else None
|
|
239
|
+
if h is None and hli and vob_id in hli:
|
|
240
|
+
h = hli[vob_id]
|
|
241
|
+
if h is not None:
|
|
242
|
+
if len(h) != 0x316 - HLI:
|
|
237
243
|
raise VtsError("the highlight information is 0x2B6 bytes")
|
|
238
|
-
pci[HLI:0x316] =
|
|
244
|
+
pci[HLI:0x316] = h
|
|
239
245
|
dsi = bytearray(0x3FA - 1)
|
|
240
246
|
# (fewer than three reference pictures: the last one's end again,
|
|
241
247
|
# as dvdauthor writes it)
|
|
@@ -63,7 +63,7 @@ import os
|
|
|
63
63
|
import struct
|
|
64
64
|
from dataclasses import dataclass, field
|
|
65
65
|
|
|
66
|
-
from . import menu as menus_, nav, ps, spu
|
|
66
|
+
from . import menu as menus_, nav, ps, spu, vm
|
|
67
67
|
from .errors import VtsError
|
|
68
68
|
|
|
69
69
|
SECTOR = ps.SECTOR
|
|
@@ -117,7 +117,11 @@ class Title:
|
|
|
117
117
|
writes none (titles are chained with commands instead). Under
|
|
118
118
|
libdvdnav, a title whose post commands run out plays on into its next
|
|
119
119
|
program chain, and one without a next stops (tests/test_titleset.py);
|
|
120
|
-
what players make of them is otherwise not described.
|
|
120
|
+
what players make of them is otherwise not described.
|
|
121
|
+
|
|
122
|
+
`buttons`: vtskit.menu.TitleButtons, buttons over stretches of the
|
|
123
|
+
title that do not overlap; their overlays must be in the stream
|
|
124
|
+
already (TitleButtons.cue, vtskit.subtitles.mux)."""
|
|
121
125
|
stream: str | None = None
|
|
122
126
|
chapters: tuple = (0.0,)
|
|
123
127
|
audio: tuple = ()
|
|
@@ -132,6 +136,7 @@ class Title:
|
|
|
132
136
|
next_pgc: int = 0
|
|
133
137
|
prev_pgc: int = 0
|
|
134
138
|
up_pgc: int = 0
|
|
139
|
+
buttons: tuple = () # vtskit.menu.TitleButtons over stretches of the title
|
|
135
140
|
|
|
136
141
|
|
|
137
142
|
@dataclass
|
|
@@ -545,18 +550,31 @@ def _clean_starts(where, stream, vs, starts):
|
|
|
545
550
|
def _title(number, title, offset, vob_id):
|
|
546
551
|
"""Read a title's parts, place its chapters, lay its VOBUs on the disc
|
|
547
552
|
(interleaving the angle blocks) and fill their navigation packs:
|
|
548
|
-
(WrittenTitle, the next free sector, the next VOB id).
|
|
553
|
+
(WrittenTitle, the next free sector, the next VOB id). The streams
|
|
554
|
+
read are closed again if the title is refused."""
|
|
555
|
+
loaded = [] # a part: [(stream, vobus, bytes)] one an angle
|
|
556
|
+
try:
|
|
557
|
+
return _lay_title(number, title, offset, vob_id, loaded)
|
|
558
|
+
except BaseException:
|
|
559
|
+
for part in loaded:
|
|
560
|
+
for _, _, buffer in part:
|
|
561
|
+
_close(buffer)
|
|
562
|
+
raise
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _lay_title(number, title, offset, vob_id, loaded):
|
|
549
566
|
parts = title.parts or (title.stream,)
|
|
550
567
|
if not parts or title.stream and title.parts:
|
|
551
568
|
raise VtsError(f"title {number}: give a stream or parts, not both")
|
|
552
|
-
loaded = [] # a part: [(stream, vobus, bytes)] one an angle
|
|
553
569
|
for k, part in enumerate(parts, 1):
|
|
554
570
|
if isinstance(part, Angles):
|
|
555
571
|
if not 2 <= len(part.streams) <= 9:
|
|
556
572
|
raise VtsError(f"title {number}, part {k}: 2 to 9 angles")
|
|
557
|
-
angles = [
|
|
558
|
-
_aligned(angles, f"title {number}, part {k}")
|
|
573
|
+
angles = []
|
|
559
574
|
loaded.append(angles)
|
|
575
|
+
for p in part.streams:
|
|
576
|
+
angles.append(_load(p))
|
|
577
|
+
_aligned(angles, f"title {number}, part {k}")
|
|
560
578
|
elif isinstance(part, str):
|
|
561
579
|
loaded.append([_load(part)])
|
|
562
580
|
else:
|
|
@@ -703,13 +721,14 @@ def _title(number, title, offset, vob_id):
|
|
|
703
721
|
for a in range(n_angles):
|
|
704
722
|
jobs.append((k, a, vob_id + a))
|
|
705
723
|
vob_id += n_angles
|
|
724
|
+
highlights = _title_buttons(number, title, loaded, part_start)
|
|
706
725
|
# fill every VOB now that every sector, and every flag of the VOBUs
|
|
707
726
|
# before an angle block, is known
|
|
708
727
|
for k, a, vid in jobs:
|
|
709
728
|
st, vs, vob = loaded[k][a]
|
|
710
729
|
vob_cells = [(vid, c.cell_id, c.first_vobu, c.last_vobu) for c in cells if c.vob_id == vid]
|
|
711
730
|
nav.fill(vob, vob_cells, stream=st, layout=layouts[(k, a)], ilvu=ilvu_info[(k, a)],
|
|
712
|
-
nsml=nsml_info[(k, a)])
|
|
731
|
+
nsml=nsml_info[(k, a)], highlights=highlights.get((k, a)))
|
|
713
732
|
lay = layouts[(k, a)]
|
|
714
733
|
if len(loaded[k]) == 1:
|
|
715
734
|
chunks.append((lay[0], vob, 0, len(vob)))
|
|
@@ -729,6 +748,71 @@ def _title(number, title, offset, vob_id):
|
|
|
729
748
|
return w, sector, vob_id
|
|
730
749
|
|
|
731
750
|
|
|
751
|
+
def _pre(title):
|
|
752
|
+
"""A title's pre commands: with buttons, first the first button
|
|
753
|
+
highlighted (SetHL_BTNN 1), as dvdauthor writes it for a title with
|
|
754
|
+
buttons (tests/test_title_buttons.py)."""
|
|
755
|
+
if not title.buttons:
|
|
756
|
+
return tuple(title.pre)
|
|
757
|
+
return (vm.set_hl_btnn(1 << 10).encode(),) + tuple(title.pre)
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def _title_buttons(number, title, loaded, part_start):
|
|
761
|
+
"""The HLI of the title's buttons: {(part, angle): {VOBU: HLI}}, in the
|
|
762
|
+
VOBUs that play during each stretch (every angle's), shown from the
|
|
763
|
+
stretch's start to its end in the VOB's time (a stretch that began in
|
|
764
|
+
an earlier part from the VOB's start; one that goes on into a later
|
|
765
|
+
part to the VOB's end; one without an end never ending). Where two
|
|
766
|
+
stretches meet inside a VOBU, it carries the one that covers more of
|
|
767
|
+
it: buttons that change between VOBUs (oncemore starts a VOBU there)
|
|
768
|
+
are not lost to a picture's difference between a time and the VOBU's
|
|
769
|
+
start, as in a film title with pulldown."""
|
|
770
|
+
if not title.buttons:
|
|
771
|
+
return {}
|
|
772
|
+
width, height = loaded[0][0][0].sequence[:2]
|
|
773
|
+
stretches = []
|
|
774
|
+
for n, tb in enumerate(title.buttons, 1):
|
|
775
|
+
what = f"title {number}, buttons {n}"
|
|
776
|
+
menus_.check_title_buttons(tb, width, height, what)
|
|
777
|
+
end = None if tb.end is None else int(round(tb.end * 90000))
|
|
778
|
+
stretches.append((int(round(tb.start * 90000)), end, tb, what))
|
|
779
|
+
stretches.sort(key=lambda x: x[0])
|
|
780
|
+
for (_, e1, _, w1), (s2, _, _, w2) in zip(stretches, stretches[1:]):
|
|
781
|
+
if e1 is None or e1 > s2:
|
|
782
|
+
raise VtsError(f"{w1} and {w2} overlap in time")
|
|
783
|
+
out = {}
|
|
784
|
+
touched, used = set(), set()
|
|
785
|
+
for k, part in enumerate(loaded):
|
|
786
|
+
for a, (_, vs, _) in enumerate(part):
|
|
787
|
+
base, vob_end = vs[0].s_ptm, vs[-1].e_ptm
|
|
788
|
+
table = out[(k, a)] = {}
|
|
789
|
+
for i, v in enumerate(vs):
|
|
790
|
+
t0 = part_start[k] + v.s_ptm - base
|
|
791
|
+
t1 = part_start[k] + v.e_ptm - base
|
|
792
|
+
# (two stretches in a VOBU: the one covering more of it, the
|
|
793
|
+
# later on a tie; a VOBU carries one set of buttons)
|
|
794
|
+
best = None
|
|
795
|
+
for s, e, tb, what in stretches:
|
|
796
|
+
cover = min(t1, e if e is not None else t1) - max(t0, s)
|
|
797
|
+
if cover > 0:
|
|
798
|
+
touched.add(what)
|
|
799
|
+
if best is None or cover >= best[0]:
|
|
800
|
+
best = (cover, s, e, tb, what)
|
|
801
|
+
if best is None:
|
|
802
|
+
continue
|
|
803
|
+
_, s, e, tb, what = best
|
|
804
|
+
he = 0xFFFFFFFF if e is None else min(base + e - part_start[k], vob_end)
|
|
805
|
+
table[i] = tb.hli(base + max(s - part_start[k], 0), he)
|
|
806
|
+
used.add(what)
|
|
807
|
+
for _, _, _, what in stretches:
|
|
808
|
+
if what not in touched:
|
|
809
|
+
raise VtsError(f"{what} start after the title ends")
|
|
810
|
+
if what not in used:
|
|
811
|
+
raise VtsError(f"{what} are shown for less of each VOBU they fall in than the buttons "
|
|
812
|
+
"beside them: a VOBU carries one set of buttons")
|
|
813
|
+
return out
|
|
814
|
+
|
|
815
|
+
|
|
732
816
|
def _attributes(written, pan_scan, letterbox, audio, subpictures, what, captions=(False, False)):
|
|
733
817
|
"""(video attr, audio count, audio attrs, subpicture count, subpicture
|
|
734
818
|
attrs, sub-streams) of the VOBs: the picture the same in every one, the
|
|
@@ -893,7 +977,7 @@ def _write(folder, vts, titles, written, menus, language, pan_scan, letterbox, c
|
|
|
893
977
|
for name in ("next_pgc", "prev_pgc", "up_pgc"):
|
|
894
978
|
if not 0 <= getattr(t, name) <= len(titles):
|
|
895
979
|
raise VtsError(f"title {n}: {name} {getattr(t, name)}: the titleset has {len(titles)} program chains")
|
|
896
|
-
pgcs = [_pgc((t
|
|
980
|
+
pgcs = [_pgc((_pre(t), t.post, t.cell_commands), w, frame, sorted(w.stream.subpictures), t.palette,
|
|
897
981
|
stills=t.stills, uops=t.uops, video=video, links=(t.next_pgc, t.prev_pgc, t.up_pgc))
|
|
898
982
|
for t, w in zip(titles, written)]
|
|
899
983
|
pgci = struct.pack(">HHI", len(pgcs), 0, 0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vtskit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: DVD-Video titlesets in pure Python: VOBs, navigation packs and IFO files
|
|
5
5
|
Author: kajisho5
|
|
6
6
|
License-Expression: LGPL-3.0-or-later
|
|
@@ -141,7 +141,18 @@ oncemore writing its discs with vtskit.
|
|
|
141
141
|
VOBU of the same moment (NSML_AGLI). Only one of the two is filled, as
|
|
142
142
|
players take NSML_AGLI whenever it is there. A stream whose interleaved
|
|
143
143
|
units start with open groups of pictures (B-pictures leaning on the group
|
|
144
|
-
before, another angle's after a switch) is refused.
|
|
144
|
+
before, another angle's after a switch) is refused. A title may carry
|
|
145
|
+
buttons over stretches of it (`vtskit.menu.TitleButtons`: from a start
|
|
146
|
+
to an end, the buttons, the palette entries that draw them): their
|
|
147
|
+
overlay is a forced subpicture added with `vtskit.subtitles`, and the
|
|
148
|
+
highlight information goes only into the VOBUs that play during the
|
|
149
|
+
stretch (where two stretches meet inside a VOBU, the one covering more
|
|
150
|
+
of it), with the stretch's start and end as its start time, end time
|
|
151
|
+
and the end of the selection (dvdinfo's hli_s_ptm, hli_e_ptm,
|
|
152
|
+
btn_sl_e_ptm). dvdauthor gives a title's buttons the whole title (every
|
|
153
|
+
VOBU, never ending), which is what buttons without an end write. As
|
|
154
|
+
dvdauthor does, a title with buttons starts with its first button
|
|
155
|
+
highlighted (SetHL_BTNN 1 before its own pre commands).
|
|
145
156
|
* `vtskit.subtitles` adds subtitles to a title's program stream, as spumux
|
|
146
157
|
does: each cue a picture of up to four colours placed on the screen and
|
|
147
158
|
shown for a while, as subpicture packs a little before their time.
|
|
@@ -149,8 +160,8 @@ oncemore writing its discs with vtskit.
|
|
|
149
160
|
`if (g5 != 0) { button = g5; }`, `call menu entry root;`) for the domain
|
|
150
161
|
a text runs in, to the same commands dvdauthor writes for it: a tool
|
|
151
162
|
that already speaks the language, as oncemore does, keeps its text. A
|
|
152
|
-
button of several commands
|
|
153
|
-
dispatcher, and a jump to a chapter of another titleset goes through pad
|
|
163
|
+
button of several commands, a menu's or a title's, runs them from the
|
|
164
|
+
program chain's post through a dispatcher, and a jump to a chapter of another titleset goes through pad
|
|
154
165
|
program chains, as dvdauthor's "jumppads" do.
|
|
155
166
|
* `vtskit.vmg` writes the video manager: VIDEO_TS.IFO and its BUP, with
|
|
156
167
|
the first-play commands, the table of titles and the titlesets'
|
|
@@ -182,7 +193,9 @@ dvdauthor's (its buttons made with spumux), and a menu's overlay in a
|
|
|
182
193
|
normal colour is spumux's unit byte for byte; subtitles muxed by vtskit are
|
|
183
194
|
spumux's units, placed where spumux places them, and the title written
|
|
184
195
|
from that stream (4:3, and 16:9 with and without pan-scan or letterbox) is
|
|
185
|
-
dvdauthor's
|
|
196
|
+
dvdauthor's; a title with buttons over the whole of it (one of them of
|
|
197
|
+
several commands) is dvdauthor's VOB and IFO byte for byte, but for the
|
|
198
|
+
palette. A VIDEO_TS written by vtskit alone plays
|
|
186
199
|
its titles and chapters in order under libdvdnav, its commands jumping
|
|
187
200
|
between titles, its menus' buttons lead where they say, and its jumps into
|
|
188
201
|
another titleset's chapters land; a title with an angle block plays and
|
|
@@ -190,7 +203,9 @@ switches angles under libdvdnav, every pointer of its navigation packs
|
|
|
190
203
|
landing on the unit it names, and the pictures libdvdnav hands a player
|
|
191
204
|
through a switch, decoded with ffmpeg, are every picture once, in order,
|
|
192
205
|
the new angle from the end of a unit (dvdauthor writes no angles, so a
|
|
193
|
-
set-top player is the check still owed)
|
|
206
|
+
set-top player is the check still owed); a title's buttons over a stretch
|
|
207
|
+
are offered by libdvdnav in that stretch's VOBUs alone, and run their
|
|
208
|
+
commands
|
|
194
209
|
(`python -m unittest discover -s tests -t .`; a test is skipped when a tool
|
|
195
210
|
it needs is missing). oncemore's switch to vtskit (step 4) is merged:
|
|
196
211
|
[oncemore#90](https://github.com/kajisho5/oncemore/pull/90) and
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|