3 logger
= logging
.getLogger('pyTivo.video.qt-faststart')
5 Quicktime/MP4 Fast Start
6 ------------------------
7 Enable streaming and pseudo-streaming of Quicktime and MP4 files by
8 moving metadata and offset information to the front of the file.
10 This program is based on qt-faststart.c from the ffmpeg project, which is
11 released into the public domain, as well as ISO 14496-12:2005 (the official
12 spec for MP4), which can be obtained from the ISO or found online.
14 The goals of this project are to run anywhere without compilation (in
15 particular, many Windows and Mac OS X users have trouble getting
16 qt-faststart.c compiled), to run about as fast as the C version, to be more
17 user friendly, and to use less actual lines of code doing so.
22 * Works everywhere Python can be installed
23 * Handles both 32-bit (stco) and 64-bit (co64) atoms
24 * Handles any file where the mdat atom is before the moov atom
25 * Preserves the order of other atoms
26 * Can replace the original file (if given no output file)
30 Copyright (C) 2008 Daniel G. Taylor <dan@programmer-art.org>
32 This program is free software: you can redistribute it and/or modify
33 it under the terms of the GNU General Public License as published by
34 the Free Software Foundation, either version 3 of the License, or
35 (at your option) any later version.
37 This program is distributed in the hope that it will be useful,
38 but WITHOUT ANY WARRANTY; without even the implied warranty of
39 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
40 GNU General Public License for more details.
42 You should have received a copy of the GNU General Public License
43 along with this program. If not, see <http://www.gnu.org/licenses/>.
49 from optparse
import OptionParser
50 from StringIO
import StringIO
53 CHUNK_SIZE
= 512 * 1024
57 def read_atom(datastream
):
59 Read an atom and return a tuple of (size, type) where size is the size
60 in bytes (including the 8 bytes already read) and type is a "fourcc"
61 like "ftyp" or "moov".
63 values
= struct
.unpack(">L4c", datastream
.read(8))
64 return values
[0], "".join(values
[1:])
66 def get_index(datastream
):
68 Return an index of top level atoms, their absolute byte-position in the
69 file and their size in a dict:
78 The keys are not guaranteed to be in order, but can be put in order
79 with a simple sort, e.g.
81 >>> keys = index.keys()
82 >>> keys.sort(lambda x, y: cmp(index[x][0], index[y][0]))
86 # Read atoms until we catch an error
89 atom_size
, atom_type
= read_atom(datastream
)
92 index
[atom_type
] = [datastream
.tell() - 8, atom_size
]
93 datastream
.seek(atom_size
- 8, os
.SEEK_CUR
)
95 # Make sure the atoms we need exist
96 for key
in ["ftyp", "moov", "mdat"]:
97 if not index
.has_key(key
):
98 logger
.debug( "%s atom not found, is this a valid MOV/MP4 file?" % key
)
103 def find_atoms(size
, datastream
):
105 This function is a generator that will yield either "stco" or "co64"
106 when either atom is found. datastream can be assumed to be 8 bytes
107 into the stco or co64 atom when the value is yielded.
109 It is assumed that datastream will be at the end of the atom after
110 the value has been yielded and processed.
112 size is the number of bytes to the end of the atom in the datastream.
114 stop
= datastream
.tell() + size
116 while datastream
.tell() < stop
:
117 atom_size
, atom_type
= read_atom(datastream
)
118 if atom_type
in ["trak", "mdia", "minf", "stbl"]:
119 # Known ancestor atom of stco or co64, search within it!
120 for atype
in find_atoms(atom_size
- 8, datastream
):
122 elif atom_type
in ["stco", "co64"]:
125 # Ignore this atom, seek to the end of it.
126 datastream
.seek(atom_size
- 8, os
.SEEK_CUR
)
128 def output(outfile
, offset
, data
):
131 if count
+ length
> offset
:
133 data
= data
[offset
- count
:]
137 def fast_start(datastream
, outfile
, offset
=0):
139 Convert a Quicktime/MP4 file for streaming by moving the metadata to
140 the front of the file. This method writes a new file.
146 # Get the top level atom index
147 index
= get_index(datastream
)
148 # Make sure moov occurs AFTER mdat, otherwise no need to run!
149 if len(index
) == 0 or index
["moov"][0] < index
["mdat"][0]:
150 logger
.debug('mp4 already streamable -- copying')
151 datastream
.seek(offset
)
153 block
= datastream
.read(CHUNK_SIZE
)
160 datastream
.seek(index
["moov"][0])
161 moov_size
= index
["moov"][1]
162 moov
= StringIO(datastream
.read(moov_size
))
165 for atom_type
in find_atoms(moov_size
- 8, moov
):
166 # Read either 32-bit or 64-bit offsets
167 ctype
, csize
= atom_type
== "stco" and ("L", 4) or ("Q", 8)
169 # Get number of entries
170 version
, entry_count
= struct
.unpack(">2L", moov
.read(8))
172 logger
.debug("Patching %s with %d entries" % (atom_type
, entry_count
))
175 entries
= struct
.unpack(">" + ctype
* entry_count
,
176 moov
.read(csize
* entry_count
))
178 # Patch and write entries
179 moov
.seek(-csize
* entry_count
, os
.SEEK_CUR
)
180 moov
.write(struct
.pack(">" + ctype
* entry_count
,
181 *[entry
+ moov_size
for entry
in entries
]))
184 datastream
.seek(index
["ftyp"][0])
185 output(outfile
, offset
, datastream
.read(index
["ftyp"][1]))
189 output(outfile
, offset
, moov
.read())
192 atoms
= [atom
for atom
in index
.keys() if atom
not in ["ftyp", "moov"]]
193 atoms
.sort(lambda x
, y
: cmp(index
[x
][0], index
[y
][0]))
195 start
, size
= index
[atom
]
196 datastream
.seek(start
)
198 # Write in chunks to not use too much memory
199 for x
in range(size
/ CHUNK_SIZE
):
200 output(outfile
, offset
, datastream
.read(CHUNK_SIZE
))
202 if size
% CHUNK_SIZE
:
203 output(outfile
, offset
, datastream
.read(size
% CHUNK_SIZE
))