Consolidate "tivo_names" and "tivo_ports" into "tivos"; (temporarily?)
[pyTivo/wmcbrine/lucasnz.git] / plugins / video / qtfaststart.py
bloba5ef3e368cc4b0dd540d4edf0c9a35dfbac59004
1 """
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.
16 Features
17 --------
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)
25 History
26 -------
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
31 to 1.4.
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,
36 version bump to 1.1
37 * 2008-09-02: Initial release
39 License
40 -------
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
62 SOFTWARE.
63 """
65 import logging
66 import os
67 import struct
69 from StringIO import StringIO
71 VERSION = "1.7wjm3"
72 CHUNK_SIZE = 8192
74 log = logging.getLogger('pyTivo.video.qt-faststart')
76 count = 0
78 class FastStartException(Exception):
79 pass
81 def read_atom(datastream):
82 """
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".
86 """
87 return struct.unpack(">L4s", datastream.read(8))
89 def get_index(datastream):
90 """
91 Return an index of top level atoms, their absolute byte-position in the
92 file and their size in a list:
94 index = [
95 ("ftyp", 0, 24),
96 ("moov", 25, 2658),
97 ("free", 2683, 8),
98 ...
101 The tuple elements will be in the order that they appear in the file.
103 index = []
105 log.debug("Getting index of top level atoms...")
107 # Read atoms until we catch an error
108 while(datastream):
109 try:
110 skip = 8
111 atom_size, atom_type = read_atom(datastream)
112 if atom_size == 1:
113 atom_size = struct.unpack(">Q", datastream.read(8))[0]
114 skip = 16
115 log.debug("%s: %s" % (atom_type, atom_size))
116 except:
117 break
119 index.append((atom_type, datastream.tell() - skip, atom_size))
121 if atom_size == 0:
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!
126 break
127 else:
128 # Weird, but just continue to try to find more atoms
129 atom_size = skip
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()
140 return index
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:
156 try:
157 atom_size, atom_type = read_atom(datastream)
158 except:
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):
165 yield atype
166 elif atom_type in ["stco", "co64"]:
167 yield atom_type
168 else:
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):
173 global count
174 length = len(data)
175 if count + length > skip:
176 if skip > count:
177 data = data[skip - count:]
178 outfile.write(data)
179 count += length
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.
187 global count
188 count = 0
190 # Get the top level atom index
191 index = get_index(datastream)
193 mdat_pos = 999999
194 free_size = 0
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!
199 if atom == "moov":
200 moov_pos = pos
201 moov_size = size
202 elif atom == "mdat":
203 mdat_pos = pos
204 elif atom == "free" and pos < mdat_pos:
205 # This free atom is before the mdat!
206 free_size += size
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
210 free_size += 8
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
218 offset -= moov_size
219 if not free_size:
220 # No free atoms and moov is correct, we are done!
221 log.debug('mp4 already streamable -- copying')
222 datastream.seek(skip)
223 while True:
224 block = datastream.read(CHUNK_SIZE)
225 if not block:
226 break
227 output(outfile, 0, block)
228 return count
230 # Read and fix moov
231 datastream.seek(moov_pos)
232 moov = StringIO(datastream.read(moov_size))
234 # Ignore moov identifier and size, start reading children
235 moov.seek(8)
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))
246 # Read entries
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...")
257 # Write ftype
258 for atom, pos, size in index:
259 if atom == "ftyp":
260 datastream.seek(pos)
261 output(outfile, skip, datastream.read(size))
263 # Write moov
264 moov.seek(0)
265 output(outfile, skip, moov.read())
267 # Write the rest
268 atoms = [item for item in index if item[0] not in ["ftyp", "moov", "free"]]
269 for atom, pos, size in atoms:
270 datastream.seek(pos)
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))
279 return count - skip