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/ps.py ADDED
@@ -0,0 +1,484 @@
1
+ """Read a DVD program stream: its packs, VOBUs, pictures and sound.
2
+
3
+ The input is what oncemore's encoder makes (ffmpeg `-f dvd`): 2048-byte
4
+ packs, each VOBU starting with a navigation pack whose PCI and DSI are
5
+ still empty. This module only reads; `vtskit.nav` fills the navigation
6
+ packs from what is read here.
7
+
8
+ `scan` reads a stream in one pass, keeping only what the navigation packs
9
+ need, so a whole title (gigabytes, given as an mmap) is never copied.
10
+
11
+ The MPEG-2 headers are those of ISO/IEC 13818-1 (pack, system and PES
12
+ headers) and 13818-2 (sequence, GOP and picture headers); the DVD's own
13
+ use of private streams is described at https://dvdnav.mplayerhq.hu/dvdinfo/.
14
+ """
15
+
16
+ import bisect
17
+ import mmap
18
+
19
+ from .errors import VtsError
20
+
21
+ SECTOR = 2048
22
+
23
+ # private stream 1 sub-streams
24
+ SUB_SPU = range(0x20, 0x40)
25
+ SUB_AC3 = range(0x80, 0x88)
26
+ SUB_DTS = range(0x88, 0x90)
27
+ SUB_LPCM = range(0xA0, 0xA8)
28
+
29
+ # the start codes that end a picture's data
30
+ _PICTURE, _SEQ, _EXT, _GOP, _SEQ_END = 0x00, 0xB3, 0xB5, 0xB8, 0xB7
31
+
32
+ # frame_rate_code: 90 kHz ticks per frame
33
+ _FRAME = {1: 3753.75, 2: 3750, 3: 3600, 4: 3003, 5: 3000, 6: 1800, 7: 1501.5, 8: 1500}
34
+
35
+
36
+ def _pts(b, o):
37
+ return ((b[o] >> 1) & 7) << 30 | b[o + 1] << 22 | (b[o + 2] >> 1) << 15 | \
38
+ b[o + 3] << 7 | b[o + 4] >> 1
39
+
40
+
41
+ class Pes:
42
+ """One PES packet in a pack."""
43
+ __slots__ = ("sid", "sub", "pts", "start", "end", "frames")
44
+
45
+ def __init__(self, sid, sub, pts, start, end, frames):
46
+ self.sid, self.sub, self.pts = sid, sub, pts
47
+ self.start, self.end = start, end # the payload, as byte offsets in the stream
48
+ self.frames = frames # (audio) frames that begin in this packet
49
+
50
+
51
+ class Pack:
52
+ __slots__ = ("sector", "scr", "nav", "pes")
53
+
54
+ def __init__(self, sector, scr, nav, pes):
55
+ self.sector, self.scr, self.nav, self.pes = sector, scr, nav, pes
56
+
57
+
58
+ def iter_packs(data):
59
+ """The packs of a stream (bytes, a bytearray or an mmap), one by one."""
60
+ if len(data) % SECTOR:
61
+ raise VtsError(f"the stream is not whole 2048-byte packs ({len(data)} bytes)")
62
+ for i in range(len(data) // SECTOR):
63
+ base = i * SECTOR
64
+ p = data[base:base + SECTOR]
65
+ if p[:4] != b"\x00\x00\x01\xba" or (p[4] >> 6) != 1:
66
+ raise VtsError(f"sector {i} is not an MPEG-2 pack")
67
+ scr = ((p[4] >> 3) & 7) << 30 | (p[4] & 3) << 28 | p[5] << 20 | \
68
+ (p[6] >> 3) << 15 | (p[6] & 3) << 13 | p[7] << 5 | p[8] >> 3
69
+ o = 14 + (p[13] & 7)
70
+ pes, nav = [], False
71
+ while o + 6 <= SECTOR and p[o:o + 3] == b"\x00\x00\x01":
72
+ sid = p[o + 3]
73
+ n = int.from_bytes(p[o + 4:o + 6], "big")
74
+ end = o + 6 + n
75
+ if end > SECTOR:
76
+ raise VtsError(f"sector {i}: a packet runs past the pack")
77
+ if sid == 0xBF:
78
+ nav = True
79
+ elif sid == 0xE0 or sid == 0xBD or 0xC0 <= sid <= 0xDF:
80
+ pts = _pts(p, o + 9) if p[o + 7] & 0x80 else None
81
+ start = o + 9 + p[o + 8]
82
+ sub, frames = None, None
83
+ if sid == 0xBD:
84
+ sub = p[start]
85
+ if sub in SUB_AC3 or sub in SUB_DTS or sub in SUB_LPCM:
86
+ frames = p[start + 1]
87
+ start += 1
88
+ pes.append(Pes(sid, sub, pts, base + start, base + end, frames))
89
+ o = end
90
+ yield Pack(i, scr, nav, pes)
91
+
92
+
93
+ def read_packs(data):
94
+ """Every pack of the stream, as a list (for short streams)."""
95
+ return list(iter_packs(data))
96
+
97
+
98
+ def _only_padding(p):
99
+ """Whether a pack holds nothing but padding packets."""
100
+ o = 14 + (p[13] & 7)
101
+ while o + 6 <= SECTOR and p[o:o + 3] == b"\x00\x00\x01":
102
+ if p[o + 3] != 0xBE:
103
+ return False
104
+ o += 6 + int.from_bytes(p[o + 4:o + 6], "big")
105
+ return True
106
+
107
+
108
+ def has_padding(data):
109
+ """Whether the stream has packs of padding alone."""
110
+ return any(_only_padding(data[i * SECTOR:(i + 1) * SECTOR]) for i in range(len(data) // SECTOR))
111
+
112
+
113
+ def drop_padding(data):
114
+ """The stream without its packs of padding alone (spumux leaves some;
115
+ a VOB has none). Every other pack keeps its order."""
116
+ out = bytearray()
117
+ for i in range(len(data) // SECTOR):
118
+ p = data[i * SECTOR:(i + 1) * SECTOR]
119
+ if not _only_padding(p):
120
+ out += p
121
+ return out
122
+
123
+
124
+ class Mapped:
125
+ """A stream file mapped read-only, with what vtskit writes into it (the
126
+ navigation packs) kept aside: a slice read sees the writes, and `write`
127
+ copies the stream with them into a file, so a title of gigabytes is
128
+ never held in memory. Slices are assigned the bytes they are long, as
129
+ `nav.fill` assigns them."""
130
+
131
+ def __init__(self, path):
132
+ self.file = open(path, "rb")
133
+ try:
134
+ self.map = mmap.mmap(self.file.fileno(), 0, access=mmap.ACCESS_READ)
135
+ except (ValueError, OSError) as e: # an empty file cannot be mapped
136
+ self.file.close()
137
+ raise VtsError(f"{path}: {e}")
138
+ self.patches = {} # offset -> bytes
139
+ self.offsets = [] # the offsets, sorted
140
+
141
+ def __len__(self):
142
+ return len(self.map)
143
+
144
+ def close(self):
145
+ self.map.close()
146
+ self.file.close()
147
+
148
+ def __enter__(self):
149
+ return self
150
+
151
+ def __exit__(self, *exc):
152
+ self.close()
153
+
154
+ def _patched(self, start, stop, out):
155
+ """Apply the patches overlapping [start, stop) to `out`, the bytes
156
+ of that range."""
157
+ k = bisect.bisect_right(self.offsets, start) - 1
158
+ if k >= 0 and self.offsets[k] + len(self.patches[self.offsets[k]]) <= start:
159
+ k += 1
160
+ k = max(k, 0)
161
+ while k < len(self.offsets) and self.offsets[k] < stop:
162
+ o = self.offsets[k]
163
+ b = self.patches[o]
164
+ a0, a1 = max(o, start), min(o + len(b), stop)
165
+ if a0 < a1:
166
+ out[a0 - start:a1 - start] = b[a0 - o:a1 - o]
167
+ k += 1
168
+ return out
169
+
170
+ def __getitem__(self, key):
171
+ if isinstance(key, slice):
172
+ start, stop, step = key.indices(len(self.map))
173
+ if step != 1:
174
+ raise VtsError("a stream is read in whole slices")
175
+ data = self.map[start:stop]
176
+ if not self.offsets:
177
+ return data
178
+ return bytes(self._patched(start, stop, bytearray(data)))
179
+ return self[key:key + 1][0]
180
+
181
+ def __setitem__(self, key, value):
182
+ if not isinstance(key, slice):
183
+ raise VtsError("a stream is written in whole slices")
184
+ start, stop, step = key.indices(len(self.map))
185
+ value = bytes(value)
186
+ if step != 1 or len(value) != stop - start:
187
+ raise VtsError("a slice is given the bytes it is long")
188
+ if start not in self.patches:
189
+ bisect.insort(self.offsets, start)
190
+ self.patches[start] = value
191
+
192
+ def write(self, f, start=0, stop=None, block=1 << 23):
193
+ """Copy [start, stop) of the stream, as patched, into the open file `f`."""
194
+ stop = len(self.map) if stop is None else stop
195
+ for a in range(start, stop, block):
196
+ b = min(a + block, stop)
197
+ f.write(self[a:b])
198
+
199
+
200
+ class Picture:
201
+ __slots__ = ("type", "start", "end", "pts", "fields", "order")
202
+
203
+ def __init__(self, type_, start, pts, order):
204
+ self.type, self.start, self.pts = type_, start, pts
205
+ self.end = None # sector of the picture's last byte
206
+ self.fields = 2
207
+ self.order = order # temporal_reference: its place in its group's display order
208
+
209
+
210
+ class _Video:
211
+ """Find the pictures in the video packets as they come, start codes
212
+ split between packets included."""
213
+
214
+ def __init__(self):
215
+ self.pictures = []
216
+ self.groups = [] # the pictures of each group of pictures
217
+ self.frame = None
218
+ self.sequence = None # (width, height, aspect_ratio_information, frame_rate_code)
219
+ self.tail = b"" # unread bytes of the packets before
220
+ self.tail_sectors = [] # the sector of each
221
+ self.before = None # the sector of the byte before the tail
222
+ self.pending_pts = None # the PTS of the packet the tail's first start code is in
223
+ self.last_sector = None
224
+
225
+ def feed(self, chunk, sector, pts):
226
+ buf = self.tail + chunk
227
+ n_tail = len(self.tail)
228
+ secs = self.tail_sectors
229
+
230
+ def sector_of(pos):
231
+ if pos < 0:
232
+ return self.before
233
+ return secs[pos] if pos < n_tail else sector
234
+
235
+ used = False
236
+ k = 0
237
+ while True:
238
+ k = buf.find(b"\x00\x00\x01", k)
239
+ if k < 0:
240
+ resume = max(len(buf) - 2, 0) # a start code may begin in the last bytes
241
+ break
242
+ if k + 9 > len(buf): # read it when the rest has come
243
+ resume = k
244
+ break
245
+ code = buf[k + 3]
246
+ if code in (_PICTURE, _SEQ, _GOP, _SEQ_END) and self.pictures and \
247
+ self.pictures[-1].end is None:
248
+ self.pictures[-1].end = sector_of(k - 1)
249
+ if code == _SEQ and self.frame is None:
250
+ self.frame = _FRAME.get(buf[k + 7] & 0x0F)
251
+ self.sequence = (buf[k + 4] << 4 | buf[k + 5] >> 4,
252
+ (buf[k + 5] & 0x0F) << 8 | buf[k + 6],
253
+ buf[k + 7] >> 4, buf[k + 7] & 0x0F)
254
+ elif code == _GOP:
255
+ self.groups.append([])
256
+ elif code == _PICTURE:
257
+ t = "?IPB"[(buf[k + 5] >> 3) & 7]
258
+ # a packet's PTS belongs to the first picture that starts in it
259
+ if k < n_tail:
260
+ own, self.pending_pts = self.pending_pts, None
261
+ elif pts is not None and not used:
262
+ own, used = pts, True
263
+ else:
264
+ own = None
265
+ picture = Picture(t, sector_of(k), own, buf[k + 4] << 2 | buf[k + 5] >> 6)
266
+ if not self.groups or t == "I" and self.groups[-1]:
267
+ self.groups.append([]) # (a group without its header)
268
+ self.groups[-1].append(picture)
269
+ self.pictures.append(picture)
270
+ elif code == _EXT and self.pictures and (buf[k + 4] >> 4) == 8:
271
+ self.pictures[-1].fields = 3 if buf[k + 7] & 0x02 else 2
272
+ k += 3
273
+ self.before = sector_of(resume - 1)
274
+ if resume >= n_tail:
275
+ self.pending_pts = pts if not used else None
276
+ self.tail = buf[resume:]
277
+ self.tail_sectors = [sector_of(j) for j in range(resume, len(buf))]
278
+ self.last_sector = sector
279
+
280
+ def close(self):
281
+ if self.pictures and self.pictures[-1].end is None:
282
+ self.pictures[-1].end = self.last_sector
283
+ if self.pictures and self.frame is None:
284
+ raise VtsError("the video has no sequence header")
285
+ self._time_pictures()
286
+
287
+ def _time_pictures(self):
288
+ """A picture whose packet carries no PTS (ffmpeg's muxer may give one
289
+ to the first picture of a packet only) is shown at its place in its
290
+ group's display order: temporal_reference counts the pictures of
291
+ the group from the first shown, each lasting its fields."""
292
+ for group in self.groups:
293
+ shown = sorted(group, key=lambda p: p.order)
294
+ known = next((p for p in shown if p.pts is not None), None)
295
+ if known is None or all(p.pts is not None for p in shown):
296
+ continue
297
+ at, times = 0, {}
298
+ for p in shown:
299
+ times[id(p)] = at
300
+ at += p.fields * self.frame / 2
301
+ base = known.pts - times[id(known)]
302
+ for p in shown:
303
+ if p.pts is None:
304
+ p.pts = int(round(base + times[id(p)]))
305
+
306
+
307
+ class _Subpictures:
308
+ """Collect the subpicture units of each stream as their packets come."""
309
+
310
+ def __init__(self):
311
+ self.units = {} # sub-stream -> [(first sector, shown from, shown until)]
312
+ self.current = {} # sub-stream -> [first sector, pts, bytes]
313
+
314
+ def feed(self, sub, chunk, sector, pts):
315
+ unit = self.current.get(sub)
316
+ if unit is None or pts is not None:
317
+ if pts is None:
318
+ raise VtsError(f"sector {sector}: a subpicture unit without a PTS")
319
+ unit = self.current[sub] = [sector, pts, bytearray()]
320
+ unit[2] += chunk
321
+ size = int.from_bytes(unit[2][:2], "big") if len(unit[2]) >= 2 else 0
322
+ if size and len(unit[2]) >= size:
323
+ self.units.setdefault(sub, []).append(_spu_times(*unit))
324
+ del self.current[sub]
325
+
326
+
327
+ def _spu_times(sector, pts, spu):
328
+ """When a subpicture unit is on screen: its PTS plus the delays (in
329
+ units of 1024 ticks) of its start and stop display commands
330
+ (dvdinfo spu.html)."""
331
+ o = int.from_bytes(spu[2:4], "big")
332
+ start = stop = None
333
+ seen = set()
334
+ while o + 4 <= len(spu) and o not in seen:
335
+ seen.add(o)
336
+ delay = int.from_bytes(spu[o:o + 2], "big") * 1024
337
+ nxt = int.from_bytes(spu[o + 2:o + 4], "big")
338
+ k = o + 4
339
+ while k < len(spu):
340
+ cmd = spu[k]
341
+ if cmd == 0xFF:
342
+ break
343
+ if cmd in (0x00, 0x01) and start is None:
344
+ start = pts + delay
345
+ elif cmd == 0x02 and stop is None:
346
+ stop = pts + delay
347
+ if cmd == 0x07: # CHG_COLCON: its size word counts itself
348
+ k += 1 + int.from_bytes(spu[k + 1:k + 3], "big")
349
+ else:
350
+ k += {0x03: 3, 0x04: 3, 0x05: 7, 0x06: 5}.get(cmd, 1)
351
+ if nxt == o:
352
+ break
353
+ o = nxt
354
+ return sector, start if start is not None else pts, stop
355
+
356
+
357
+ class Stream:
358
+ """What a navigation pack and a titleset need to know of a stream."""
359
+
360
+ def __init__(self):
361
+ self.sectors = 0
362
+ self.navs = [] # (sector, SCR) of each navigation pack
363
+ self.pictures = []
364
+ self.frame = None # 90 kHz ticks per frame
365
+ self.sequence = None # (width, height, aspect_ratio_information, frame_rate_code)
366
+ self.audio = {} # (stream id, sub-stream) -> [(sector, pts, frames)]
367
+ self.mpeg_audio = {} # stream id -> [bytes so far, first 4 bytes]
368
+ self.subpictures = {} # sub-stream -> [(first sector, shown from, until)]
369
+
370
+ @property
371
+ def video_end(self):
372
+ """When the last picture has been shown."""
373
+ ends = [p.pts + p.fields * self.frame / 2 for p in self.pictures if p.pts is not None]
374
+ return int(max(ends)) if ends else None
375
+
376
+ def audio_end(self, key):
377
+ """When a sound stream ends (None when unknown)."""
378
+ if key[0] != 0xBD:
379
+ return mpeg_audio_end(self.audio[key], self.mpeg_audio.get(key[0]))
380
+ return audio_end(self.audio[key], key)
381
+
382
+ @property
383
+ def end(self):
384
+ """When the last of the video and the sound ends."""
385
+ ends = [self.video_end] + [self.audio_end(k) for k in self.audio]
386
+ return max(e for e in ends if e is not None)
387
+
388
+
389
+ def scan(data):
390
+ """Read a stream (bytes, a bytearray or an mmap) in one pass."""
391
+ out = Stream()
392
+ video, spu = _Video(), _Subpictures()
393
+ for pk in iter_packs(data):
394
+ out.sectors += 1
395
+ if pk.nav:
396
+ out.navs.append((pk.sector, pk.scr))
397
+ for e in pk.pes:
398
+ if e.sid == 0xE0:
399
+ video.feed(bytes(data[e.start:e.end]), pk.sector, e.pts)
400
+ elif e.sid == 0xBD and e.sub in SUB_SPU:
401
+ spu.feed(e.sub, data[e.start:e.end], pk.sector, e.pts)
402
+ elif e.sid == 0xBD or 0xC0 <= e.sid <= 0xDF:
403
+ key = (0xBD, e.sub) if e.sid == 0xBD else (e.sid, None)
404
+ out.audio.setdefault(key, []).append((pk.sector, e.pts, e.frames))
405
+ if e.sid != 0xBD:
406
+ m = out.mpeg_audio.setdefault(e.sid, [0, bytes(data[e.start:e.start + 4])])
407
+ m[0] += e.end - e.start
408
+ video.close()
409
+ out.pictures, out.frame = video.pictures, video.frame
410
+ out.sequence = video.sequence
411
+ out.subpictures = spu.units
412
+ return out
413
+
414
+
415
+ def audio_streams(packs):
416
+ """{stream key: [(sector, pts, frames)]} of each sound stream's packets
417
+ in a list of packs; the key is (0xBD, sub-stream) or (stream id, None)."""
418
+ out = {}
419
+ for pk in packs:
420
+ for e in pk.pes:
421
+ if e.sid == 0xBD and e.sub not in SUB_SPU:
422
+ out.setdefault((0xBD, e.sub), []).append((pk.sector, e.pts, e.frames))
423
+ elif 0xC0 <= e.sid <= 0xDF:
424
+ out.setdefault((e.sid, None), []).append((pk.sector, e.pts, None))
425
+ return out
426
+
427
+
428
+ def audio_frame_ticks(key):
429
+ """90 kHz ticks per audio frame of a sub-stream (the frames counted in
430
+ its packet headers); None for MPEG sound, whose packets do not count
431
+ them."""
432
+ sid, sub = key
433
+ if sid == 0xBD and sub in SUB_AC3:
434
+ return 2880 # 1536 samples at 48 kHz
435
+ if sid == 0xBD and sub in SUB_DTS:
436
+ return 960 # 512 samples at 48 kHz
437
+ if sid == 0xBD and sub in SUB_LPCM:
438
+ return 150 # 1/600 s
439
+ return None
440
+
441
+
442
+ def audio_end(packets, key):
443
+ """When a sound stream ends: its last stamped packet's PTS plus the
444
+ frames that begin there and after (None for MPEG sound)."""
445
+ tick = audio_frame_ticks(key)
446
+ if tick is None:
447
+ return None
448
+ last, frames = None, 0
449
+ for _, pts, n in packets:
450
+ if pts is not None:
451
+ last, frames = pts, 0
452
+ frames += n or 0
453
+ return None if last is None else last + frames * tick
454
+
455
+
456
+ # MPEG-1 audio (ISO/IEC 11172-3) frame header: bit rates in kbit/s by layer
457
+ _MPEG_BITRATES = {
458
+ 3: (0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448), # layer I
459
+ 2: (0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384), # layer II
460
+ }
461
+ _MPEG_RATES = (44100, 48000, 32000)
462
+
463
+
464
+ def mpeg_audio_end(packets, counted):
465
+ """When an MPEG audio stream ends: its first stamped packet's PTS plus
466
+ the frames its bytes hold (`counted`: [bytes, the first frame header]),
467
+ at the constant bit rate of that header. None when nothing is known."""
468
+ if not counted or counted[0] == 0:
469
+ return None
470
+ first = next((pts for _, pts, _ in packets if pts is not None), None)
471
+ if first is None:
472
+ return None
473
+ h = counted[1]
474
+ if len(h) < 4 or h[0] != 0xFF or (h[1] & 0xF8) != 0xF8:
475
+ raise VtsError("MPEG audio that is not MPEG-1 (the only kind a DVD carries)")
476
+ layer = (h[1] >> 1) & 3
477
+ bitrate = _MPEG_BITRATES.get(layer, (0,) * 16)[h[2] >> 4] * 1000
478
+ rate_index = (h[2] >> 2) & 3
479
+ if layer not in _MPEG_BITRATES or not bitrate or rate_index > 2:
480
+ raise VtsError("an MPEG audio frame header vtskit cannot read")
481
+ rate = _MPEG_RATES[rate_index]
482
+ samples = 384 if layer == 3 else 1152
483
+ size = samples // 8 * bitrate // rate
484
+ return first + round(counted[0] / size) * samples * 90000 // rate