2 Quicktime/MP4 Fast Start
3 ------------------------
4 Enable streaming and pseudo-streaming of Quicktime and MP4 files by
5 moving metadata and offset information to the front of the file.
7 This program is based on qt-faststart.c from the ffmpeg project, which is
8 released into the public domain, as well as ISO 14496-12:2005 (the official
9 spec for MP4), which can be obtained from the ISO or found online.
11 The goals of this project are to run anywhere without compilation (in
12 particular, many Windows and Mac OS X users have trouble getting
13 qt-faststart.c compiled), to run about as fast as the C version, to be more
14 user friendly, and to use less actual lines of code doing so.
19 * Works everywhere Python can be installed
20 * Handles both 32-bit (stco) and 64-bit (co64) atoms
21 * Handles any file where the mdat atom is before the moov atom
22 * Preserves the order of other atoms
23 * Can replace the original file (if given no output file)
27 * 2013-01-28: Support strange zero-name, zero-length atoms, re-license
28 under the MIT license, version bump to 1.7
29 * 2010-02-21: Add support for final mdat atom with zero size, patch by
30 Dmitry Simakov <basilio AT j-vista DOT ru>, version bump
32 * 2009-11-05: Add --sample option. Version bump to 1.3.
33 * 2009-03-13: Update to be more library-friendly by using logging module,
34 rename fast_start => process, version bump to 1.2
35 * 2008-10-04: Bug fixes, support multiple atoms of the same type,
37 * 2008-09-02: Initial release
42 Copyright (C) 2008 - 2013 Daniel G. Taylor <dan@programmer-art.org>
44 Permission is hereby granted, free of charge, to any person
45 obtaining a copy of this software and associated documentation files
46 (the "Software"), to deal in the Software without restriction,
47 including without limitation the rights to use, copy, modify, merge,
48 publish, distribute, sublicense, and/or sell copies of the Software,
49 and to permit persons to whom the Software is furnished to do so,
50 subject to the following conditions:
52 The above copyright notice and this permission notice shall be
53 included in all copies or substantial portions of the Software.
55 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
56 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
57 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
58 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
59 BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
60 ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
61 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
69 from StringIO
import StringIO
74 log
= logging
.getLogger('pyTivo.video.qt-faststart')
78 class FastStartException(Exception):
81 def read_atom(datastream
):
83 Read an atom and return a tuple of (size, type) where size is the size
84 in bytes (including the 8 bytes already read) and type is a "fourcc"
85 like "ftyp" or "moov".
87 return struct
.unpack(">L4s", datastream
.read(8))
89 def get_index(datastream
):
91 Return an index of top level atoms, their absolute byte-position in the
92 file and their size in a list:
101 The tuple elements will be in the order that they appear in the file.
105 log
.debug("Getting index of top level atoms...")
107 # Read atoms until we catch an error
111 atom_size
, atom_type
= read_atom(datastream
)
113 atom_size
= struct
.unpack(">Q", datastream
.read(8))[0]
115 log
.debug("%s: %s" % (atom_type
, atom_size
))
119 index
.append((atom_type
, datastream
.tell() - skip
, atom_size
))
122 if atom_type
== "mdat":
123 # Some files may end in mdat with no size set, which
124 # generally means to seek to the end of the file. We can
125 # just stop indexing as no more entries will be found!
128 # Weird, but just continue to try to find more atoms
131 datastream
.seek(atom_size
- skip
, os
.SEEK_CUR
)
133 # Make sure the atoms we need exist
134 top_level_atoms
= set([item
[0] for item
in index
])
135 for key
in ["moov", "mdat"]:
136 if key
not in top_level_atoms
:
137 log
.error("%s atom not found, is this a valid MOV/MP4 file?" % key
)
138 raise FastStartException()
142 def find_atoms(size
, datastream
):
144 This function is a generator that will yield either "stco" or "co64"
145 when either atom is found. datastream can be assumed to be 8 bytes
146 into the stco or co64 atom when the value is yielded.
148 It is assumed that datastream will be at the end of the atom after
149 the value has been yielded and processed.
151 size is the number of bytes to the end of the atom in the datastream.
153 stop
= datastream
.tell() + size
155 while datastream
.tell() < stop
:
157 atom_size
, atom_type
= read_atom(datastream
)
159 log
.exception("Error reading next atom!")
160 raise FastStartException()
162 if atom_type
in ["trak", "mdia", "minf", "stbl"]:
163 # Known ancestor atom of stco or co64, search within it!
164 for atype
in find_atoms(atom_size
- 8, datastream
):
166 elif atom_type
in ["stco", "co64"]:
169 # Ignore this atom, seek to the end of it.
170 datastream
.seek(atom_size
- 8, os
.SEEK_CUR
)
172 def output(outfile
, skip
, data
):
175 if count
+ length
> skip
:
177 data
= data
[skip
- count
:]
181 def process(datastream
, outfile
, skip
=0):
183 Convert a Quicktime/MP4 file for streaming by moving the metadata to
184 the front of the file. This method writes a new file.
190 # Get the top level atom index
191 index
= get_index(datastream
)
196 # Make sure moov occurs AFTER mdat, otherwise no need to run!
197 for atom
, pos
, size
in index
:
198 # The atoms are guaranteed to exist from get_index above!
204 elif atom
== "free" and pos
< mdat_pos
:
205 # This free atom is before the mdat!
207 log
.info("Removing free atom at %d (%d bytes)" % (pos
, size
))
208 elif atom
== "\x00\x00\x00\x00" and pos
< mdat_pos
:
209 # This is some strange zero atom with incorrect size
211 log
.info("Removing strange zero atom at %s (8 bytes)" % pos
)
213 # Offset to shift positions
214 offset
= moov_size
- free_size
216 if moov_pos
< mdat_pos
:
217 # moov appears to be in the proper place, don't shift by moov size
220 # No free atoms and moov is correct, we are done!
221 log
.debug('mp4 already streamable -- copying')
222 datastream
.seek(skip
)
224 block
= datastream
.read(CHUNK_SIZE
)
227 output(outfile
, 0, block
)
231 datastream
.seek(moov_pos
)
232 moov
= StringIO(datastream
.read(moov_size
))
234 # Ignore moov identifier and size, start reading children
237 for atom_type
in find_atoms(moov_size
- 8, moov
):
238 # Read either 32-bit or 64-bit offsets
239 ctype
, csize
= atom_type
== "stco" and ("L", 4) or ("Q", 8)
241 # Get number of entries
242 version
, entry_count
= struct
.unpack(">2L", moov
.read(8))
244 log
.info("Patching %s with %d entries" % (atom_type
, entry_count
))
247 entries
= struct
.unpack(">" + ctype
* entry_count
,
248 moov
.read(csize
* entry_count
))
250 # Patch and write entries
251 moov
.seek(-csize
* entry_count
, os
.SEEK_CUR
)
252 moov
.write(struct
.pack(">" + ctype
* entry_count
,
253 *[entry
+ offset
for entry
in entries
]))
255 log
.info("Writing output...")
258 for atom
, pos
, size
in index
:
261 output(outfile
, skip
, datastream
.read(size
))
265 output(outfile
, skip
, moov
.read())
268 atoms
= [item
for item
in index
if item
[0] not in ["ftyp", "moov", "free"]]
269 for atom
, pos
, size
in atoms
:
272 # Write in chunks to not use too much memory
273 for x
in range(size
/ CHUNK_SIZE
):
274 output(outfile
, skip
, datastream
.read(CHUNK_SIZE
))
276 if size
% CHUNK_SIZE
:
277 output(outfile
, skip
, datastream
.read(size
% CHUNK_SIZE
))