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/spu.py ADDED
@@ -0,0 +1,262 @@
1
+ """Subpicture units: the button overlay of a menu, or a subtitle, as a DVD
2
+ subpicture.
3
+
4
+ A subpicture unit (https://dvdnav.mplayerhq.hu/dvdinfo/spu.html) is a
5
+ header, the run-length coded pixels of the top and bottom fields, and
6
+ display control sequences: the first shows the picture (its colours from
7
+ the program chain's palette, a contrast each), a second may take it off
8
+ the screen after a delay. Pixels have four codes: 0 background, 1 pattern,
9
+ 2 and 3 emphasis. A menu's overlay here is drawn with code 1 where a button
10
+ lights up; the unit shows nothing by itself (every code transparent) and
11
+ the player colours a button's pixels from the PCI's colour table when it is
12
+ selected or activated (vtskit.menu). A subtitle (vtskit.subtitles) is a
13
+ picture placed at a point of the screen, shown for a while. The unit is
14
+ carried in one or more packs of private stream 1, sub-stream 0x20 + n.
15
+ """
16
+
17
+ import struct
18
+
19
+ from . import ps
20
+ from .errors import VtsError
21
+
22
+ MAX_UNIT = 53220 # the subpicture buffer
23
+
24
+
25
+ class Overlay:
26
+ """A picture of pixel codes (bytes, one a pixel, row by row)."""
27
+
28
+ def __init__(self, width, height, pixels):
29
+ if len(pixels) != width * height:
30
+ raise VtsError(f"{len(pixels)} pixels for {width}x{height}")
31
+ if any(p > 3 for p in pixels):
32
+ raise VtsError("a subpicture pixel is 0 to 3")
33
+ self.width, self.height, self.pixels = width, height, bytes(pixels)
34
+
35
+ @classmethod
36
+ def from_rects(cls, width, height, rects, code=1):
37
+ """Code `code` inside each (x0, y0, x1, y1) (x1, y1 exclusive), 0 elsewhere."""
38
+ rows = []
39
+ for y in range(height):
40
+ row = bytearray(width)
41
+ for x0, y0, x1, y1 in rects:
42
+ if y0 <= y < y1:
43
+ row[x0:x1] = bytes([code]) * (x1 - x0)
44
+ rows.append(bytes(row))
45
+ return cls(width, height, b"".join(rows))
46
+
47
+ def bounds(self):
48
+ """(x0, y0, x1, y1) around the non-zero pixels, inclusive; None if blank."""
49
+ xs, ys = [], []
50
+ for y in range(self.height):
51
+ row = self.pixels[y * self.width:(y + 1) * self.width]
52
+ if any(row):
53
+ ys.append(y)
54
+ xs.append(next(x for x, p in enumerate(row) if p))
55
+ xs.append(self.width - 1 - next(x for x, p in enumerate(reversed(row)) if p))
56
+ if not ys:
57
+ return None
58
+ return min(xs), ys[0], max(xs), ys[-1]
59
+
60
+
61
+ class _Bits:
62
+ def __init__(self):
63
+ self.out = bytearray()
64
+ self.acc, self.n = 0, 0
65
+
66
+ def put(self, value, bits):
67
+ self.acc = (self.acc << bits) | value
68
+ self.n += bits
69
+ while self.n >= 8:
70
+ self.n -= 8
71
+ self.out.append((self.acc >> self.n) & 0xFF)
72
+ self.acc &= (1 << self.n) - 1
73
+
74
+ def align(self):
75
+ if self.n:
76
+ self.put(0, 8 - self.n)
77
+
78
+
79
+ def _encode_line(bits, line):
80
+ """One line of pixel codes as runs: 4, 8, 12 or 16 bits a run, the
81
+ shortest that holds the count; a run to the end of the line that needs
82
+ 16 bits (over 63) is count 0 in 16 bits (as spumux chooses)."""
83
+ x = 0
84
+ n = len(line)
85
+ while x < n:
86
+ code = line[x]
87
+ run = 1
88
+ while x + run < n and line[x + run] == code:
89
+ run += 1
90
+ if x + run == n and run > 63:
91
+ bits.put(code, 16) # count 0, to the end of the line
92
+ elif run <= 3:
93
+ bits.put(run << 2 | code, 4)
94
+ elif run <= 15:
95
+ bits.put(run << 2 | code, 8)
96
+ elif run <= 63:
97
+ bits.put(run << 2 | code, 12)
98
+ elif run <= 255:
99
+ bits.put(run << 2 | code, 16)
100
+ else:
101
+ bits.put(255 << 2 | code, 16)
102
+ run = 255
103
+ x += run
104
+ bits.align()
105
+
106
+
107
+ def encode(overlay, forced=True, colours=(0, 0, 0, 0), contrasts=(0, 0, 0, 0), at=None,
108
+ stop=None):
109
+ """The subpicture unit of an overlay: its pixels inside their bounds
110
+ (or the whole picture with its top left corner `at` (x, y)), shown
111
+ from its PTS on (forced: shown even with subtitles off), the four
112
+ codes drawn with these palette entries and contrasts (0 transparent to
113
+ 15 opaque; all transparent by default, for a button overlay), taken off
114
+ the screen `stop` ticks (90 kHz) after the PTS if given."""
115
+ for name, values in (("colours", colours), ("contrasts", contrasts)):
116
+ if len(values) != 4 or not all(0 <= v <= 15 for v in values):
117
+ raise VtsError(f"the four {name} are 0 to 15")
118
+ if at is None:
119
+ box = overlay.bounds()
120
+ if box is None:
121
+ raise VtsError("the overlay is blank")
122
+ lx0, ly0, lx1, ly1 = box # in the picture
123
+ x0, y0, x1, y1 = box # on the screen
124
+ else:
125
+ lx0, ly0, lx1, ly1 = 0, 0, overlay.width - 1, overlay.height - 1
126
+ x0, y0 = at
127
+ x1, y1 = x0 + overlay.width - 1, y0 + overlay.height - 1
128
+ if not (0 <= x0 and x1 < 1 << 10 and 0 <= y0 and y1 < 1 << 10):
129
+ raise VtsError(f"a picture at {at} of {overlay.width}x{overlay.height} is off the screen")
130
+ fields = []
131
+ for first in (ly0, ly0 + 1): # the top field: lines y0, y0 + 2, ...
132
+ bits = _Bits()
133
+ for y in range(first, ly1 + 1, 2):
134
+ _encode_line(bits, overlay.pixels[y * overlay.width + lx0:y * overlay.width + lx1 + 1])
135
+ fields.append(bytes(bits.out))
136
+ top = 4
137
+ bottom = top + len(fields[0])
138
+ dcsqt = bottom + len(fields[1])
139
+ if dcsqt + 24 > MAX_UNIT:
140
+ raise VtsError(f"the subpicture unit is over {dcsqt} bytes (at most {MAX_UNIT})")
141
+ commands = bytes([0x00 if forced else 0x01])
142
+ commands += bytes([0x03, colours[3] << 4 | colours[2], colours[1] << 4 | colours[0]])
143
+ commands += bytes([0x04, contrasts[3] << 4 | contrasts[2], contrasts[1] << 4 | contrasts[0]])
144
+ commands += bytes([0x05, x0 >> 4, (x0 & 0xF) << 4 | x1 >> 8, x1 & 0xFF,
145
+ y0 >> 4, (y0 & 0xF) << 4 | y1 >> 8, y1 & 0xFF])
146
+ commands += struct.pack(">BHH", 0x06, top, bottom)
147
+ commands += b"\xff"
148
+ if stop is None:
149
+ dcsq = struct.pack(">HH", 0, dcsqt) + commands # one sequence, pointing at itself
150
+ else:
151
+ delay = int(round(stop / 1024))
152
+ if not 0 < delay <= 0xFFFF:
153
+ raise VtsError(f"a picture shown for {stop} ticks: up to {0xFFFF * 1024} and over 0")
154
+ second = dcsqt + 4 + len(commands)
155
+ dcsq = struct.pack(">HH", 0, second) + commands + struct.pack(">HH", delay, second) + b"\x02\xff"
156
+ size = dcsqt + len(dcsq)
157
+ if size > MAX_UNIT:
158
+ raise VtsError(f"the subpicture unit is {size} bytes (at most {MAX_UNIT})")
159
+ return struct.pack(">HH", size, dcsqt) + fields[0] + fields[1] + dcsq
160
+
161
+
162
+ def decode(unit):
163
+ """The pixel codes of a unit as (x0, y0, {(x, y): code}) and its
164
+ commands, for tests."""
165
+ size, dcsqt = struct.unpack_from(">HH", unit, 0)
166
+ if size != len(unit):
167
+ raise VtsError("the unit's size does not match")
168
+ cmds = unit[dcsqt + 4:]
169
+ area = top = bottom = None
170
+ i = 0
171
+ while i < len(cmds) and cmds[i] != 0xFF:
172
+ c = cmds[i]
173
+ if c in (0x00, 0x01, 0x02):
174
+ i += 1
175
+ elif c in (0x03, 0x04):
176
+ i += 3
177
+ elif c == 0x05:
178
+ b = cmds[i + 1:i + 7]
179
+ area = (b[0] << 4 | b[1] >> 4, (b[1] & 0xF) << 8 | b[2], b[3] << 4 | b[4] >> 4,
180
+ (b[4] & 0xF) << 8 | b[5])
181
+ i += 7
182
+ elif c == 0x06:
183
+ top, bottom = struct.unpack_from(">HH", cmds, i + 1)
184
+ i += 5
185
+ else:
186
+ raise VtsError(f"subpicture command {c:#x} is not decoded here")
187
+ x0, x1, y0, y1 = area
188
+ width = x1 - x0 + 1
189
+ pixels = {}
190
+
191
+ def read_field(offset, first):
192
+ pos = offset * 8
193
+ for y in range(first, y1 + 1, 2):
194
+ x = 0
195
+ while x < width:
196
+ v, bits = 0, 0
197
+ while True:
198
+ v = v << 4 | (unit[pos >> 3] >> (4 - (pos & 7))) & 0xF
199
+ pos += 4
200
+ bits += 4
201
+ if v >= (1 << (bits // 2)) or bits == 16: # 4 bits: n >= 1; 8: n >= 4; 12: n >= 16
202
+ break
203
+ n, code = v >> 2, v & 3
204
+ if n == 0:
205
+ n = width - x
206
+ for k in range(n):
207
+ pixels[(x0 + x + k, y)] = code
208
+ x += n
209
+ pos = (pos + 7) & ~7
210
+ read_field(top, y0)
211
+ read_field(bottom, y0 + 1)
212
+ return area, pixels, cmds
213
+
214
+
215
+ def insert(data, unit, pts, sub=0x20):
216
+ """The stream (bytes or bytearray) with the unit added right after its
217
+ first navigation pack, in packs of private stream 1, sub-stream `sub`,
218
+ stamped `pts`. Returns a new bytearray."""
219
+ packs = list(ps.iter_packs(data))
220
+ if not packs or not packs[0].nav:
221
+ raise VtsError("the stream does not start with a navigation pack")
222
+ nav = bytes(data[:ps.SECTOR])
223
+ scr = packs[0].scr
224
+ out = bytearray(nav)
225
+ remaining = unit
226
+ first = True
227
+ while remaining:
228
+ room = ps.SECTOR - 14 - 6 - (3 + 5 if first else 3) - 1 # pack, PES, header, sub id
229
+ chunk, remaining = remaining[:room], remaining[room:]
230
+ header = bytes([0x81, 0x80, 0x05]) + _pts(pts) if first else bytes([0x81, 0x00, 0x00])
231
+ pes = b"\x00\x00\x01\xbd" + struct.pack(">H", len(header) + 1 + len(chunk)) + header \
232
+ + bytes([sub]) + chunk
233
+ scr += 1
234
+ pack = _pack_header(scr, nav) + pes
235
+ pad = ps.SECTOR - len(pack)
236
+ if pad:
237
+ if pad < 6:
238
+ raise VtsError("no room for the padding packet") # (cannot happen: room leaves 1)
239
+ pack += b"\x00\x00\x01\xbe" + struct.pack(">H", pad - 6) + b"\xff" * (pad - 6)
240
+ out += pack
241
+ first = False
242
+ out += data[ps.SECTOR:]
243
+ return out
244
+
245
+
246
+ def _pts(t):
247
+ return bytes([0x21 | (t >> 29) & 0x0E, (t >> 22) & 0xFF, 0x01 | (t >> 14) & 0xFE,
248
+ (t >> 7) & 0xFF, 0x01 | (t << 1) & 0xFE])
249
+
250
+
251
+ def _pack_header(scr, like):
252
+ """A pack header with this SCR (90 kHz; extension 0) and the mux rate of
253
+ the pack `like`."""
254
+ b = bytearray(like[:14])
255
+ b[4] = 0x44 | ((scr >> 30) & 7) << 3 | (scr >> 28) & 3
256
+ b[5] = (scr >> 20) & 0xFF
257
+ b[6] = 0x04 | ((scr >> 15) & 0x1F) << 3 | (scr >> 13) & 3
258
+ b[7] = (scr >> 5) & 0xFF
259
+ b[8] = 0x04 | ((scr & 0x1F) << 3)
260
+ b[9] = 0x01
261
+ b[13] = 0xF8 # no stuffing
262
+ return bytes(b)
vtskit/subtitles.py ADDED
@@ -0,0 +1,126 @@
1
+ """Subtitles: pictures shown over a title's video for a while, added to its
2
+ program stream as subpicture packs (what spumux does).
3
+
4
+ A cue is a picture of pixel codes (vtskit.spu.Overlay) placed at a point
5
+ of the screen, shown from `start` to `end` (seconds from the title's first
6
+ picture), each code drawn with an entry of the title's palette
7
+ (vtskit.titleset.Title.palette, 16 colours) and a contrast. `mux` turns
8
+ the cues of each subpicture stream into units and places their packs in
9
+ the stream a little before they are shown: the unit's packs go after the
10
+ last pack whose clock (SCR) is `lead` seconds before the cue's time, so the
11
+ player has the picture when it is due (spumux delivers about 0.15 s
12
+ early). The other packs stay as they are; the clock of an added pack is
13
+ the previous pack's plus one. Each stream's cues must not overlap.
14
+
15
+ The DVD-Video program stream from ffmpeg (-f dvd) carries no subpicture
16
+ streams; `mux` adds them before vtskit.titleset.write fills the
17
+ navigation packs, which then point at the units (DSI SP_SYNCA).
18
+ """
19
+
20
+ import bisect
21
+ import struct
22
+ from dataclasses import dataclass
23
+
24
+ from . import nav, ps, spu
25
+ from .errors import VtsError
26
+
27
+ LEAD = 0.15
28
+
29
+
30
+ @dataclass
31
+ class Cue:
32
+ start: float
33
+ end: float
34
+ picture: spu.Overlay
35
+ x: int = 0
36
+ y: int = 0
37
+ colours: tuple = (0, 1, 2, 3) # the palette entry drawing each pixel code
38
+ contrasts: tuple = (0, 15, 15, 15) # and its contrast, 0 (not drawn) to 15
39
+ forced: bool = False # shown even with subtitles off
40
+
41
+
42
+ def unit(cue):
43
+ """The subpicture unit of a cue."""
44
+ if not cue.end > cue.start >= 0:
45
+ raise VtsError(f"a cue from {cue.start} to {cue.end} s")
46
+ return spu.encode(cue.picture, cue.forced, cue.colours, cue.contrasts, (cue.x, cue.y),
47
+ stop=(cue.end - cue.start) * 90000)
48
+
49
+
50
+ def _packets(unit_, pts, sub, scr, like):
51
+ """The packs carrying a unit, the first stamped `pts`, clocks from
52
+ `scr` + 1 on."""
53
+ out = []
54
+ remaining = unit_
55
+ first = True
56
+ while remaining:
57
+ room = ps.SECTOR - 14 - 6 - (3 + 5 if first else 3) - 1
58
+ chunk, remaining = remaining[:room], remaining[room:]
59
+ header = bytes([0x81, 0x80, 0x05]) + spu._pts(pts) if first else bytes([0x81, 0x00, 0x00])
60
+ pes = b"\x00\x00\x01\xbd" + struct.pack(">H", len(header) + 1 + len(chunk)) + header \
61
+ + bytes([sub]) + chunk
62
+ scr += 1
63
+ pack = spu._pack_header(scr, like) + pes
64
+ pad = ps.SECTOR - len(pack)
65
+ if pad:
66
+ pack += b"\x00\x00\x01\xbe" + struct.pack(">H", pad - 6) + b"\xff" * (pad - 6)
67
+ out.append(pack)
68
+ first = False
69
+ return out, scr
70
+
71
+
72
+ def mux(data, streams, lead=LEAD, out=None):
73
+ """The program stream `data` (bytes, or a ps.Mapped file) with the cues
74
+ of each subpicture stream added: `streams` is {stream number 0-31:
75
+ [Cue]}. Returns a bytearray, or writes into the open file `out` and
76
+ returns None (a title of gigabytes passes through in blocks)."""
77
+ packs = ps.read_packs(data)
78
+ if not packs or not packs[0].nav:
79
+ raise VtsError("the stream does not start with a navigation pack")
80
+ stream = ps.scan(data)
81
+ s0 = nav.vobus(stream)[0].s_ptm
82
+ scrs = [p.scr for p in packs]
83
+ if any(b <= a for a, b in zip(scrs, scrs[1:])):
84
+ raise VtsError("the packs' clocks are not in order")
85
+ events = [] # (pack to go before, pts, stream, unit)
86
+ for number, cues in streams.items():
87
+ if not 0 <= number <= 31:
88
+ raise VtsError(f"subpicture stream {number}: 0 to 31")
89
+ if 0x20 + number in stream.subpictures:
90
+ raise VtsError(f"the stream already has subpicture stream {number}")
91
+ last = None
92
+ for cue in sorted(cues, key=lambda c: c.start):
93
+ if last is not None and cue.start < last:
94
+ raise VtsError(f"subpicture stream {number}: a cue at {cue.start} s overlaps the one before")
95
+ last = cue.end
96
+ pts = s0 + int(round(cue.start * 90000))
97
+ k = bisect.bisect_right(scrs, pts - int(lead * 90000)) # after the last pack early enough
98
+ events.append((max(k, 1), pts, 0x20 + number, unit(cue)))
99
+ events.sort(key=lambda e: (e[0], e[1], e[2]))
100
+ result = bytearray() if out is None else None
101
+ e = 0
102
+ last_scr = None
103
+ first = bytes(data[:14])
104
+ k = 0
105
+ while k < len(packs):
106
+ while e < len(events) and events[e][0] == k:
107
+ _, pts, sub, u = events[e]
108
+ added, last_scr = _packets(u, pts, sub, last_scr, first)
109
+ if last_scr >= packs[k].scr:
110
+ raise VtsError(f"no room in the clock before pack {k} for the subpicture packs")
111
+ for p in added:
112
+ _emit(result, out, p)
113
+ e += 1
114
+ # the stretch of packs up to the next insertion goes through whole
115
+ nxt = events[e][0] if e < len(events) else len(packs)
116
+ _emit(result, out, data[k * ps.SECTOR:nxt * ps.SECTOR])
117
+ last_scr = packs[nxt - 1].scr
118
+ k = nxt
119
+ return result
120
+
121
+
122
+ def _emit(result, out, data):
123
+ if out is None:
124
+ result += data
125
+ else:
126
+ out.write(data)