Skip to content

Commit 888f1f4

Browse files
author
wm4
committed
Add png->mkv muxing script and example file
The file is from wikipedia. Only 10 times larger than the gif (I never said this was a good idea). Only mpv can play this file. Hail Satan.
1 parent f27c211 commit 888f1f4

File tree

2 files changed

+166
-0
lines changed

2 files changed

+166
-0
lines changed

matroska/png/example.mkv

2.79 MB
Binary file not shown.

matroska/png/write_mkv_png.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
#!/usr/bin/env python3
2+
3+
#
4+
# This file is part of mpv.
5+
#
6+
# mpv is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU Lesser General Public
8+
# License as published by the Free Software Foundation; either
9+
# version 2.1 of the License, or (at your option) any later version.
10+
#
11+
# mpv is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public
17+
# License along with mpv. If not, see <http://www.gnu.org/licenses/>.
18+
#
19+
20+
import binascii
21+
import io
22+
import os
23+
import sys
24+
25+
class Elem:
26+
27+
def __init__(self, ebml_id, sub = None):
28+
self.ebml_id = ebml_id
29+
self.sub = sub
30+
31+
files = []
32+
outfile = None
33+
fps = 25
34+
for i in sys.argv[1:]:
35+
if i.startswith("--o="):
36+
assert outfile is None
37+
outfile = open(i[4:], "wb")
38+
elif i.startswith("--fps="):
39+
fps = float(i[6:])
40+
elif i.startswith("-"):
41+
print("Unknown option '%s'" % i)
42+
print("Usage: %s --fps=25 --o=outfile.mkv infile1.png infile2.png ..." %
43+
sys.argv[0])
44+
exit(1)
45+
else:
46+
files.append(open(i, "rb").read())
47+
48+
if outfile is None or not files:
49+
print("no")
50+
exit(1)
51+
52+
global duration
53+
duration = 0
54+
55+
global clusters
56+
clusters = []
57+
58+
for f in files:
59+
# Making each image its own cluster is slightly wasteful, but simpler.
60+
data = io.BytesIO()
61+
data.write((0x80 | 1).to_bytes(1, "big")) # Track Number
62+
data.write((0).to_bytes(2, "big")) # block Timestamp (relative to cluster's)
63+
data.write((0).to_bytes(1, "big")) # Block Header flags
64+
data.write(f)
65+
66+
block_duration = 1.0 / fps
67+
68+
clusters.append(
69+
Elem('1f43b675', [ # Cluster
70+
Elem('e7', int(duration * 1e9 / 1000000)), # Timestamp
71+
Elem('a0', [ # BlockGroup
72+
Elem('a1', data.getvalue()), # Block
73+
Elem('9b', int(block_duration * 1e9 / 1000000)), # BlockDuration
74+
])
75+
])
76+
)
77+
78+
duration += block_duration
79+
80+
def write_elem(dst, elem):
81+
dst.write(binascii.unhexlify(elem.ebml_id))
82+
83+
content = io.BytesIO()
84+
85+
val = elem.sub
86+
if type(val) == int:
87+
# EBML/Matroska does have signed integers. But whether a value is signed
88+
# or unsigned (i.e. meaning of the MSB) depends on the element ID.
89+
# The Python type is ambiguous for this purpose. Fortunately, we use
90+
# only unsigned elements.
91+
assert(val >= 0)
92+
blen = max((val.bit_length() + 7) // 8, 1)
93+
assert(blen <= 8)
94+
#debug print(val, val.to_bytes(blen, "big"))
95+
content.write(val.to_bytes(blen, "big"))
96+
elif type(val) == float:
97+
# Why the fuck do integers have to_bytes, but floats don't?
98+
import struct
99+
content.write(struct.pack(">f", val))
100+
elif type(val) == str:
101+
content.write(val.encode("utf-8"))
102+
elif type(val) == bytes:
103+
content.write(val)
104+
elif type(val) == list:
105+
for sub in val:
106+
write_elem(content, sub)
107+
else:
108+
assert False, "Unknown type %s" % type(val)
109+
110+
buf = content.getvalue()
111+
112+
# element length
113+
e_len = len(buf)
114+
num_bytes = -1
115+
for i in range(1, 9):
116+
if e_len < 2 ** (i * 8 - i) - 1:
117+
num_bytes = i
118+
break
119+
assert(num_bytes >= 0 and num_bytes <= 8)
120+
mask = 2 ** (8 - num_bytes)
121+
len_bytes = e_len.to_bytes(num_bytes, "big")
122+
dst.write((len_bytes[0] | mask).to_bytes(1, "big"))
123+
dst.write(len_bytes[1:])
124+
125+
dst.write(buf)
126+
127+
header = Elem('1a45dfa3', [ # EMBL
128+
Elem('4286', 1), # EBMLVersion
129+
Elem('42f7', 1), # EBMLReadVersion
130+
Elem('42f2', 4), # EBMLMaxIDLength
131+
Elem('42f3', 8), # EBMLMaxSizeLength
132+
Elem('4282', "matroska"), # DocType
133+
Elem('4287', 2), # DocTypeVersion
134+
Elem('4285', 2), # DocTypeReadVersion
135+
])
136+
137+
segment = Elem('18538067', [ # Segment
138+
Elem('1549a966', [ # Info
139+
Elem('4d80', "shitty python script"), # MuxingApp
140+
Elem('5741', "sorry I don't know"), # WritingApp
141+
Elem('2ad7b1', 1000000), # TimestampScale
142+
# The stuff the Matroska devs smoked, was it xiph-laced with lead?
143+
Elem('4489', duration * 1e9 / 1000000), # Duration
144+
]),
145+
Elem('1654ae6b', [ # Tracks
146+
Elem('ae', [ # TrackEntry
147+
Elem('d7', 1), # TrackNumber
148+
Elem('73c5', 1), # TrackUID
149+
Elem('83', 1), # TrackType
150+
Elem('23e383', int((1.0 / fps) * 1e9)), # DefaultDuration
151+
Elem('86', "V_PNG"), # CodecID
152+
# Note: elements like video dimension are probably required normally,
153+
# but I can't be arsed.
154+
Elem('e0', []), # Video
155+
]),
156+
]),
157+
# Note: you could avoid buffering all clusters in memory by picking smaller
158+
# clusters, and using "unknown size" for the segment EBML size, or reserve
159+
# the maximum for it.
160+
] + clusters)
161+
162+
write_elem(outfile, header)
163+
write_elem(outfile, segment)
164+
outfile.close()
165+
166+
exit(1)

0 commit comments

Comments
 (0)