vtskit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
vtskit/vm.py ADDED
@@ -0,0 +1,482 @@
1
+ """The DVD virtual machine's instructions, as the 8-byte commands a program
2
+ chain carries (pre, post and cell commands, and a button's command).
3
+
4
+ The encoding is https://dvdnav.mplayerhq.hu/dvdinfo/vmi.html (types 0 to
5
+ 3; the combined set-compare-link types 4 to 6 are not written). Each
6
+ instruction here is a function returning an `Instruction`; `assemble`
7
+ turns a list of instructions and `If` blocks into the commands, resolving
8
+ the Goto lines, the way dvdauthor compiles its language (tests/test_vm.py
9
+ checks the bytes against dvdauthor's for each form):
10
+
11
+ * an instruction that can carry a comparison does so (the Link family and
12
+ the Goto with a register or a number; Jump and Call with two registers;
13
+ the Set families with a register or a number);
14
+ * otherwise `If` becomes a Goto past the body on the opposite comparison,
15
+ and a Goto needs a line to land on, so a NOP ends a program whose last
16
+ instruction is conditional;
17
+ * a Set (of a general register or a system register) followed by a Link
18
+ within the domain (LinkPGCN, LinkCN, LinkTailPGC, RSM, ...), neither
19
+ comparing, becomes the one "set then link" command (`g1 = 0; jump menu
20
+ 2;` is one command in dvdauthor's output).
21
+
22
+ `Label` and `Goto` place a program's own branches (a menu's button
23
+ dispatcher).
24
+
25
+ Registers: `GPRM(n)` is general register n (0-15), `SPRM(n)` system
26
+ register n (0-23); a plain int is a number.
27
+ """
28
+
29
+ import struct
30
+ from dataclasses import dataclass
31
+
32
+ from .errors import VtsError
33
+
34
+
35
+ class Register(int):
36
+ """A register operand; a plain int is an immediate value."""
37
+ def __new__(cls, code):
38
+ return int.__new__(cls, code)
39
+
40
+ def __repr__(self):
41
+ return f"SPRM({self - 0x80})" if self >= 0x80 else f"GPRM({int(self)})"
42
+
43
+
44
+ def GPRM(n):
45
+ if not 0 <= n <= 15:
46
+ raise VtsError(f"GPRM {n}: general registers are 0 to 15")
47
+ return Register(n)
48
+
49
+
50
+ def SPRM(n):
51
+ if not 0 <= n <= 23:
52
+ raise VtsError(f"SPRM {n}: system registers are 0 to 23")
53
+ return Register(0x80 + n)
54
+
55
+
56
+ ASTN, SPSTN, AGLN, TTN, VTS_TTN, TT_PGCN, PTTN, HL_BTNN = (SPRM(n) for n in range(1, 9))
57
+
58
+ # comparisons (vmi.html, "CMP op")
59
+ BC, EQ, NE, GE, GT, LE, LT = range(1, 8)
60
+ _INVERSE = {EQ: NE, NE: EQ, GE: LT, LT: GE, GT: LE, LE: GT}
61
+ # set operations ("SET op")
62
+ MOV, SWP, ADD, SUB, MUL, DIV, MOD, RND, AND, OR, XOR = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11
63
+ # the Link subset (operand 3 of Link, cmd 1)
64
+ TOP_CELL, NEXT_CELL, PREV_CELL, TOP_PG, NEXT_PG, PREV_PG = 1, 2, 3, 5, 6, 7
65
+ TOP_PGC, NEXT_PGC, PREV_PGC, GOUP_PGC, TAIL_PGC, RSM = 9, 10, 11, 12, 13, 16
66
+ # menus, for JumpSS and CallSS
67
+ MENU_TITLE, MENU_ROOT, MENU_SUBPICTURE, MENU_AUDIO, MENU_ANGLE, MENU_PTT = 2, 3, 4, 5, 6, 7
68
+
69
+ # user operations a program chain may forbid (dvdinfo uops.html), as bits
70
+ (UOP_TIME_SEARCH, UOP_PTT_SEARCH, UOP_TITLE_PLAY, UOP_STOP, UOP_GO_UP, UOP_TIME_PTT_SEARCH,
71
+ UOP_PREV_TOP_PG, UOP_NEXT_PG, UOP_FORWARD_SCAN, UOP_BACKWARD_SCAN, UOP_MENU_TITLE, UOP_MENU_ROOT,
72
+ UOP_MENU_SUBPICTURE, UOP_MENU_AUDIO, UOP_MENU_ANGLE, UOP_MENU_PTT, UOP_RESUME, UOP_BUTTON,
73
+ UOP_STILL_OFF, UOP_PAUSE_ON, UOP_AUDIO_CHANGE, UOP_SUBPICTURE_CHANGE, UOP_ANGLE_CHANGE,
74
+ UOP_KARAOKE_MIX, UOP_VIDEO_MODE) = (1 << n for n in range(25))
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class Condition:
79
+ """`a` (a register) compared with `b` (a register or a number)."""
80
+ op: int
81
+ a: Register
82
+ b: int
83
+
84
+ def __post_init__(self):
85
+ if self.op not in _INVERSE and self.op != BC:
86
+ raise VtsError(f"comparison {self.op} is not one of BC, EQ, NE, GE, GT, LE, LT")
87
+ if not isinstance(self.a, Register):
88
+ raise VtsError("the first operand of a comparison is a register")
89
+ if not isinstance(self.b, Register) and not 0 <= self.b <= 0xFFFF:
90
+ raise VtsError(f"{self.b} does not fit a 16-bit value")
91
+
92
+ @property
93
+ def immediate(self):
94
+ return not isinstance(self.b, Register)
95
+
96
+ def inverse(self):
97
+ if self.op == BC:
98
+ raise VtsError("a bitwise comparison has no opposite")
99
+ return Condition(_INVERSE[self.op], self.a, self.b)
100
+
101
+
102
+ def compare(a, op, b):
103
+ return Condition(op, a, b)
104
+
105
+
106
+ # where a type puts its comparison: (byte of cr1, byte of cr2, bytes of the
107
+ # immediate, immediate allowed)
108
+ _SLOTS = {
109
+ "special": (3, 5, (4, 6), True), # type 0
110
+ "link": (3, 5, (4, 6), True), # type 1, Direct 0
111
+ "jump": (6, 7, None, False), # type 1, Direct 1
112
+ "set": (6, 7, None, False), # type 2 (SetSystem)
113
+ "gprm": (2, 7, (6, 8), True), # type 3 (Set)
114
+ }
115
+
116
+
117
+ @dataclass(frozen=True)
118
+ class Instruction:
119
+ kind: str
120
+ body: bytes # the 8 bytes without a comparison
121
+ condition: object = None
122
+
123
+ def encode(self):
124
+ out = bytearray(self.body)
125
+ c = self.condition
126
+ if c is not None:
127
+ cr1, cr2, imm, allowed = _SLOTS[self.kind]
128
+ out[1] |= c.op << 4
129
+ out[cr1] = c.a
130
+ if c.immediate:
131
+ if not allowed:
132
+ raise VtsError(f"a {self.kind} instruction compares registers only")
133
+ out[1] |= 0x80
134
+ struct.pack_into(">H", out, imm[0], c.b)
135
+ else:
136
+ out[cr2] = c.b
137
+ return bytes(out)
138
+
139
+ def when(self, condition):
140
+ """This instruction made conditional (VtsError if it cannot be)."""
141
+ if self.condition is not None:
142
+ raise VtsError("the instruction already has a comparison")
143
+ if self.kind == "set" and self.body[1] & 0x0F:
144
+ raise VtsError("a set instruction with a link cannot also compare")
145
+ if condition.immediate and not _SLOTS[self.kind][3]:
146
+ raise VtsError(f"a {self.kind} instruction compares registers only")
147
+ return Instruction(self.kind, self.body, condition)
148
+
149
+
150
+ def _ins(kind, *bytes_):
151
+ return Instruction(kind, bytes(bytes_).ljust(8, b"\0"))
152
+
153
+
154
+ def _check(name, value, low, high):
155
+ if not low <= value <= high:
156
+ raise VtsError(f"{name} {value}: {low} to {high}")
157
+ return value
158
+
159
+
160
+ # --- type 0: special --------------------------------------------------------
161
+
162
+ def nop():
163
+ return _ins("special", 0, 0)
164
+
165
+
166
+ def goto(line):
167
+ """Go to line `line` (1-based) of the same command list."""
168
+ return _ins("special", 0, 1, 0, 0, 0, 0, 0, _check("line", line, 1, 255))
169
+
170
+
171
+ def break_():
172
+ return _ins("special", 0, 2)
173
+
174
+
175
+ # --- type 1, Direct 0: links within the domain -----------------------------
176
+
177
+ def _button(button):
178
+ return _check("button", button, 0, 63) << 10
179
+
180
+
181
+ def link(subset, button=0):
182
+ """LinkTopCell, LinkNextCell, ... LinkTailPGC, RSM: one of the Link
183
+ subset constants, with a button to highlight (0: none)."""
184
+ if subset not in (TOP_CELL, NEXT_CELL, PREV_CELL, TOP_PG, NEXT_PG, PREV_PG, TOP_PGC,
185
+ NEXT_PGC, PREV_PGC, GOUP_PGC, TAIL_PGC, RSM):
186
+ raise VtsError(f"{subset} is not a Link subset code")
187
+ return Instruction("link", struct.pack(">BBHHH", 0x20, 1, 0, 0, _button(button) | subset))
188
+
189
+
190
+ def link_pgcn(pgcn):
191
+ return Instruction("link", struct.pack(">BBHHH", 0x20, 4, 0, 0, _check("PGC", pgcn, 1, 32767)))
192
+
193
+
194
+ def link_pttn(pttn, button=0):
195
+ return Instruction("link", struct.pack(">BBHHH", 0x20, 5, 0, 0,
196
+ _button(button) | _check("PTT", pttn, 1, 99)))
197
+
198
+
199
+ def link_pgn(pgn, button=0):
200
+ return Instruction("link", struct.pack(">BBHHH", 0x20, 6, 0, 0,
201
+ _button(button) | _check("program", pgn, 1, 255)))
202
+
203
+
204
+ def link_cn(cn, button=0):
205
+ return Instruction("link", struct.pack(">BBHHH", 0x20, 7, 0, 0,
206
+ _button(button) | _check("cell", cn, 1, 255)))
207
+
208
+
209
+ # --- type 1, Direct 1: jumps and calls between domains ----------------------
210
+
211
+ def exit():
212
+ return _ins("jump", 0x30, 1)
213
+
214
+
215
+ def jump_tt(ttn):
216
+ """To title `ttn` of the disc (from a menu)."""
217
+ return _ins("jump", 0x30, 2, 0, 0, 0, _check("title", ttn, 1, 99))
218
+
219
+
220
+ def jump_vts_tt(ttn):
221
+ """To title `ttn` of this titleset."""
222
+ return _ins("jump", 0x30, 3, 0, 0, 0, _check("title", ttn, 1, 99))
223
+
224
+
225
+ def jump_vts_ptt(ttn, pttn):
226
+ """To chapter `pttn` of title `ttn` of this titleset."""
227
+ return Instruction("jump", struct.pack(">BBHBBH", 0x30, 5, _check("PTT", pttn, 1, 1023), 0,
228
+ _check("title", ttn, 1, 99), 0))
229
+
230
+
231
+ def jump_ss_fp():
232
+ """To the first-play program chain."""
233
+ return _ins("jump", 0x30, 6)
234
+
235
+
236
+ def jump_ss_vmgm_menu(menu):
237
+ """To the VMG's menu of this kind (MENU_TITLE)."""
238
+ return _ins("jump", 0x30, 6, 0, 0, 0, 0x40 | _check("menu", menu, 2, 7))
239
+
240
+
241
+ def jump_ss_vmgm_pgc(pgcn):
242
+ """To program chain `pgcn` of the VMG's menus."""
243
+ return Instruction("jump", struct.pack(">BBHBBH", 0x30, 6, _check("PGC", pgcn, 1, 32767),
244
+ 0, 0xC0, 0))
245
+
246
+
247
+ def jump_ss_vtsm(vts, ttn, menu):
248
+ """To the menu of kind `menu` (MENU_ROOT, ...) of titleset `vts`, for
249
+ its title `ttn`."""
250
+ return Instruction("jump", struct.pack(">BBHBBH", 0x30, 6, _check("title", ttn, 1, 99),
251
+ _check("titleset", vts, 1, 99),
252
+ 0x80 | _check("menu", menu, 2, 7), 0))
253
+
254
+
255
+ def _call(rsm_cell, byte5, pgcn=0):
256
+ return Instruction("jump", struct.pack(">BBHBBH", 0x30, 8, pgcn,
257
+ _check("resume cell", rsm_cell, 0, 255), byte5, 0))
258
+
259
+
260
+ def call_ss_fp(rsm_cell=1):
261
+ """To the first-play program chain, to resume at cell `rsm_cell`."""
262
+ return _call(rsm_cell, 0)
263
+
264
+
265
+ def call_ss_vmgm_menu(menu, rsm_cell=1):
266
+ return _call(rsm_cell, 0x40 | _check("menu", menu, 2, 7))
267
+
268
+
269
+ def call_ss_vtsm_menu(menu, rsm_cell=1):
270
+ return _call(rsm_cell, 0x80 | _check("menu", menu, 2, 7))
271
+
272
+
273
+ def call_ss_vmgm_pgc(pgcn, rsm_cell=1):
274
+ return _call(rsm_cell, 0xC0, _check("PGC", pgcn, 1, 32767))
275
+
276
+
277
+ # --- type 2: the system registers -------------------------------------------
278
+
279
+ def set_stn(audio=None, subpicture=None, angle=None):
280
+ """SetSTN: the audio stream (0-7), subpicture stream (0-63, +64 hidden) and angle
281
+ (1-9) to play, each a number or a GPRM, those given."""
282
+ values = (audio, subpicture, angle)
283
+ if all(v is None for v in values):
284
+ raise VtsError("SetSTN sets at least one of audio, subpicture and angle")
285
+ regs = [isinstance(v, Register) for v in values if v is not None]
286
+ if any(regs) and not all(regs):
287
+ raise VtsError("SetSTN takes all registers or all numbers")
288
+ direct = not regs[0]
289
+ limits = (("audio", 0, 7), ("subpicture", 0, 127), ("angle", 1, 9)) # bit 6: do not show
290
+ out = bytearray(((0x50 if direct else 0x40) | 1, 0, 0, 0, 0, 0, 0, 0))
291
+ for k, (v, (name, low, high)) in enumerate(zip(values, limits)):
292
+ if v is None:
293
+ continue
294
+ if isinstance(v, Register):
295
+ if v >= 0x80:
296
+ raise VtsError("SetSTN reads general registers only")
297
+ out[3 + k] = 0x80 | v
298
+ else:
299
+ out[3 + k] = 0x80 | _check(name, v, low, high)
300
+ return Instruction("set", bytes(out))
301
+
302
+
303
+ def set_hl_btnn(value):
304
+ """SetHL_BTNN: the highlighted button, as button number * 1024, a number
305
+ or a GPRM."""
306
+ if isinstance(value, Register):
307
+ if value >= 0x80:
308
+ raise VtsError("SetHL_BTNN reads general registers only")
309
+ return _ins("set", 0x46, 0, 0, 0, 0, value)
310
+ return Instruction("set", struct.pack(">BBHHH", 0x56, 0, 0, _check("value", value, 0, 0xFFFF), 0))
311
+
312
+
313
+ def set_nvtmr(seconds, pgcn):
314
+ """SetNVTMR: the navigation timer, a number of seconds or a GPRM, and
315
+ the program chain to jump to when it runs out."""
316
+ pgcn = _check("PGC", pgcn, 1, 32767)
317
+ if isinstance(seconds, Register):
318
+ return Instruction("set", struct.pack(">BBBBHH", 0x42, 0, 0, seconds, pgcn, 0))
319
+ return Instruction("set", struct.pack(">BBHHH", 0x52, 0, _check("seconds", seconds, 0, 0xFFFF),
320
+ pgcn, 0))
321
+
322
+
323
+ def set_gprmmd(gprm, value, counter=False):
324
+ """SetGPRMMD: general register `gprm` set to a number or a register, in
325
+ counter mode (counting seconds) if `counter`."""
326
+ mode = (0x80 if counter else 0) | _check("GPRM", gprm, 0, 15)
327
+ if isinstance(value, Register):
328
+ return Instruction("set", struct.pack(">BBBBBBH", 0x43, 0, 0, value, 0, mode, 0))
329
+ return Instruction("set", struct.pack(">BBHBBH", 0x53, 0, _check("value", value, 0, 0xFFFF),
330
+ 0, mode, 0))
331
+
332
+
333
+ # --- type 3: arithmetic on the general registers ----------------------------
334
+
335
+ def set_gprm(gprm, op, value):
336
+ """Set: `gprm` op= `value`, a number or a register (MOV, SWP, ADD, SUB,
337
+ MUL, DIV, MOD, RND, AND, OR, XOR)."""
338
+ _check("GPRM", gprm, 0, 15)
339
+ if not MOV <= op <= XOR:
340
+ raise VtsError(f"{op} is not a set operation")
341
+ if isinstance(value, Register):
342
+ return Instruction("gprm", struct.pack(">BBHBBH", 0x60 | op, 0, gprm, 0, value, 0))
343
+ return Instruction("gprm", struct.pack(">BBHHH", 0x70 | op, 0, gprm,
344
+ _check("value", value, 0, 0xFFFF), 0))
345
+
346
+
347
+ # --- programs ---------------------------------------------------------------
348
+
349
+ @dataclass
350
+ class If:
351
+ """`then` (instructions and Ifs) when `condition` holds, else `orelse`.
352
+ `condition` is a Condition, or a list of them that must all hold."""
353
+ condition: object
354
+ then: list
355
+ orelse: list = ()
356
+
357
+
358
+ @dataclass
359
+ class Any:
360
+ """`then` when any of the `conditions` holds, else `orelse`."""
361
+ conditions: list
362
+ then: list
363
+ orelse: list = ()
364
+
365
+
366
+ @dataclass(frozen=True)
367
+ class Label:
368
+ """A place a Goto lands on; `name` is any hashable."""
369
+ name: object
370
+
371
+
372
+ @dataclass(frozen=True)
373
+ class Goto:
374
+ """Go to the Label `name`, when `condition` holds (always if None)."""
375
+ name: object
376
+ condition: object = None
377
+
378
+
379
+ def _transfers(ins):
380
+ """Whether this instruction always leaves the command list."""
381
+ return isinstance(ins, Instruction) and ins.condition is None and \
382
+ (ins.kind == "jump" or (ins.kind in ("link", "set", "gprm") and ins.body[1] & 0x0F))
383
+
384
+
385
+ def _fold(placed):
386
+ """A Set followed by a Link, neither comparing and nothing landing
387
+ between them, as one command."""
388
+ out = []
389
+ for p in placed:
390
+ prev = out[-1] if out else None
391
+ if isinstance(p, Instruction) and p.kind == "link" and p.condition is None \
392
+ and isinstance(prev, Instruction) and prev.kind in ("set", "gprm") \
393
+ and prev.condition is None and not prev.body[1] & 0x0F:
394
+ body = bytearray(prev.body)
395
+ body[1] |= p.body[1] & 0x0F
396
+ body[6:8] = p.body[6:8]
397
+ out[-1] = Instruction(prev.kind, bytes(body))
398
+ else:
399
+ out.append(p)
400
+ return out
401
+
402
+
403
+ def _conditions(c):
404
+ return list(c) if isinstance(c, (list, tuple)) else [c]
405
+
406
+
407
+ def _flatten(items, out):
408
+ """Place instructions and Ifs into `out`: [Instruction | ("goto", target
409
+ list id, inverse)]; returns the list."""
410
+ for item in items:
411
+ if isinstance(item, (If, Any)):
412
+ conds = _conditions(item.conditions if isinstance(item, Any) else item.condition)
413
+ body = []
414
+ _flatten(item.then, body)
415
+ orelse = []
416
+ _flatten(item.orelse, orelse)
417
+ # one plain instruction that can take the comparison: fold it in
418
+ if len(conds) == 1 and len(body) == 1 and not orelse and \
419
+ isinstance(body[0], Instruction) and body[0].condition is None:
420
+ try:
421
+ out.append(body[0].when(conds[0]))
422
+ continue
423
+ except VtsError:
424
+ pass
425
+ end = object()
426
+ else_start = object()
427
+ start = object()
428
+ skip = else_start if orelse else end
429
+ if isinstance(item, Any):
430
+ for c in conds[:-1]:
431
+ out.append(("goto", start, c))
432
+ out.append(("goto", skip, conds[-1].inverse()))
433
+ out.append(("label", start))
434
+ else:
435
+ for c in conds:
436
+ out.append(("goto", skip, c.inverse()))
437
+ out.extend(body)
438
+ if orelse:
439
+ if not (body and _transfers(body[-1])):
440
+ out.append(("goto", end, None))
441
+ out.append(("label", else_start))
442
+ out.extend(orelse)
443
+ out.append(("label", end))
444
+ elif isinstance(item, Instruction):
445
+ out.append(item)
446
+ elif isinstance(item, Label):
447
+ out.append(("label", item.name))
448
+ elif isinstance(item, Goto):
449
+ out.append(("goto", item.name, item.condition))
450
+ else:
451
+ raise VtsError(f"{item!r} is not an instruction")
452
+ return out
453
+
454
+
455
+ def assemble(items):
456
+ """The commands (8 bytes each) of a list of instructions and Ifs."""
457
+ placed = _fold(_flatten(items, []))
458
+ lines = {}
459
+ n = 0
460
+ for p in placed:
461
+ if isinstance(p, tuple) and p[0] == "label":
462
+ lines[p[1]] = n + 1
463
+ else:
464
+ n += 1
465
+ for p in placed:
466
+ if isinstance(p, tuple) and p[0] == "goto" and p[1] not in lines:
467
+ raise VtsError(f"Goto {p[1]!r}: no such Label")
468
+ needs_landing = any(isinstance(p, tuple) and p[0] == "goto" and lines[p[1]] == n + 1
469
+ for p in placed)
470
+ if needs_landing:
471
+ placed.append(nop())
472
+ n += 1
473
+ if n > 128:
474
+ raise VtsError(f"{n} commands (a program chain holds at most 128)")
475
+ out = []
476
+ for p in placed:
477
+ if isinstance(p, Instruction):
478
+ out.append(p.encode())
479
+ elif p[0] == "goto":
480
+ ins = goto(lines[p[1]])
481
+ out.append((ins.when(p[2]) if p[2] is not None else ins).encode())
482
+ return out
vtskit/vmg.py ADDED
@@ -0,0 +1,159 @@
1
+ """Write the video manager of a disc: VIDEO_TS.IFO and its BUP, and
2
+ VIDEO_TS.VOB when the disc has a title menu.
3
+
4
+ The VMG IFO (https://dvdnav.mplayerhq.hu/dvdinfo/ifo.html, ifo_vmg.html)
5
+ holds the VMGI_MAT header with the first-play program chain right after
6
+ it, TT_SRPT (every title of the disc: its titleset, number there, chapters,
7
+ angles, and where the titleset starts on the disc), VTS_ATRT (a copy of
8
+ each titleset's stream attributes) and, with menus, VMGM_PGCI_UT (one
9
+ language unit), VMGM_C_ADT and VMGM_VOBU_ADMAP. The parental masks and
10
+ the text data are absent. Each table starts at a sector; the BUP is the
11
+ IFO again.
12
+
13
+ From dvdauthor's output: the VMG takes its IFO, its menu VOB and its BUP
14
+ on the disc, 32 sectors at least, and the first titleset starts right
15
+ after; the first-play chain's playback time is 0 with the disc's
16
+ frame-rate flag, and it jumps to the title menu when there is one, else to
17
+ title 1; a title's type is 0x3C (one sequential program chain, jumps
18
+ allowed everywhere).
19
+ """
20
+
21
+ import os
22
+ import struct
23
+
24
+ from . import nav, vm
25
+ from .errors import VtsError
26
+ from .titleset import SECTOR, VOB_FILE_SECTORS, _pad, menu_domain
27
+
28
+ VMG_SECTORS = 32 # the least room dvdauthor gives the VMG
29
+
30
+
31
+ def write(folder, titlesets, menus=(), first_play=None, language="en", provider="vtskit",
32
+ pal=None, pan_scan=True, letterbox=True):
33
+ """Write VIDEO_TS.IFO and VIDEO_TS.BUP (and VIDEO_TS.VOB) for
34
+ `titlesets` (the Titleset results of vtskit.titleset.write, numbered
35
+ 1, 2, ... in order) and the VMG's `menus` (vtskit.menu.Menu; the one
36
+ with entry "title" answers the Title key). `first_play`: the first-play
37
+ chain's commands (8 bytes each; by default, as dvdauthor chooses it: a
38
+ jump to the title menu, else to titleset 1's root menu if that titleset
39
+ has menus, else to title 1). `pal`: the disc's standard, else the first
40
+ titleset's. Returns the sectors of the whole VIDEO_TS, VMG first."""
41
+ if not titlesets:
42
+ raise VtsError("a disc has at least one titleset")
43
+ if [t.vts for t in titlesets] != list(range(1, len(titlesets) + 1)):
44
+ raise VtsError("titlesets are numbered 1, 2, ... in order")
45
+ if sum(len(t.titles) for t in titlesets) > 99:
46
+ raise VtsError("a disc has at most 99 titles")
47
+ for m in menus:
48
+ if m.entry not in (None, "title"):
49
+ raise VtsError(f"a VMG menu answers the Title key or none, not {m.entry!r}")
50
+ if first_play is None:
51
+ if menus:
52
+ first_play = vm.assemble([vm.jump_ss_vmgm_menu(vm.MENU_TITLE)])
53
+ elif titlesets[0].menus:
54
+ first_play = vm.assemble([vm.jump_ss_vtsm(1, 1, vm.MENU_ROOT)])
55
+ else:
56
+ first_play = vm.assemble([vm.jump_tt(1)])
57
+ for c in first_play:
58
+ if len(c) != 8:
59
+ raise VtsError("a VM command is 8 bytes")
60
+ if not 1 <= len(first_play) <= 128:
61
+ raise VtsError("the first-play chain has 1 to 128 commands")
62
+ provider = provider.encode("ascii", "replace")[:32].ljust(32, b"\0")
63
+ if pal is None:
64
+ pal = titlesets[0].titles[0].stream.frame == 3600
65
+
66
+ # the first-play program chain: no cells, pre commands only
67
+ fp = bytearray(0xEC)
68
+ fp[4:8] = nav.elapsed(0, 3600 if pal else 3003)
69
+ struct.pack_into(">H", fp, 0xE4, 0xEC)
70
+ fp += struct.pack(">HHHH", len(first_play), 0, 0, 8 + 8 * len(first_play) - 1)
71
+ fp += b"".join(first_play)
72
+
73
+ menu_written, menu_data, menu_tables, menu_attributes = [], [], None, bytes(0x100)
74
+ if menus:
75
+ # (a video manager whose menus are commands alone: dvdauthor marks
76
+ # the picture format 0xFFF8)
77
+ menu_written, menu_data, *menu_tables, menu_attributes = menu_domain(
78
+ menus, language, 0, pan_scan, letterbox, 3600 if pal else 3003, b"\xff\xf8", domain="vmgm")
79
+ menu_sectors = sum(w.sectors for w in menu_written)
80
+ if menu_sectors > VOB_FILE_SECTORS:
81
+ raise VtsError("the VMG menus are larger than one VOB file")
82
+
83
+ # the tables, each at a sector
84
+ order = [("tt_srpt", None)]
85
+ if menus:
86
+ order.append(("vmgm_pgci_ut", menu_tables[0]))
87
+ order.append(("vts_atrt", None))
88
+ if menus:
89
+ order += [("vmgm_c_adt", menu_tables[1]), ("vmgm_admap", menu_tables[2])]
90
+ head = _pad(bytes(0x400) + fp)
91
+ ifo_sectors = len(head) // SECTOR
92
+ for name, t in order:
93
+ if t is not None:
94
+ ifo_sectors += len(_pad(t)) // SECTOR
95
+ ifo_sectors += 2 # TT_SRPT and VTS_ATRT below, a sector each (checked)
96
+ vmg_sectors = max(2 * ifo_sectors + menu_sectors, VMG_SECTORS)
97
+
98
+ # TT_SRPT: the titles of the disc
99
+ entries = b""
100
+ start = vmg_sectors
101
+ count = 0
102
+ for ts in titlesets:
103
+ for t in ts.titles:
104
+ count += 1
105
+ # (the title's type: one sequential chain, jumps everywhere, and the
106
+ # two search operations it forbids, as in its program chain)
107
+ entries += struct.pack(">BBHHBBI", 0x3C | (t.uops & 3), t.angles, len(t.programs) or 1,
108
+ 0, ts.vts, t.number, start)
109
+ start += ts.sectors
110
+ tt_srpt = struct.pack(">HHI", count, 0, 8 + len(entries) - 1) + entries
111
+
112
+ # VTS_ATRT: each titleset's attributes
113
+ atrs = []
114
+ for ts in titlesets:
115
+ if len(ts.attributes) != 0x300:
116
+ raise VtsError(f"titleset {ts.vts}: attributes are 0x300 bytes")
117
+ atrs.append(struct.pack(">II", 8 + 0x300 - 1, 0) + ts.attributes)
118
+ offsets, o = [], 8 + 4 * len(atrs)
119
+ for a in atrs:
120
+ offsets.append(o)
121
+ o += len(a)
122
+ vts_atrt = struct.pack(">HHI", len(atrs), 0, o - 1) + \
123
+ struct.pack(f">{len(atrs)}I", *offsets) + b"".join(atrs)
124
+
125
+ tables = {"tt_srpt": _pad(tt_srpt), "vts_atrt": _pad(vts_atrt)}
126
+ if menus:
127
+ tables.update(vmgm_pgci_ut=_pad(menu_tables[0]), vmgm_c_adt=_pad(menu_tables[1]),
128
+ vmgm_admap=_pad(menu_tables[2]))
129
+ sectors, pointers = len(head) // SECTOR, {}
130
+ for name, _ in order:
131
+ pointers[name] = sectors
132
+ sectors += len(tables[name]) // SECTOR
133
+ if sectors != ifo_sectors:
134
+ raise VtsError("the title table or the attributes outgrew a sector") # (99 titles fit)
135
+ mat = bytearray(head)
136
+ mat[:12] = b"DVDVIDEO-VMG"
137
+ struct.pack_into(">I", mat, 0x0C, vmg_sectors - 1)
138
+ struct.pack_into(">I", mat, 0x1C, sectors - 1)
139
+ struct.pack_into(">H", mat, 0x20, 0x0011)
140
+ struct.pack_into(">HHB", mat, 0x26, 1, 1, 1) # one volume, this is it, side 1
141
+ struct.pack_into(">H", mat, 0x3E, len(titlesets))
142
+ mat[0x40:0x60] = provider
143
+ struct.pack_into(">I", mat, 0x80, 0x400 + len(fp) - 1)
144
+ struct.pack_into(">I", mat, 0x84, 0x400)
145
+ struct.pack_into(">I", mat, 0xC0, sectors if menus else 0)
146
+ struct.pack_into(">II", mat, 0xC4, pointers["tt_srpt"], pointers.get("vmgm_pgci_ut", 0))
147
+ struct.pack_into(">II", mat, 0xCC, 0, pointers["vts_atrt"])
148
+ struct.pack_into(">II", mat, 0xD8, pointers.get("vmgm_c_adt", 0), pointers.get("vmgm_admap", 0))
149
+ mat[0x100:0x200] = menu_attributes
150
+ ifo = bytes(mat) + b"".join(tables[name] for name, _ in order)
151
+
152
+ os.makedirs(folder, exist_ok=True)
153
+ for name in ("VIDEO_TS.IFO", "VIDEO_TS.BUP"):
154
+ with open(os.path.join(folder, name), "wb") as f:
155
+ f.write(ifo)
156
+ if menu_sectors:
157
+ with open(os.path.join(folder, "VIDEO_TS.VOB"), "wb") as f:
158
+ f.write(b"".join(menu_data))
159
+ return start