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/__init__.py +25 -0
- vtskit/errors.py +8 -0
- vtskit/ifo.py +113 -0
- vtskit/lang.py +648 -0
- vtskit/menu.py +171 -0
- vtskit/nav.py +256 -0
- vtskit/ps.py +484 -0
- vtskit/spu.py +262 -0
- vtskit/subtitles.py +126 -0
- vtskit/titleset.py +967 -0
- vtskit/vm.py +482 -0
- vtskit/vmg.py +159 -0
- vtskit-0.1.0.dist-info/METADATA +205 -0
- vtskit-0.1.0.dist-info/RECORD +18 -0
- vtskit-0.1.0.dist-info/WHEEL +5 -0
- vtskit-0.1.0.dist-info/licenses/COPYING +674 -0
- vtskit-0.1.0.dist-info/licenses/COPYING.LESSER +165 -0
- vtskit-0.1.0.dist-info/top_level.txt +1 -0
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
|