├── README.md └── qtrotate.py /README.md: -------------------------------------------------------------------------------- 1 | Quicktime/MP4 Rotation Tools 2 | ============================ 3 | Tools to work with rotated Quicktime/MP4 files. Currently this consists of a tool to detect and return the rotation angle if one can be found. Once known, you can use the info to rotate the video using MEncoder's rotate filter, AviSynth, etc. You can also write a new rotation angle into the files. 4 | 5 | NOTE that translation info will be LOST if a new rotation angle is written. 6 | 7 | 8 | Patches and new tools welcome. 9 | 10 | Simple Usage 11 | ------------ 12 | The script is usable as both a Python library and a standalone script: 13 | 14 | $ ./qtrotate.py myfile.mp4 15 | 90 16 | 17 | $ ./qtrotate.py myfile2.mp4 -90 18 | $ ./qtrotate.py myfile2.mp4 19 | 270 20 | 21 | License 22 | ------- 23 | Copyright (c) 2008 - 2009 Daniel G. Taylor 24 | 25 | Permission is hereby granted, free of charge, to any person obtaining a copy 26 | of this software and associated documentation files (the "Software"), to deal 27 | in the Software without restriction, including without limitation the rights 28 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 29 | copies of the Software, and to permit persons to whom the Software is 30 | furnished to do so, subject to the following conditions: 31 | 32 | The above copyright notice and this permission notice shall be included in 33 | all copies or substantial portions of the Software. 34 | 35 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 36 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 37 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 38 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 39 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 40 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 41 | THE SOFTWARE. 42 | -------------------------------------------------------------------------------- /qtrotate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | QT Rotate 5 | ========= 6 | Detect Rotated Quicktime/MP4 files. This script will spit out a rotation 7 | angle if one can be found. It can also write an new rotation angle. 8 | 9 | NOTE that translation info will be LOST if a new rotation angle is written. 10 | 11 | Usage: 12 | #read rotation angle 13 | bash-3.2$ ./qtrotate.py thomas.mp4 14 | 0 15 | 16 | #set new rotation angle 17 | bash-3.2$ ./qtrotate.py thomas.mp4 90 18 | 19 | #read new rotation angle 20 | bash-3.2$ ./qtrotate.py thomas.mp4 21 | 90 22 | 23 | """ 24 | 25 | import math 26 | import os 27 | import struct 28 | import sys 29 | 30 | def read_atom(datastream): 31 | """ 32 | Read an atom and return a tuple of (size, type) where size is the size 33 | in bytes (including the 8 bytes already read) and type is a "fourcc" 34 | like "ftyp" or "moov". 35 | """ 36 | return struct.unpack(">L4s", datastream.read(8)) 37 | 38 | def get_index(datastream): 39 | """ 40 | Return an index of top level atoms, their absolute byte-position in the 41 | file and their size in a list: 42 | 43 | index = [ 44 | ("ftyp", 0, 24), 45 | ("moov", 25, 2658), 46 | ("free", 2683, 8), 47 | ... 48 | ] 49 | 50 | The tuple elements will be in the order that they appear in the file. 51 | """ 52 | index = [] 53 | 54 | # Read atoms until we catch an error 55 | while(datastream): 56 | try: 57 | atom_size, atom_type = read_atom(datastream) 58 | except: 59 | break 60 | index.append((atom_type, datastream.tell() - 8, atom_size)) 61 | 62 | if atom_size < 8: 63 | break 64 | else: 65 | datastream.seek(atom_size - 8, os.SEEK_CUR) 66 | 67 | # Make sure the atoms we need exist 68 | top_level_atoms = set([item[0] for item in index]) 69 | for key in ["ftyp", "moov", "mdat"]: 70 | if key not in top_level_atoms: 71 | print "%s atom not found, is this a valid MOV/MP4 file?" % key 72 | raise SystemExit(1) 73 | 74 | return index 75 | 76 | def find_atoms(size, datastream): 77 | """ 78 | This function is a generator that will yield either "mvhd" or "tkhd" 79 | when either atom is found. datastream can be assumed to be 8 bytes 80 | into the atom when the value is yielded. 81 | 82 | It is assumed that datastream will be at the end of the atom after 83 | the value has been yielded and processed. 84 | 85 | size is the number of bytes to the end of the atom in the datastream. 86 | """ 87 | stop = datastream.tell() + size 88 | 89 | while datastream.tell() < stop: 90 | try: 91 | atom_size, atom_type = read_atom(datastream) 92 | except: 93 | print "Error reading next atom!" 94 | raise SystemExit(1) 95 | 96 | if atom_type in ["trak"]: 97 | # Known ancestor atom of stco or co64, search within it! 98 | for atype in find_atoms(atom_size - 8, datastream): 99 | yield atype 100 | elif atom_type in ["mvhd", "tkhd"]: 101 | yield atom_type 102 | else: 103 | # Ignore this atom, seek to the end of it. 104 | datastream.seek(atom_size - 8, os.SEEK_CUR) 105 | 106 | def get_set_rotation(infilename, set_degrees=None): 107 | """ 108 | Get and return the degrees of rotation in a file, or -1 if it cannot 109 | be determined. 110 | 111 | See ISO 14496-12:2005 and 112 | http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap2/chapter_3_section_2.html#//apple_ref/doc/uid/TP40000939-CH204-56313 113 | """ 114 | datastream = open(infilename, "r+b") 115 | 116 | # Get the top level atom index 117 | index = get_index(datastream) 118 | 119 | for atom, pos, size in index: 120 | if atom == "moov": 121 | moov_size = size 122 | datastream.seek(pos + 8) 123 | break 124 | else: 125 | print "Couldn't find moov!" 126 | raise SystemExit(1) 127 | 128 | degrees = set() 129 | 130 | for atom_type in find_atoms(moov_size - 8, datastream): 131 | #print atom_type + " found!" 132 | vf = datastream.read(4) 133 | version = struct.unpack(">Bxxx", vf)[0] 134 | flags = struct.unpack(">L", vf)[0] & 0x00ffffff 135 | if version == 1: 136 | if atom_type == "mvhd": 137 | datastream.read(28) 138 | elif atom_type == "tkhd": 139 | datastream.read(32) 140 | elif version == 0: 141 | if atom_type == "mvhd": 142 | datastream.read(16) 143 | elif atom_type == "tkhd": 144 | datastream.read(20) 145 | else: 146 | print "Unknown %s version: %d!" % (atom_type, version) 147 | raise SystemExit(1) 148 | 149 | datastream.read(16) 150 | 151 | matrix = list(struct.unpack(">9l", datastream.read(36))) 152 | 153 | if ('tkhd' == atom_type) and (set_degrees != None): 154 | radians = math.radians(set_degrees) 155 | cos_deg = int((1<<16)*math.cos(radians)) 156 | sin_deg = int((1<<16)*math.sin(radians)) 157 | value = struct.pack(">9l", cos_deg, sin_deg, 0, -sin_deg, cos_deg, 0, 0, 0, (1<<30)) 158 | datastream.seek(-36, 1) 159 | datastream.write(value) 160 | 161 | else: 162 | for x in range(9): 163 | if (x + 1) % 3: 164 | #print x, matrix[x] 165 | matrix[x] = float(matrix[x]) / (1 << 16) 166 | else: 167 | #print x, matrix[x] 168 | matrix[x] = float(matrix[x]) / (1 << 30) 169 | 170 | #print matrix 171 | 172 | 173 | #for row in [matrix[:3], matrix[3:6], matrix[6:]]: 174 | # print "\t".join([str(round(item, 1)) for item in row]) 175 | #print "" 176 | 177 | if atom_type in ["mvhd", "tkhd"]: 178 | deg = -math.degrees(math.asin(matrix[3])) % 360 179 | if not deg: 180 | deg = math.degrees(math.acos(matrix[0])) 181 | if deg: 182 | degrees.add(deg) 183 | 184 | if atom_type == "mvhd": 185 | datastream.read(28) 186 | elif atom_type == "tkhd": 187 | datastream.read(8) 188 | 189 | if len(degrees) == 0: 190 | return 0 191 | elif len(degrees) == 1: 192 | return degrees.pop() 193 | else: 194 | return -1 195 | 196 | if __name__ == "__main__": 197 | try: 198 | if 3==len(sys.argv): 199 | set_degrees = int(sys.argv[2]) 200 | # print "Forcing transformation matrix to %d degrees..." % set_degrees 201 | deg = get_set_rotation(sys.argv[1], set_degrees) 202 | else: 203 | deg = get_set_rotation(sys.argv[1]) 204 | 205 | if deg == -1: 206 | deg = 0 207 | 208 | print int(deg) 209 | 210 | except Exception, e: 211 | print e 212 | raise SystemExit(1) 213 | 214 | 215 | --------------------------------------------------------------------------------