bdmvkit 0.2.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.
- bdmvkit/__init__.py +16 -0
- bdmvkit/__main__.py +86 -0
- bdmvkit/clip.py +70 -0
- bdmvkit/disc.py +257 -0
- bdmvkit/encode.py +152 -0
- bdmvkit/errors.py +9 -0
- bdmvkit/formats.py +45 -0
- bdmvkit/hdmv.py +149 -0
- bdmvkit/igs.py +241 -0
- bdmvkit/m2ts.py +78 -0
- bdmvkit/pgs.py +260 -0
- bdmvkit/playlist.py +62 -0
- bdmvkit-0.2.0.dist-info/METADATA +165 -0
- bdmvkit-0.2.0.dist-info/RECORD +19 -0
- bdmvkit-0.2.0.dist-info/WHEEL +5 -0
- bdmvkit-0.2.0.dist-info/entry_points.txt +2 -0
- bdmvkit-0.2.0.dist-info/licenses/COPYING +674 -0
- bdmvkit-0.2.0.dist-info/licenses/COPYING.LESSER +165 -0
- bdmvkit-0.2.0.dist-info/top_level.txt +1 -0
bdmvkit/hdmv.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""HDMV navigation: the 12-byte navigation commands, MovieObject.bdmv and
|
|
2
|
+
index.bdmv."""
|
|
3
|
+
|
|
4
|
+
import struct
|
|
5
|
+
|
|
6
|
+
from .formats import VERSION
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _u8(v):
|
|
10
|
+
return struct.pack(">B", v)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _u16(v):
|
|
14
|
+
return struct.pack(">H", v)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _u32(v):
|
|
18
|
+
return struct.pack(">I", v)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _pad(data, n):
|
|
22
|
+
return data + b"\0" * (-len(data) % n)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def index_bdmv(title_objects, first_play, top_menu, video_format, frame_rate, interactive=()):
|
|
26
|
+
"""title_objects: the movie object of every title (title 1 first);
|
|
27
|
+
interactive: the titles (1-based) that are menus."""
|
|
28
|
+
app = _u32(34) + bytes([0, video_format << 4 | frame_rate]) + bytes(32)
|
|
29
|
+
|
|
30
|
+
def hdmv(obj, playback_type):
|
|
31
|
+
# playback_type 0 = movie, 1 = interactive
|
|
32
|
+
return _u16(playback_type << 14) + _u16(obj) + _u32(0)
|
|
33
|
+
|
|
34
|
+
body = _u32(1 << 30) + hdmv(first_play, 1) + _u32(1 << 30) + hdmv(top_menu, 1)
|
|
35
|
+
body += _u16(len(title_objects))
|
|
36
|
+
for n, obj in enumerate(title_objects, 1):
|
|
37
|
+
body += _u32(1 << 30) + hdmv(obj, int(n in interactive)) # HDMV, access permitted
|
|
38
|
+
index_start = 40 + len(app)
|
|
39
|
+
return b"INDX" + VERSION + _u32(index_start) + _u32(0) + bytes(24) + app + \
|
|
40
|
+
_u32(len(body)) + body
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _cmd(b0, b1, dst=0, src=0, b3=0, b2=0):
|
|
44
|
+
"""A 12-byte navigation command: operand count / group / sub-group,
|
|
45
|
+
immediate flags / branch option, compare option, set option; operands."""
|
|
46
|
+
return bytes([b0, b1, b2, b3]) + _u32(dst) + _u32(src)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def play_pl(playlist):
|
|
50
|
+
"""Play PlayList <playlist> (an immediate number)."""
|
|
51
|
+
return _cmd(0x22, 0x80, playlist)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def set_subtitles(number):
|
|
55
|
+
"""Set Stream: subtitle (PG) track `number` shown, or 0 = none shown."""
|
|
56
|
+
# Set group, SetSystem sub-group, two immediate operands, option 1 (Set Stream);
|
|
57
|
+
# the first operand: PG stream number valid (0x8000), shown (0x4000)
|
|
58
|
+
return _cmd(0x51, 0xC0, 0xC000 | number if number else 0, 0, 0x01)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def jump_title(title):
|
|
62
|
+
"""Jump Title <title>: 1..N, 0 = top menu."""
|
|
63
|
+
return _cmd(0x21, 0x81, title)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def jump_title_reg(reg):
|
|
67
|
+
"""Jump Title to the number held in general register `reg`."""
|
|
68
|
+
return _cmd(0x21, 0x01, reg)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def jump_object(obj):
|
|
72
|
+
"""Jump Object <obj> (a movie object number)."""
|
|
73
|
+
return _cmd(0x21, 0x80, obj)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def play_pl_mark(playlist, mark):
|
|
77
|
+
"""Play PlayList <playlist> from mark <mark> (0-based)."""
|
|
78
|
+
return _cmd(0x42, 0xC2, playlist, mark)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def goto(pc):
|
|
82
|
+
"""Goto command <pc> of the same object."""
|
|
83
|
+
return _cmd(0x20, 0x81, pc)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def move(reg, value):
|
|
88
|
+
"""Move <value> into general register <reg>."""
|
|
89
|
+
return _cmd(0x50, 0x40, reg, value, 0x01)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def set_button(button=None, reg=None):
|
|
93
|
+
"""Set Button Page: select button `button` (an id), or the one held in
|
|
94
|
+
general register `reg`; the page stays."""
|
|
95
|
+
# Set group, SetSystem sub-group, option 3; the operand's top bit: button valid
|
|
96
|
+
if reg is not None:
|
|
97
|
+
return _cmd(0x51, 0x40, 0x80000000 | reg, 0, 0x03)
|
|
98
|
+
return _cmd(0x51, 0xC0, 0x80000000 | button, 0, 0x03)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def add(reg, value):
|
|
102
|
+
"""Add <value> to general register <reg>."""
|
|
103
|
+
return _cmd(0x50, 0x40, reg, value, 0x03)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def if_at_least(reg, value):
|
|
107
|
+
"""Compare: the next command runs only if general register `reg` >= value."""
|
|
108
|
+
return _cmd(0x48, 0x40, reg, value, b2=0x04)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def if_equal(reg, value):
|
|
112
|
+
"""Compare: the next command runs only if general register `reg` == value."""
|
|
113
|
+
return _cmd(0x48, 0x40, reg, value, b2=0x02)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def if_not_equal(reg, value):
|
|
117
|
+
"""Compare: the next command runs only if general register `reg` != value."""
|
|
118
|
+
return _cmd(0x48, 0x40, reg, value, b2=0x03)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def popup_off():
|
|
122
|
+
"""Pop-up Off: close the pop-up menu."""
|
|
123
|
+
return _cmd(0x11, 0x00, b3=0x07)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def resume():
|
|
127
|
+
"""Resume the title the menu was called from."""
|
|
128
|
+
return _cmd(0x01, 0x04)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def set_streams(audio=None, subtitle=None):
|
|
132
|
+
"""Set Stream: audio track `audio` (1-based) and/or subtitle track
|
|
133
|
+
`subtitle` shown (0 = subtitles off)."""
|
|
134
|
+
dst = 0
|
|
135
|
+
if audio:
|
|
136
|
+
dst |= 0x80000000 | audio << 16
|
|
137
|
+
if subtitle is not None:
|
|
138
|
+
dst |= (0xC000 | subtitle) if subtitle else 0
|
|
139
|
+
return _cmd(0x51, 0xC0, dst, 0, 0x01)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def movie_objects(objects):
|
|
143
|
+
"""objects: lists of 12-byte navigation commands."""
|
|
144
|
+
data = b""
|
|
145
|
+
for cmds in objects:
|
|
146
|
+
# resume_intention_flag set: a menu called during playback can resume it
|
|
147
|
+
data += _u16(1 << 15) + _u16(len(cmds)) + b"".join(cmds)
|
|
148
|
+
body = _u32(0) + _u16(len(objects)) + data
|
|
149
|
+
return b"MOBJ" + VERSION + _u32(0) + bytes(28) + _u32(len(body)) + body
|
bdmvkit/igs.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Interactive Graphics (IG) streams: a menu's buttons.
|
|
2
|
+
|
|
3
|
+
A menu on a Blu-ray is a clip (its picture) carrying an IG stream: one
|
|
4
|
+
interactive composition with a page of buttons, each with a picture for
|
|
5
|
+
its normal, selected and activated states, the buttons it moves to with
|
|
6
|
+
the arrow keys, and the navigation commands it runs when activated.
|
|
7
|
+
|
|
8
|
+
buttons = [Button("play", 200, 600, selected=rgba, activated=rgba, w=400, h=80,
|
|
9
|
+
commands=[hdmv.jump_title(1)])]
|
|
10
|
+
data = ig_stream(buttons, frame_rate=1) # write it as a .sup
|
|
11
|
+
|
|
12
|
+
The state pictures are RGBA (straight alpha) of the button's size; they
|
|
13
|
+
share one palette of up to 255 colours (quantize). Without a normal
|
|
14
|
+
picture a button shows nothing until it is selected (the highlight
|
|
15
|
+
is drawn over the menu's own picture). A pop-up composition (`popup`)
|
|
16
|
+
is shown by the remote's Pop-up key over a title instead of a menu clip.
|
|
17
|
+
|
|
18
|
+
ffmpeg muxes an IG stream given as a .sup like a subtitle stream; the
|
|
19
|
+
clip's PMT is then corrected with patch_ig_stream_type.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import dataclasses
|
|
23
|
+
import struct
|
|
24
|
+
|
|
25
|
+
from . import pgs
|
|
26
|
+
from .errors import BdmvError
|
|
27
|
+
from .formats import CODING_IG, HEIGHT, IG_PID, PMT_PID, WIDTH
|
|
28
|
+
|
|
29
|
+
ICS, PDS, ODS, END = 0x18, 0x14, 0x15, 0x80
|
|
30
|
+
NO_OBJECT = 0xFFFF
|
|
31
|
+
DIRECTIONS = ("up", "down", "left", "right")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclasses.dataclass
|
|
35
|
+
class Button:
|
|
36
|
+
id: str
|
|
37
|
+
x: int # top-left on the 1920 x 1080 frame
|
|
38
|
+
y: int
|
|
39
|
+
w: int
|
|
40
|
+
h: int
|
|
41
|
+
selected: bytes # RGBA, w x h
|
|
42
|
+
activated: bytes = None # (default: the selected picture)
|
|
43
|
+
normal: bytes = None # (default: nothing until selected)
|
|
44
|
+
commands: list = dataclasses.field(default_factory=list) # 12-byte commands
|
|
45
|
+
nav: dict = dataclasses.field(default_factory=dict) # direction -> button id
|
|
46
|
+
auto: bool = False # runs as soon as it is selected
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def quantize(images):
|
|
50
|
+
"""RGBA pictures -> (palette [(r, g, b, a)] for indices 1.., index
|
|
51
|
+
pictures) with at most 255 colours; fully transparent pixels are index
|
|
52
|
+
0. Fewer bits per channel are kept until the colours fit; past that the
|
|
53
|
+
rarest go to their nearest kept colour."""
|
|
54
|
+
for bits in (8, 7, 6, 5, 4, 3):
|
|
55
|
+
mask = (0xFF << (8 - bits)) & 0xFF
|
|
56
|
+
table = bytes(v & mask for v in range(256))
|
|
57
|
+
q = [im.translate(table) for im in images]
|
|
58
|
+
counts = {}
|
|
59
|
+
for qi in q:
|
|
60
|
+
for i in range(0, len(qi), 4):
|
|
61
|
+
c = qi[i:i + 4]
|
|
62
|
+
if c[3]:
|
|
63
|
+
counts[c] = counts.get(c, 0) + 1
|
|
64
|
+
if len(counts) <= 255:
|
|
65
|
+
break
|
|
66
|
+
kept = sorted(counts, key=counts.get, reverse=True)[:255]
|
|
67
|
+
index = {c: k + 1 for k, c in enumerate(kept)}
|
|
68
|
+
for c in counts:
|
|
69
|
+
if c not in index:
|
|
70
|
+
index[c] = min(index.items(), key=lambda kv: sum((a - b) ** 2 for a, b in
|
|
71
|
+
zip(kv[0], c)))[1]
|
|
72
|
+
half = (~mask & 0xFF) >> 1
|
|
73
|
+
palette = [tuple(v | half if v else v for v in c) for c in kept]
|
|
74
|
+
out = [bytes(index.get(qi[i:i + 4], 0) if qi[i + 3] else 0 for i in range(0, len(qi), 4))
|
|
75
|
+
for qi in q]
|
|
76
|
+
return palette, out
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def auto_nav(buttons):
|
|
80
|
+
"""The arrow keys between buttons that do not say where they go: the
|
|
81
|
+
nearest button whose centre is that way (closer across counts more
|
|
82
|
+
than along), else the button itself. Returns {id: {direction: id}}."""
|
|
83
|
+
centre = {b.id: (b.x + b.w / 2, b.y + b.h / 2) for b in buttons}
|
|
84
|
+
out = {}
|
|
85
|
+
for b in buttons:
|
|
86
|
+
cx, cy = centre[b.id]
|
|
87
|
+
nav = {}
|
|
88
|
+
for d in DIRECTIONS:
|
|
89
|
+
if d in b.nav:
|
|
90
|
+
nav[d] = b.nav[d]
|
|
91
|
+
continue
|
|
92
|
+
best, score = b.id, None
|
|
93
|
+
for o in buttons:
|
|
94
|
+
if o is b:
|
|
95
|
+
continue
|
|
96
|
+
ox, oy = centre[o.id]
|
|
97
|
+
along = {"up": cy - oy, "down": oy - cy, "left": cx - ox, "right": ox - cx}[d]
|
|
98
|
+
across = abs(ox - cx) if d in ("up", "down") else abs(oy - cy)
|
|
99
|
+
if along <= 0:
|
|
100
|
+
continue
|
|
101
|
+
s = along + 2 * across
|
|
102
|
+
if score is None or s < score:
|
|
103
|
+
best, score = o.id, s
|
|
104
|
+
nav[d] = best
|
|
105
|
+
out[b.id] = nav
|
|
106
|
+
return out
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _segment(kind, data):
|
|
110
|
+
return b"PG" + struct.pack(">IIBH", 0, 0, kind, len(data)) + data
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _objects(oid, w, h, pixels):
|
|
114
|
+
"""ODS segment(s) of one object (split when longer than a segment)."""
|
|
115
|
+
coded = pgs.rle(pixels, w, h)
|
|
116
|
+
data = (len(coded) + 4).to_bytes(3, "big") + struct.pack(">HH", w, h) + coded
|
|
117
|
+
size = pgs.MAX_SEGMENT - 4
|
|
118
|
+
chunks = [data[i:i + size] for i in range(0, len(data), size)]
|
|
119
|
+
out = b""
|
|
120
|
+
for k, chunk in enumerate(chunks):
|
|
121
|
+
flags = (0x80 if k == 0 else 0) | (0x40 if k == len(chunks) - 1 else 0)
|
|
122
|
+
out += _segment(ODS, struct.pack(">HBB", oid, 0, flags) + chunk)
|
|
123
|
+
return out
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def ig_stream(buttons, frame_rate, default=None, popup=False, button_offset=0,
|
|
127
|
+
follow_selection=False):
|
|
128
|
+
"""The buttons as an IG stream (.sup layout: "PG" segments).
|
|
129
|
+
|
|
130
|
+
frame_rate: the menu clip's frame rate code (formats.FORMATS);
|
|
131
|
+
default: the id of the button selected when the menu starts (the
|
|
132
|
+
first); follow_selection: keep the player's selection instead (a menu
|
|
133
|
+
entered with a chosen button, set_button before playing it);
|
|
134
|
+
button_offset: the number keys select button n with n + this."""
|
|
135
|
+
if not buttons:
|
|
136
|
+
raise BdmvError("a menu needs at least one button")
|
|
137
|
+
ids = [b.id for b in buttons]
|
|
138
|
+
if len(set(ids)) != len(ids):
|
|
139
|
+
raise BdmvError("button ids must be unique")
|
|
140
|
+
for b in buttons:
|
|
141
|
+
if b.x < 0 or b.y < 0 or b.x + b.w > WIDTH or b.y + b.h > HEIGHT:
|
|
142
|
+
raise BdmvError(f"button {b.id!r} is not inside the {WIDTH} x {HEIGHT} frame")
|
|
143
|
+
for name in ("selected", "activated", "normal"):
|
|
144
|
+
pic = getattr(b, name)
|
|
145
|
+
if pic is not None and len(pic) != b.w * b.h * 4:
|
|
146
|
+
raise BdmvError(f"button {b.id!r}: its {name} picture is not {b.w} x {b.h} RGBA")
|
|
147
|
+
for d, to in b.nav.items():
|
|
148
|
+
if d not in DIRECTIONS or to not in ids:
|
|
149
|
+
raise BdmvError(f"button {b.id!r}: nav {d} -> {to!r}")
|
|
150
|
+
if default is not None and default not in ids:
|
|
151
|
+
raise BdmvError(f"no button {default!r}")
|
|
152
|
+
if button_offset + len(buttons) > 9999:
|
|
153
|
+
raise BdmvError("button numbers go up to 9999")
|
|
154
|
+
nav = auto_nav(buttons)
|
|
155
|
+
index = {b.id: k for k, b in enumerate(buttons)}
|
|
156
|
+
normal = any(b.normal is not None for b in buttons)
|
|
157
|
+
pictures = []
|
|
158
|
+
for b in buttons:
|
|
159
|
+
empty = bytes(b.w * b.h * 4)
|
|
160
|
+
pictures += [b.normal if b.normal is not None else empty, b.selected,
|
|
161
|
+
b.activated if b.activated is not None else b.selected]
|
|
162
|
+
colours, indexed = quantize(pictures)
|
|
163
|
+
palette = [(0, 16, 128, 128, 0)]
|
|
164
|
+
for k, (r, g, bl, a) in enumerate(colours, 1):
|
|
165
|
+
y, cb, cr = pgs.ycbcr((r, g, bl))
|
|
166
|
+
palette.append((k, y, cr, cb, a))
|
|
167
|
+
objects, body_buttons = b"", b""
|
|
168
|
+
for k, b in enumerate(buttons):
|
|
169
|
+
oids = {}
|
|
170
|
+
for slot in (0, 1, 2): # normal, selected, activated
|
|
171
|
+
if slot == 0 and not normal:
|
|
172
|
+
continue
|
|
173
|
+
oid = 3 * k + slot
|
|
174
|
+
objects += _objects(oid, b.w, b.h, indexed[3 * k + slot])
|
|
175
|
+
oids[slot] = oid
|
|
176
|
+
n_oid = oids.get(0, NO_OBJECT)
|
|
177
|
+
moves = [index[nav[b.id][d]] for d in DIRECTIONS]
|
|
178
|
+
cmds = list(b.commands)
|
|
179
|
+
for c in cmds:
|
|
180
|
+
if len(c) != 12:
|
|
181
|
+
raise BdmvError(f"button {b.id!r}: a navigation command is 12 bytes")
|
|
182
|
+
button = struct.pack(">HHBHH", k, button_offset + k + 1, 0x80 if b.auto else 0, b.x, b.y)
|
|
183
|
+
button += struct.pack(">HHHH", *moves)
|
|
184
|
+
button += struct.pack(">HHB", n_oid, n_oid, 0)
|
|
185
|
+
button += struct.pack(">BHHB", 0xFF, oids[1], oids[1], 0)
|
|
186
|
+
button += struct.pack(">BHH", 0xFF, oids[2], oids[2])
|
|
187
|
+
button += struct.pack(">H", len(cmds)) + b"".join(cmds)
|
|
188
|
+
# one button per overlap group: the buttons do not overlap
|
|
189
|
+
body_buttons += struct.pack(">HB", k, 1) + button
|
|
190
|
+
first = 0xFFFF if follow_selection else index.get(default, 0)
|
|
191
|
+
page = struct.pack(">BB", 0, 0) + bytes(8) # page 0, no user-operation mask
|
|
192
|
+
page += bytes([0, 0]) + bytes([0, 0]) # no in / out effects
|
|
193
|
+
page += struct.pack(">BHHBB", 0, first, NO_OBJECT, 0, len(buttons)) + body_buttons
|
|
194
|
+
# multiplexed; always on, or a pop-up (the Pop-up key shows it); no time-outs
|
|
195
|
+
body = bytes([0x40 if popup else 0x00]) + bytes(10)
|
|
196
|
+
body += (0).to_bytes(3, "big") + bytes([1]) + page # no user time-out; one page
|
|
197
|
+
ics = struct.pack(">HHB", WIDTH, HEIGHT, frame_rate << 4)
|
|
198
|
+
ics += struct.pack(">HB", 0, 0x80) # composition 0, epoch start
|
|
199
|
+
ics += bytes([0xC0]) + len(body).to_bytes(3, "big") + body
|
|
200
|
+
if len(ics) > pgs.MAX_SEGMENT:
|
|
201
|
+
raise BdmvError("too many buttons or button commands for one composition")
|
|
202
|
+
pds = bytes([0, 0]) + b"".join(bytes(e) for e in palette)
|
|
203
|
+
return _segment(ICS, ics) + _segment(PDS, pds) + objects + _segment(END, b"")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def patch_ig_stream_type(path, pmt_pid=PMT_PID, pid=IG_PID):
|
|
207
|
+
"""ffmpeg muxes the IG stream as a PGS stream (stream type 0x90): mark it
|
|
208
|
+
0x91 in every PMT of the clip (and fix the section CRC)."""
|
|
209
|
+
with open(path, "r+b") as f:
|
|
210
|
+
data = bytearray(f.read())
|
|
211
|
+
for off in range(0, len(data), 192):
|
|
212
|
+
p = off + 4
|
|
213
|
+
if (data[p + 1] & 0x1F) << 8 | data[p + 2] != pmt_pid or not data[p + 1] & 0x40:
|
|
214
|
+
continue
|
|
215
|
+
q = p + 4
|
|
216
|
+
if data[p + 3] & 0x20:
|
|
217
|
+
q += 1 + data[q]
|
|
218
|
+
q += 1 + data[q] # pointer field
|
|
219
|
+
length = (data[q + 1] & 0x0F) << 8 | data[q + 2]
|
|
220
|
+
end = q + 3 + length - 4
|
|
221
|
+
info = (data[q + 10] & 0x0F) << 8 | data[q + 11]
|
|
222
|
+
e = q + 12 + info
|
|
223
|
+
while e < end:
|
|
224
|
+
es_pid = (data[e + 1] & 0x1F) << 8 | data[e + 2]
|
|
225
|
+
if es_pid == pid:
|
|
226
|
+
data[e] = CODING_IG
|
|
227
|
+
e += 5 + ((data[e + 3] & 0x0F) << 8 | data[e + 4])
|
|
228
|
+
data[end:end + 4] = crc32(bytes(data[q:end])).to_bytes(4, "big")
|
|
229
|
+
f.seek(0)
|
|
230
|
+
f.write(data)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def crc32(data):
|
|
234
|
+
"""MPEG-2 section CRC (polynomial 0x04C11DB7, no reflection)."""
|
|
235
|
+
crc = 0xFFFFFFFF
|
|
236
|
+
for b in data:
|
|
237
|
+
crc ^= b << 24
|
|
238
|
+
for _ in range(8):
|
|
239
|
+
crc = ((crc << 1) ^ 0x04C11DB7) & 0xFFFFFFFF if crc & 0x80000000 else \
|
|
240
|
+
(crc << 1) & 0xFFFFFFFF
|
|
241
|
+
return crc
|
bdmvkit/m2ts.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Reading a BDAV transport stream (.m2ts) back: where its key frames are
|
|
2
|
+
and the span of its pictures, for the clip information and chapter marks."""
|
|
3
|
+
|
|
4
|
+
import dataclasses
|
|
5
|
+
|
|
6
|
+
from .errors import BdmvError
|
|
7
|
+
from .formats import VIDEO_PID
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclasses.dataclass
|
|
11
|
+
class ClipScan:
|
|
12
|
+
packets: int # 192-byte source packets
|
|
13
|
+
first_pts: int # 90 kHz, first picture shown
|
|
14
|
+
end_pts: int # 90 kHz, end of the last picture
|
|
15
|
+
pcr_pid: int
|
|
16
|
+
entries: list # [(pts, spn, size in bytes)] of the key frames, in order
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _pts(b, i):
|
|
20
|
+
return (((b[i] >> 1) & 7) << 30 | b[i + 1] << 22 | (b[i + 2] >> 1) << 15 |
|
|
21
|
+
b[i + 3] << 7 | b[i + 4] >> 1)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def scan_m2ts(path, video_pid=VIDEO_PID):
|
|
25
|
+
"""Where the key frames of a .m2ts are (for the entry-point map) and the
|
|
26
|
+
span of its pictures. Every picture is one PES packet (ffmpeg); key
|
|
27
|
+
frames carry the random-access flag."""
|
|
28
|
+
entries = []
|
|
29
|
+
pts_all = []
|
|
30
|
+
pcr_pid = None
|
|
31
|
+
spn = 0
|
|
32
|
+
with open(path, "rb") as f:
|
|
33
|
+
while True:
|
|
34
|
+
buf = f.read(192 * 8192)
|
|
35
|
+
if not buf:
|
|
36
|
+
break
|
|
37
|
+
if len(buf) % 192:
|
|
38
|
+
raise BdmvError(f"{path} is not a BDAV stream (length is not 192 × n)")
|
|
39
|
+
for off in range(0, len(buf), 192):
|
|
40
|
+
p = off + 4
|
|
41
|
+
b1 = buf[p + 1]
|
|
42
|
+
if not b1 & 0x40:
|
|
43
|
+
continue
|
|
44
|
+
if buf[p] != 0x47:
|
|
45
|
+
raise BdmvError(f"{path}: lost sync at packet {spn + off // 192}")
|
|
46
|
+
pid = (b1 & 0x1F) << 8 | buf[p + 2]
|
|
47
|
+
if pid != video_pid:
|
|
48
|
+
continue
|
|
49
|
+
afc = buf[p + 3] >> 4 & 3
|
|
50
|
+
q = p + 4
|
|
51
|
+
rai = False
|
|
52
|
+
if afc & 2:
|
|
53
|
+
alen = buf[q]
|
|
54
|
+
if alen:
|
|
55
|
+
rai = bool(buf[q + 1] & 0x40)
|
|
56
|
+
if buf[q + 1] & 0x10:
|
|
57
|
+
pcr_pid = pid
|
|
58
|
+
q += 1 + alen
|
|
59
|
+
# (a PES header must start in this packet with its PTS: 14 bytes)
|
|
60
|
+
if not afc & 1 or q + 14 > p + 188 or buf[q:q + 3] != b"\0\0\1" \
|
|
61
|
+
or not buf[q + 7] & 0x80:
|
|
62
|
+
continue
|
|
63
|
+
pts = _pts(buf, q + 9)
|
|
64
|
+
n = spn + off // 192
|
|
65
|
+
if entries and entries[-1][2] is None:
|
|
66
|
+
entries[-1][2] = (n - entries[-1][1]) * 192
|
|
67
|
+
pts_all.append(pts)
|
|
68
|
+
if rai:
|
|
69
|
+
entries.append([pts, n, None])
|
|
70
|
+
spn += len(buf) // 192
|
|
71
|
+
if not pts_all or not entries:
|
|
72
|
+
raise BdmvError(f"{path}: no pictures with key frames found")
|
|
73
|
+
if entries[-1][2] is None:
|
|
74
|
+
entries[-1][2] = (spn - entries[-1][1]) * 192
|
|
75
|
+
ordered = sorted(pts_all)
|
|
76
|
+
step = ordered[1] - ordered[0] if len(ordered) > 1 else 3003
|
|
77
|
+
return ClipScan(spn, ordered[0], ordered[-1] + step, pcr_pid or video_pid,
|
|
78
|
+
[tuple(e) for e in entries])
|