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.
Files changed (32) hide show
  1. {vtskit-0.1.1/vtskit.egg-info → vtskit-0.3.0}/PKG-INFO +21 -6
  2. {vtskit-0.1.1 → vtskit-0.3.0}/README.md +20 -5
  3. {vtskit-0.1.1 → vtskit-0.3.0}/pyproject.toml +1 -1
  4. {vtskit-0.1.1 → vtskit-0.3.0}/tests/test_lang.py +12 -1
  5. vtskit-0.3.0/tests/test_title_buttons.py +247 -0
  6. {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/__init__.py +1 -1
  7. {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/lang.py +9 -7
  8. {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/menu.py +103 -13
  9. {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/nav.py +11 -5
  10. {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/titleset.py +92 -8
  11. {vtskit-0.1.1 → vtskit-0.3.0/vtskit.egg-info}/PKG-INFO +21 -6
  12. {vtskit-0.1.1 → vtskit-0.3.0}/vtskit.egg-info/SOURCES.txt +1 -0
  13. {vtskit-0.1.1 → vtskit-0.3.0}/COPYING +0 -0
  14. {vtskit-0.1.1 → vtskit-0.3.0}/COPYING.LESSER +0 -0
  15. {vtskit-0.1.1 → vtskit-0.3.0}/setup.cfg +0 -0
  16. {vtskit-0.1.1 → vtskit-0.3.0}/tests/test_angles.py +0 -0
  17. {vtskit-0.1.1 → vtskit-0.3.0}/tests/test_ifo.py +0 -0
  18. {vtskit-0.1.1 → vtskit-0.3.0}/tests/test_menu.py +0 -0
  19. {vtskit-0.1.1 → vtskit-0.3.0}/tests/test_nav.py +0 -0
  20. {vtskit-0.1.1 → vtskit-0.3.0}/tests/test_subtitles.py +0 -0
  21. {vtskit-0.1.1 → vtskit-0.3.0}/tests/test_titleset.py +0 -0
  22. {vtskit-0.1.1 → vtskit-0.3.0}/tests/test_vm.py +0 -0
  23. {vtskit-0.1.1 → vtskit-0.3.0}/tests/test_vmg.py +0 -0
  24. {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/errors.py +0 -0
  25. {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/ifo.py +0 -0
  26. {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/ps.py +0 -0
  27. {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/spu.py +0 -0
  28. {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/subtitles.py +0 -0
  29. {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/vm.py +0 -0
  30. {vtskit-0.1.1 → vtskit-0.3.0}/vtskit/vmg.py +0 -0
  31. {vtskit-0.1.1 → vtskit-0.3.0}/vtskit.egg-info/dependency_links.txt +0 -0
  32. {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.1.1
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 runs them from the menu's post through a
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. A VIDEO_TS written by vtskit alone plays
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 runs them from the menu's post through a
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. A VIDEO_TS written by vtskit alone plays
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vtskit"
7
- version = "0.1.1"
7
+ version = "0.3.0"
8
8
  description = "DVD-Video titlesets in pure Python: VOBs, navigation packs and IFO files"
9
9
  readme = "README.md"
10
10
  license = "LGPL-3.0-or-later"
@@ -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;", "vts", 1)
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()
@@ -21,5 +21,5 @@ subtitles and angles, and dvdauthor's command language:
21
21
 
22
22
  from .errors import VtsError
23
23
 
24
- __version__ = "0.1.1"
24
+ __version__ = "0.3.0"
25
25
  __all__ = ["VtsError"]
@@ -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 compiles to more
29
- sets GPRM 15 to its number and links to the end of the program chain,
30
- whose post commands start with a dispatcher (`g14 = g15; g15 = 0;` and a
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
- for n, b in enumerate(menu.buttons, 1):
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(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:
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
- buttons = menu.buttons
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, 0xFFFFFFFF, 0xFFFFFFFF, 0x1000,
153
- menu.button_offset, len(buttons), len(buttons), 0, menu.first_button, 0)
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
- 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))
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. Returns the VOBUs."""
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
- if hli and vob_id in hli:
236
- if len(hli[vob_id]) != 0x316 - HLI:
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] = hli[vob_id]
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 = [_load(p) for p in part.streams]
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.pre, t.post, t.cell_commands), w, frame, sorted(w.stream.subpictures), t.palette,
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.1.1
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 runs them from the menu's post through a
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. A VIDEO_TS written by vtskit alone plays
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
@@ -8,6 +8,7 @@ tests/test_lang.py
8
8
  tests/test_menu.py
9
9
  tests/test_nav.py
10
10
  tests/test_subtitles.py
11
+ tests/test_title_buttons.py
11
12
  tests/test_titleset.py
12
13
  tests/test_vm.py
13
14
  tests/test_vmg.py
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