oxi 0.1.0__tar.gz
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.
- oxi-0.1.0/LICENSE +21 -0
- oxi-0.1.0/PKG-INFO +32 -0
- oxi-0.1.0/README.md +14 -0
- oxi-0.1.0/oxi/__init__.py +19 -0
- oxi-0.1.0/oxi/__main__.py +13 -0
- oxi-0.1.0/oxi/activate_this.py +24 -0
- oxi-0.1.0/oxi/app.py +8 -0
- oxi-0.1.0/oxi/examples/__init__.py +0 -0
- oxi-0.1.0/oxi/examples/activate_this.py +24 -0
- oxi-0.1.0/oxi/mp4parser.py +464 -0
- oxi-0.1.0/oxi/server.py +308 -0
- oxi-0.1.0/oxi/template.py +990 -0
- oxi-0.1.0/oxi/tests/__init__.py +0 -0
- oxi-0.1.0/oxi/tests/activate_this.py +24 -0
- oxi-0.1.0/oxi/tests/first.py +3 -0
- oxi-0.1.0/oxi/tests/response.py +12 -0
- oxi-0.1.0/oxi/tests/second.py +4 -0
- oxi-0.1.0/oxi/utils.py +110 -0
- oxi-0.1.0/oxi/websocket.py +285 -0
- oxi-0.1.0/oxi.egg-info/PKG-INFO +32 -0
- oxi-0.1.0/oxi.egg-info/SOURCES.txt +24 -0
- oxi-0.1.0/oxi.egg-info/dependency_links.txt +1 -0
- oxi-0.1.0/oxi.egg-info/entry_points.txt +3 -0
- oxi-0.1.0/oxi.egg-info/top_level.txt +1 -0
- oxi-0.1.0/setup.cfg +4 -0
- oxi-0.1.0/setup.py +28 -0
oxi-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Domingo Ernesto Savoretti
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
oxi-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: oxi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A simple library to read and write dbase III files.
|
|
5
|
+
Home-page: https://github.com/sandy98/pybase3
|
|
6
|
+
Author: Domingo E. Savoretti
|
|
7
|
+
Author-email: esavoretti@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Platform: UNKNOWN
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<img src="static/img/oxi_1_rounded.jpg" alt="Oxi Logo" width="125px" />
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<h1 align="center">Oxi</h1>
|
|
22
|
+
|
|
23
|
+
ASGI Full Stack:
|
|
24
|
+
|
|
25
|
+
* Web Server
|
|
26
|
+
* Web App
|
|
27
|
+
|
|
28
|
+
...and yes, Oxi is a cutie faithful dog!
|
|
29
|
+
|
|
30
|
+
Coming soon...
|
|
31
|
+
|
|
32
|
+
|
oxi-0.1.0/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
__version__ = "0.1.0"
|
|
3
|
+
__author__ = "Domingo E. Savoretti"
|
|
4
|
+
__email__ = "esavoretti@gmail.com"
|
|
5
|
+
__license__ = "MIT"
|
|
6
|
+
__url__ = "https://github.com/sandy98/pybase3"
|
|
7
|
+
__description__ = "A simple library to read and write dbase III files."
|
|
8
|
+
__copyright__ = "Copyright (c) 2025 Domingo E. Savoretti"
|
|
9
|
+
__oxi_port__ = 8086
|
|
10
|
+
__oxi_host__ = "0.0.0.0"
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"utils",
|
|
14
|
+
"mp4parser",
|
|
15
|
+
"template",
|
|
16
|
+
"websocket",
|
|
17
|
+
"app",
|
|
18
|
+
"server"
|
|
19
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# This script is used to manually activate a virtual environment by modifying
|
|
4
|
+
# the system path and environment variables to point to the virtual environment.
|
|
5
|
+
|
|
6
|
+
import os, sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
oxi_env = False
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
print("Activating virtualenv...")
|
|
13
|
+
|
|
14
|
+
# Path to the virtual environment
|
|
15
|
+
oxi_path = Path(os.path.abspath(__file__)).parent.parent
|
|
16
|
+
|
|
17
|
+
# Update sys.path to include oxi path
|
|
18
|
+
sys.path.insert(0, str(oxi_path))
|
|
19
|
+
|
|
20
|
+
oxi_env = True
|
|
21
|
+
print(f"Virtual environment activated for Oxi: {str(oxi_path)}.")
|
|
22
|
+
except Exception as e:
|
|
23
|
+
print("Virtual environment activation failed. Error: {0}".format(str(e)))
|
|
24
|
+
|
oxi-0.1.0/oxi/app.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# This script is used to manually activate a virtual environment by modifying
|
|
4
|
+
# the system path and environment variables to point to the virtual environment.
|
|
5
|
+
|
|
6
|
+
import os, sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
oxi_env = False
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
print("Activating virtualenv...")
|
|
13
|
+
|
|
14
|
+
# Path to the virtual environment
|
|
15
|
+
oxi_path = Path(os.path.abspath(__file__)).parent.parent.parent
|
|
16
|
+
|
|
17
|
+
# Update sys.path to include oxi path
|
|
18
|
+
sys.path.insert(0, str(oxi_path))
|
|
19
|
+
|
|
20
|
+
oxi_env = True
|
|
21
|
+
print(f"Virtual environment activated for Oxi: {str(oxi_path)}.")
|
|
22
|
+
except Exception as e:
|
|
23
|
+
print("Virtual environment activation failed. Error: {0}".format(str(e)))
|
|
24
|
+
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf_8 -*-
|
|
3
|
+
|
|
4
|
+
# test with: static/Media/Videos/Cele/Cele_patinando_cerca_de_Venecia.mp4
|
|
5
|
+
|
|
6
|
+
import os, mmap, io
|
|
7
|
+
from mimetypes import guess_type
|
|
8
|
+
import argparse #, copy
|
|
9
|
+
from . import __version__ as oxi_version
|
|
10
|
+
|
|
11
|
+
class Smmap:
|
|
12
|
+
def __init__(self, fileno, offset:int = 0, limit:int = 0):
|
|
13
|
+
self._mmap = mmap.mmap(fileno, 0, flags=mmap.MAP_PRIVATE)
|
|
14
|
+
self._offset = offset
|
|
15
|
+
self._limit = limit
|
|
16
|
+
self._mmap.seek(self._offset)
|
|
17
|
+
|
|
18
|
+
def __getitem__(self, index):
|
|
19
|
+
shifted_index = None
|
|
20
|
+
if isinstance(index, slice):
|
|
21
|
+
indexstart = 0 if not index.start else index.start
|
|
22
|
+
indexstop = len(self._mmap) if not index.stop else index.stop
|
|
23
|
+
indexstart = 0 if indexstart < 0 else indexstart
|
|
24
|
+
indexstart = self._limit if indexstart > self._limit else indexstart
|
|
25
|
+
indexstop = 0 if indexstop < 0 else indexstop
|
|
26
|
+
indexstop = self._limit if indexstop > self._limit else indexstop
|
|
27
|
+
shifted_index = slice(indexstart + self._offset, indexstop + self._offset, index.step)
|
|
28
|
+
else:
|
|
29
|
+
shifted_index = index
|
|
30
|
+
shifted_index = 0 if shifted_index < 0 else shifted_index
|
|
31
|
+
shifted_index = self._limit if shifted_index > self._limit else shifted_index
|
|
32
|
+
shifted_index += self._offset
|
|
33
|
+
return self._mmap.__getitem__(shifted_index)
|
|
34
|
+
|
|
35
|
+
def __setitem__(self, index, value):
|
|
36
|
+
shifted_index = None
|
|
37
|
+
if isinstance(index, slice):
|
|
38
|
+
indexstart = 0 if not index.start else index.start
|
|
39
|
+
indexstop = len(self._mmap) if not index.stop else index.stop
|
|
40
|
+
indexstart = 0 if indexstart < 0 else indexstart
|
|
41
|
+
indexstart = self._limit if indexstart > self._limit else indexstart
|
|
42
|
+
indexstop = 0 if indexstop < 0 else indexstop
|
|
43
|
+
indexstop = self._limit if indexstop > self._limit else indexstop
|
|
44
|
+
diff = indexstop - indexstart
|
|
45
|
+
if not isinstance(value, bytes) or len(value) > diff:
|
|
46
|
+
raise ValueError(f"value must be an instance of bytes with len <= to {diff}")
|
|
47
|
+
for i in range(diff):
|
|
48
|
+
self._mmap[indexstart + i + self._offset] = value[i]
|
|
49
|
+
else:
|
|
50
|
+
shifted_index = index
|
|
51
|
+
shifted_index = 0 if shifted_index < 0 else shifted_index
|
|
52
|
+
shifted_index = self._limit if shifted_index > self._limit else shifted_index
|
|
53
|
+
shifted_index += self._offset
|
|
54
|
+
self._mmap[shifted_index], = value
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def size(self):
|
|
58
|
+
return self._limit
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def offset(self):
|
|
62
|
+
return self._offset
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def mmap(self):
|
|
66
|
+
return self._mmap
|
|
67
|
+
|
|
68
|
+
def seek(self, num: int):
|
|
69
|
+
num = self._limit if num >= self._limit else num
|
|
70
|
+
self._mmap.seek(num + self._offset)
|
|
71
|
+
|
|
72
|
+
def tell(self):
|
|
73
|
+
return self._mmap.tell() - self._offset
|
|
74
|
+
|
|
75
|
+
def read(self, nbytes: int = -1):
|
|
76
|
+
if nbytes == -1 or nbytes >= (self._limit - self.tell()):
|
|
77
|
+
nbytes = self._limit - self.tell()
|
|
78
|
+
return self._mmap.read(nbytes)
|
|
79
|
+
|
|
80
|
+
def write(self, what: bytes = b''):
|
|
81
|
+
self._mmap.write(what)
|
|
82
|
+
|
|
83
|
+
def find(self, what: bytes):
|
|
84
|
+
found = self._mmap.find(what)
|
|
85
|
+
return found if found == -1 else found - self._offset
|
|
86
|
+
|
|
87
|
+
def rfind(self, what: bytes):
|
|
88
|
+
found = self._mmap.rfind(what)
|
|
89
|
+
return found if found == -1 else found - self._offset
|
|
90
|
+
|
|
91
|
+
def close(self):
|
|
92
|
+
self._mmap.close()
|
|
93
|
+
|
|
94
|
+
def __del__(self):
|
|
95
|
+
try:
|
|
96
|
+
if not self._mmap.closed:
|
|
97
|
+
self._mmap.close()
|
|
98
|
+
except:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Mp4:
|
|
103
|
+
"""Stub for Atom"""
|
|
104
|
+
|
|
105
|
+
class Atom:
|
|
106
|
+
def __init__(self, offset: int, size: int, name: str, level: int = 0, ordinal: int = -1, container=None):
|
|
107
|
+
self.offset = offset
|
|
108
|
+
self.size = size
|
|
109
|
+
self.name = name
|
|
110
|
+
self.ordinal = ordinal
|
|
111
|
+
self.level = level
|
|
112
|
+
# self.contents = contents
|
|
113
|
+
self.container = container
|
|
114
|
+
self.children: list = []
|
|
115
|
+
self._mm = None
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def contents(self):
|
|
119
|
+
if self._mm:
|
|
120
|
+
return self._mm
|
|
121
|
+
if not self.container:
|
|
122
|
+
return None
|
|
123
|
+
fp = self.container.fp
|
|
124
|
+
if not fp:
|
|
125
|
+
return None
|
|
126
|
+
self._mm = Smmap(fp.fileno(), self.offset, self.size)
|
|
127
|
+
return self._mm
|
|
128
|
+
|
|
129
|
+
def setcontents(self, new_mm):
|
|
130
|
+
self._mm = new_mm
|
|
131
|
+
|
|
132
|
+
def __repr__(self):
|
|
133
|
+
# Atom ftyp @ 0 of size: 32, ends @ 32
|
|
134
|
+
args = ('\t' * self.level, self.name, f"{self.offset:,}", f"{self.size:,}", f"{(self.offset + self.size):,}")
|
|
135
|
+
return "{0}Atom {1} @ {2} of size: {3} , ends @ {4}".format(*args)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def __str__(self):
|
|
139
|
+
mainline = repr(self)
|
|
140
|
+
lenchildren = len(self.children) if self.children else 0
|
|
141
|
+
childrenlines = ''.join(map(str, self.children)) if lenchildren else ''
|
|
142
|
+
return "\n".join([mainline, childrenlines])
|
|
143
|
+
|
|
144
|
+
def load_contents(self, fp = None):
|
|
145
|
+
# Retained for backward compatibility
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
def load_children(self):
|
|
149
|
+
container: Mp4 = self.container
|
|
150
|
+
ordinal = 0
|
|
151
|
+
offset = self.offset + 8
|
|
152
|
+
fp = mmap.mmap(container.fp.fileno(), container.filesize, flags=mmap.MAP_PRIVATE)
|
|
153
|
+
while offset < (self.size + self.offset):
|
|
154
|
+
child = container._get_atom(fp, offset, ordinal, self.level + 1)
|
|
155
|
+
if child.size == 0:
|
|
156
|
+
raise ValueError("Atom of size 0 not admitted out of 0 level.")
|
|
157
|
+
if child.name == '\x00\x00\x00\x00':
|
|
158
|
+
offset += child.size
|
|
159
|
+
continue
|
|
160
|
+
if child.name in ["trak", "mdia", "minf", "stbl"]:
|
|
161
|
+
# Known ancestor atom of stco or co64, search within it!
|
|
162
|
+
# for res in _find_atoms_ex(atom, datastream):
|
|
163
|
+
# yield res
|
|
164
|
+
child.load_children()
|
|
165
|
+
elif child.name in ["stco", "co64"]:
|
|
166
|
+
self.container.relocation_targets.append(child)
|
|
167
|
+
self.children.append(child)
|
|
168
|
+
ordinal += 1
|
|
169
|
+
offset += child.size
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def boundaries(self):
|
|
173
|
+
return [self.offset, self.offset + self.size]
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def named_boundaries(self):
|
|
177
|
+
return {self.name: self.boundaries}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class Mp4:
|
|
181
|
+
def __init__(self, filename):
|
|
182
|
+
if not os.path.exists(filename):
|
|
183
|
+
raise NameError(f"File {filename} doesn't exist.")
|
|
184
|
+
if guess_type(filename)[0] not in ['video/mp4', 'video/quicktime']:
|
|
185
|
+
raise TypeError(f"File {filename} is not an MP4 file.")
|
|
186
|
+
self.filename = filename
|
|
187
|
+
self.filesize = os.path.getsize(filename)
|
|
188
|
+
self.loop = None
|
|
189
|
+
self.atoms: list[Atom] = []
|
|
190
|
+
self.relocation_targets: list[Atom] = []
|
|
191
|
+
self.ftyp: Atom = None
|
|
192
|
+
self.moov: Atom = None
|
|
193
|
+
self.free: Atom = None
|
|
194
|
+
self.mdat: Atom = None
|
|
195
|
+
self._patched_moov: bytearray = None
|
|
196
|
+
self.fp = open(self.filename, 'rb')
|
|
197
|
+
self.atoms = self._collectatoms()
|
|
198
|
+
|
|
199
|
+
def __del__(self):
|
|
200
|
+
try:
|
|
201
|
+
if not self.fp.closed:
|
|
202
|
+
self.fp.close()
|
|
203
|
+
except:
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
def __str__(self):
|
|
207
|
+
atomstrs = "\n".join(list(map(lambda a: " " + str(a), self.atoms)))
|
|
208
|
+
return f"""
|
|
209
|
+
|
|
210
|
+
{'_' * 80}
|
|
211
|
+
|
|
212
|
+
File: {self.filename}
|
|
213
|
+
Size: {self.filesize:,}
|
|
214
|
+
{'_' * 80}
|
|
215
|
+
|
|
216
|
+
{atomstrs}
|
|
217
|
+
|
|
218
|
+
{'_' * 80}
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
def __repr__(self):
|
|
222
|
+
return "\n".join(self.quicklist())
|
|
223
|
+
|
|
224
|
+
def _get_atom(self, fp: mmap.mmap, offset: int, ordinal: int=0, level:int=0):
|
|
225
|
+
size, name = [int.from_bytes(fp[offset:offset+4], byteorder='big'), fp[offset+4:offset+8].decode('latin-1')]
|
|
226
|
+
if size == 1:
|
|
227
|
+
size = int.from_bytes(fp[offset+8:offset+16], byteorder='big')
|
|
228
|
+
atom = Atom(offset=offset, size=size, name=name, level=level, ordinal = ordinal, container=self)
|
|
229
|
+
return atom
|
|
230
|
+
|
|
231
|
+
def _parse_mp4(self):
|
|
232
|
+
filesize = self.filesize
|
|
233
|
+
with mmap.mmap(self.fp.fileno(), filesize, flags=mmap.MAP_PRIVATE) as fp:
|
|
234
|
+
ordinal = 0
|
|
235
|
+
offset = 0
|
|
236
|
+
while offset < filesize:
|
|
237
|
+
atom = self._get_atom(fp, offset, ordinal, 0)
|
|
238
|
+
if atom.size == 0:
|
|
239
|
+
atom.size = self.filesize - offset
|
|
240
|
+
yield atom
|
|
241
|
+
return
|
|
242
|
+
ordinal += 1
|
|
243
|
+
offset += atom.size
|
|
244
|
+
if atom.name != '\x00\x00\x00\x00':
|
|
245
|
+
yield atom
|
|
246
|
+
else:
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
def _collectatoms(self):
|
|
250
|
+
atoms = [self._classify_atom(atom) for atom in self._parse_mp4()]
|
|
251
|
+
# if self.moov:
|
|
252
|
+
# self.moov.load_children()
|
|
253
|
+
return atoms
|
|
254
|
+
|
|
255
|
+
def _is_relocatable(self):
|
|
256
|
+
return (not not self.moov) and (not self._is_compressed()) and (self.moov.ordinal > self.mdat.ordinal)
|
|
257
|
+
|
|
258
|
+
def _patch_moov(self):
|
|
259
|
+
if self._patched_moov is not None or not self.moov or self._is_compressed():
|
|
260
|
+
return
|
|
261
|
+
offset_shift = self.moov.size
|
|
262
|
+
self.moov.contents.seek(0)
|
|
263
|
+
self._patched_moov = mmap.mmap(-1, self.moov.size)
|
|
264
|
+
self._patched_moov.write(self.moov.contents.read())
|
|
265
|
+
for atom in self.relocation_targets:
|
|
266
|
+
os.sys.stderr.write(f'{atom.name} located at ' + str(atom.offset) + '\n')
|
|
267
|
+
os.sys.stderr.flush()
|
|
268
|
+
offset_pos_begin = atom.offset - self.moov.offset
|
|
269
|
+
offset_pos = offset_pos_begin
|
|
270
|
+
entry_count = int.from_bytes(self._patched_moov[offset_pos+12:offset_pos+16], byteorder='big')
|
|
271
|
+
print(f"Patching atom '{atom.name}' with {entry_count} entries.")
|
|
272
|
+
displacement = 4 if atom.name == 'stco' else 8 # in case atom.name == 'co64'
|
|
273
|
+
for i in range(entry_count):
|
|
274
|
+
offset_pos = offset_pos_begin + 16 + (i * displacement)
|
|
275
|
+
current_offset = int.from_bytes(self._patched_moov[offset_pos:offset_pos+4], byteorder='big')
|
|
276
|
+
new_offset = current_offset + offset_shift
|
|
277
|
+
# print(f"{i}: Transferring offset {current_offset} to {new_offset}")
|
|
278
|
+
self._patched_moov[offset_pos:offset_pos+4] = new_offset.to_bytes(displacement, byteorder='big')
|
|
279
|
+
|
|
280
|
+
def _classify_atom(self, atom: Atom):
|
|
281
|
+
# atom.container = self
|
|
282
|
+
# atom.load_contents(self.fp)
|
|
283
|
+
if atom.name == 'ftyp':
|
|
284
|
+
self.ftyp = atom
|
|
285
|
+
elif atom.name == 'moov':
|
|
286
|
+
atom.load_children()
|
|
287
|
+
self.moov = atom
|
|
288
|
+
elif atom.name == 'free':
|
|
289
|
+
self.free = atom
|
|
290
|
+
elif atom.name == 'mdat':
|
|
291
|
+
self.mdat = atom
|
|
292
|
+
elif not hasattr(self, atom.name):
|
|
293
|
+
setattr(self, atom.name, atom)
|
|
294
|
+
return atom
|
|
295
|
+
|
|
296
|
+
def _get_chunks(self, limit: int, stream = None, chunksize:int = None):
|
|
297
|
+
if stream is None:
|
|
298
|
+
stream = self.fp
|
|
299
|
+
chunk_size = chunksize or mmap.PAGESIZE
|
|
300
|
+
while chunk_size < (2 ** 15):
|
|
301
|
+
chunk_size *= 2
|
|
302
|
+
remaining = limit
|
|
303
|
+
while remaining:
|
|
304
|
+
chunk = stream.read(min(remaining, chunk_size))
|
|
305
|
+
if not chunk:
|
|
306
|
+
return
|
|
307
|
+
remaining -= len(chunk)
|
|
308
|
+
yield chunk
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def boundaries(self):
|
|
312
|
+
return list(map(lambda a: a.named_boundaries, self.atoms))
|
|
313
|
+
|
|
314
|
+
@property
|
|
315
|
+
def faststart(self):
|
|
316
|
+
if not self._is_relocatable():
|
|
317
|
+
return self.atoms
|
|
318
|
+
temp_ordinal = self.mdat.ordinal
|
|
319
|
+
self.mdat.ordinal = self.moov.ordinal
|
|
320
|
+
self.moov.ordinal = temp_ordinal
|
|
321
|
+
self.moov.setcontents(self.patched_moov)
|
|
322
|
+
self.atoms.sort(key=lambda atom: atom.ordinal)
|
|
323
|
+
for index, atm in enumerate(self.atoms):
|
|
324
|
+
atm.contents.seek(0)
|
|
325
|
+
if index == 0:
|
|
326
|
+
continue
|
|
327
|
+
atm.offset = self.atoms[index-1].offset + self.atoms[index-1].size
|
|
328
|
+
return self.atoms
|
|
329
|
+
|
|
330
|
+
@property
|
|
331
|
+
def faststart_boundaries(self):
|
|
332
|
+
return list(map(lambda a: a.named_boundaries, self.faststart))
|
|
333
|
+
|
|
334
|
+
@property
|
|
335
|
+
def patched_moov(self):
|
|
336
|
+
if not self._patched_moov:
|
|
337
|
+
self._patch_moov()
|
|
338
|
+
return self._patched_moov
|
|
339
|
+
|
|
340
|
+
def _get_moov_atom(self):
|
|
341
|
+
"""
|
|
342
|
+
Deprecated. 'moov' is now an Mp4 attribute.
|
|
343
|
+
"""
|
|
344
|
+
for atom in self.atoms:
|
|
345
|
+
if atom.name == 'moov':
|
|
346
|
+
return atom
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
def _is_compressed(self):
|
|
350
|
+
for atom in self.moov.children:
|
|
351
|
+
if atom.name == 'cmov':
|
|
352
|
+
print(f"Warning!\nFile {self.filename} cannot be optimized for streaming because it's compressed.")
|
|
353
|
+
return True
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
def stream(self, begin: int=0, end: int=None):
|
|
357
|
+
for atm in self.faststart:
|
|
358
|
+
atm.contents.seek(0)
|
|
359
|
+
if atm.name not in ['mdat', 'moov']:
|
|
360
|
+
yield atm.contents.read()
|
|
361
|
+
else:
|
|
362
|
+
chunkgen = self._get_chunks(limit=atm.size, stream=atm.contents)
|
|
363
|
+
for chunk in chunkgen:
|
|
364
|
+
try:
|
|
365
|
+
yield chunk
|
|
366
|
+
except:
|
|
367
|
+
chunkgen.close()
|
|
368
|
+
break
|
|
369
|
+
print(f"Mp4 '{atm.name}' stream sent succesfully ({atm.size:,} bytes).")
|
|
370
|
+
|
|
371
|
+
def stream_range(self, begin: int=0, end: int=0):
|
|
372
|
+
if not end:
|
|
373
|
+
end = self.filesize - 1
|
|
374
|
+
beginner, ender = {} , {}
|
|
375
|
+
for atom in self.faststart:
|
|
376
|
+
if begin in range(*atom.boundaries):
|
|
377
|
+
beginner['name'] = atom.name
|
|
378
|
+
beginner['ordinal'] = atom.ordinal
|
|
379
|
+
beginner['offset'] = begin - atom.offset
|
|
380
|
+
if end in range(*atom.boundaries):
|
|
381
|
+
ender['name'] = atom.name
|
|
382
|
+
ender['ordinal'] = atom.ordinal
|
|
383
|
+
ender['offset'] = end - atom.offset
|
|
384
|
+
if not ender:
|
|
385
|
+
atom = self.faststart[-1]
|
|
386
|
+
ender['name'] = atom.name
|
|
387
|
+
ender['ordinal'] = atom.ordinal
|
|
388
|
+
ender['offset'] = atom.size - 1
|
|
389
|
+
if not beginner:
|
|
390
|
+
atom = self.faststart[0]
|
|
391
|
+
ender['name'] = atom.name
|
|
392
|
+
ender['ordinal'] = atom.ordinal
|
|
393
|
+
ender['offset'] = 0
|
|
394
|
+
fragment = None
|
|
395
|
+
for atom in self.faststart:
|
|
396
|
+
if atom.ordinal < beginner.get('ordinal'):
|
|
397
|
+
continue
|
|
398
|
+
if atom.ordinal > ender.get('ordinal'):
|
|
399
|
+
return
|
|
400
|
+
if atom.ordinal == beginner.get('ordinal'):
|
|
401
|
+
frag_begin = beginner.get('offset')
|
|
402
|
+
if atom.ordinal == ender.get('ordinal'):
|
|
403
|
+
length = ender.get('offset') - frag_begin
|
|
404
|
+
# fragment = atom.contents[beginner.get('offset'):ender.get('offset')]
|
|
405
|
+
else:
|
|
406
|
+
length = atom.size - frag_begin
|
|
407
|
+
# fragment = atom.contents[beginner.get('offset'):]
|
|
408
|
+
elif atom.ordinal == ender.get('ordinal'):
|
|
409
|
+
frag_begin = 0
|
|
410
|
+
length = ender.get('offset')
|
|
411
|
+
# fragment = atom.contents[:ender.get('offset')]
|
|
412
|
+
else:
|
|
413
|
+
frag_begin = 0
|
|
414
|
+
length = atom.size
|
|
415
|
+
# fragment = atom.contents[:]
|
|
416
|
+
atom.contents.seek(frag_begin)
|
|
417
|
+
gen = self._get_chunks(length, atom.contents)
|
|
418
|
+
for chunk in gen:
|
|
419
|
+
yield chunk
|
|
420
|
+
|
|
421
|
+
def save(self, outputfile: str = 'out.mp4'):
|
|
422
|
+
with open(outputfile, 'wb') as fp:
|
|
423
|
+
for chunk in self.stream():
|
|
424
|
+
fp.write(chunk)
|
|
425
|
+
|
|
426
|
+
print(f"Mp4 file {self.filename} saved succesfully to {outputfile}.")
|
|
427
|
+
|
|
428
|
+
def quicklist(self):
|
|
429
|
+
fileline = f"File: {self.filename}"
|
|
430
|
+
sizeline= f"Size: {self.filesize:,}"
|
|
431
|
+
underline = "_" * max(len(fileline), len(sizeline))
|
|
432
|
+
return [underline, fileline, sizeline, underline, *list(map(repr, self.atoms))]
|
|
433
|
+
|
|
434
|
+
def main():
|
|
435
|
+
parser = argparse.ArgumentParser(description='Command line arguments for Mp4Parser', prog='mp4parser')
|
|
436
|
+
parser.add_argument('-V', '--version', action='version', version=f"{parser.prog} v {oxi_version}", help=f"Shows {parser.prog} version and exits.")
|
|
437
|
+
parser.add_argument('inputfile', type=str, help="Name of the file to be listed/processed.")
|
|
438
|
+
parser.add_argument('-o', '--outputfile', type=str, default='out.mp4', help="Name of the file you wish to save to. Defaults to 'out.mp4'")
|
|
439
|
+
group = parser.add_mutually_exclusive_group(required=False)
|
|
440
|
+
group.add_argument('-s', '--save', action="store_true", help="Use this option to save the input file to a new one.")
|
|
441
|
+
group.add_argument('-l', '--list', action="store_true", help="Shows a list of the main level atoms contained in the input file.")
|
|
442
|
+
group.add_argument('-L', '--full-list', action="store_true", help="Shows a detailed list of the atoms contained in the input file.")
|
|
443
|
+
|
|
444
|
+
args = parser.parse_args()
|
|
445
|
+
filename = args.inputfile
|
|
446
|
+
|
|
447
|
+
try:
|
|
448
|
+
mp4 = Mp4(filename)
|
|
449
|
+
except Exception as exc:
|
|
450
|
+
print(f"ERROR: {exc}")
|
|
451
|
+
raise SystemExit
|
|
452
|
+
|
|
453
|
+
if args.save:
|
|
454
|
+
mp4.save(args.outputfile)
|
|
455
|
+
elif args.list:
|
|
456
|
+
print('\n'.join(mp4.quicklist()))
|
|
457
|
+
elif args.full_list:
|
|
458
|
+
print(mp4)
|
|
459
|
+
else:
|
|
460
|
+
print('\n'.join(mp4.quicklist()))
|
|
461
|
+
|
|
462
|
+
if __name__ == '__main__':
|
|
463
|
+
main()
|
|
464
|
+
|