]>
code.delx.au - transcoding/blob - video-transform
d30015263d210dc8cc694d983cf59b2d6cb6c4c4
3 from __future__
import division
14 DRY_RUN
= False # this will still delete caches
17 AUDIO_SAMPLE_RATE
= 48000
22 if re
.match("^[a-zA-Z0-9\-\\.,/@_:=]*$", arg
):
35 def fail(line_count
, msg
):
36 raise Exception(msg
+ " on line %d" % line_count
)
38 def convert_frame_to_sample(frame
):
39 return frame
* AUDIO_SAMPLE_RATE
/ VIDEO_FPS
42 print "$", " ".join(map(mkarg
, cmd
))
46 ret
= subprocess
.Popen(cmd
).wait()
48 print >>sys
.stderr
, "Failed on command", cmd
49 raise Exception("Command returned non-zero: " + str(ret
))
51 def read_file_contents(filename
):
54 data
= f
.read().strip()
60 def explode_video_to_png(source
):
61 image_cache_desc
= os
.path
.join(TMP_DIR
, "image_cache.txt")
62 image_cache_dir
= os
.path
.join(TMP_DIR
, "image_cache")
64 # Do nothing if the current cache is what we need
65 current_cache
= read_file_contents(image_cache_desc
)
66 if source
== current_cache
:
67 return image_cache_dir
70 if os
.path
.exists(image_cache_dir
):
71 ### print "Confirm removal of image cache:", current_cache
72 ### ok = raw_input("(Y/n) ")
74 ### print "Exiting..."
76 shutil
.rmtree(image_cache_dir
)
80 "-vo", "png:outdir=%s" % image_cache_dir
,
89 # Cache has been created, save the description
90 f
= open(image_cache_desc
, "w")
94 return image_cache_dir
97 def explode_video_to_wav(source
):
98 audio_cache_desc
= os
.path
.join(TMP_DIR
, "audio_cache.txt")
99 audio_cache_file
= os
.path
.join(TMP_DIR
, "audio_cache.wav")
101 # Do nothing if the current cache is what we need
102 if source
== read_file_contents(audio_cache_desc
):
103 return audio_cache_file
110 "-o", audio_cache_file
+ ".raw",
117 "-r", str(AUDIO_SAMPLE_RATE
), "-b", "16", "-e", "signed-integer",
118 audio_cache_file
+ ".raw",
123 # Cache has been created, save the description
124 f
= open(audio_cache_desc
, "w")
128 return audio_cache_file
130 def apply_audio_effects(source
, dest
, crop_start
, crop_end
, audio_normalize
):
137 cmd
+= ["gain", "-n"]
138 if crop_start
and crop_end
:
139 c
= convert_frame_to_sample
140 cmd
+= ["trim", "%ds" % c(crop_start
), "%ds" % c(crop_end
- crop_start
)]
143 def apply_single_image_effects(source_file
, dest_file
, color_matrix
):
147 "-color-matrix", color_matrix
,
152 def apply_image_effects(source_dir
, crop_start
, crop_end
, color_matrix
):
153 dest_dir
= os
.path
.join(TMP_DIR
, "image_processed")
154 if os
.path
.exists(dest_dir
):
155 shutil
.rmtree(dest_dir
)
160 while inframe
<= crop_end
:
161 source_file
= os
.path
.join(source_dir
, str(inframe
+1).zfill(8) + ".png")
162 dest_file
= os
.path
.join(dest_dir
, str(outframe
+1).zfill(8) + ".png")
164 apply_single_image_effects(source_file
, dest_file
, color_matrix
)
166 os
.link(source_file
, dest_file
)
172 def combine_audio_video(audio_file
, image_dir
, dest
):
175 "mf://%s/*.png" % image_dir
,
176 "-audiofile", audio_file
,
177 "-force-avi-aspect", ASPECT_RATIO
,
181 "-lavcopts", "vcodec=ffv1:ilme:ildct",
192 self
.crop_start
= None
194 self
.color_matrix
= None
195 self
.audio_normalize
= True
197 def set_source(self
, arg
):
198 self
.source
= os
.path
.join(SOURCE_DIR
, arg
)
200 def set_dest(self
, arg
):
201 self
.dest
= os
.path
.join(DEST_DIR
, arg
)
202 if not self
.dest
.endswith(".avi"):
205 def set_crop(self
, arg
):
206 a
, b
= arg
.split("-")
207 self
.crop_start
= int(a
)
208 self
.crop_end
= int(b
)
210 def set_colormatrix(self
, arg
):
211 [float(x
) for x
in arg
.split(" ") if x
] # check it's valid
212 self
.color_matrix
= arg
214 def set_whitecolor(self
, arg
):
217 r
= 0xff / int(color
[0:2], 16)
218 g
= 0xff / int(color
[2:4], 16)
219 b
= 0xff / int(color
[4:6], 16)
220 # don't change the brightness
221 avg
= (r
+ g
+ b
) / 3
228 brightness
= float(arg
[1])
232 self
.set_colormatrix("%.3f 0 0 0 %.3f 0 0 0 %.3f" % (r
, g
, b
))
234 def set_audionormalize(self
, arg
):
235 self
.audio_normalize
= int(arg
)
237 def validate(self
, line_count
, unique
):
238 if self
.dest
in unique
:
239 fail(line_count
, "Non-unique output file: " + self
.dest
)
240 if self
.source
is None:
241 fail(line_count
, "Missing source")
242 if self
.dest
is None:
243 fail(line_count
, "Missing dest")
244 if not os
.path
.isfile(self
.source
):
245 fail(line_count
, "Unable to find source: " + self
.source
)
248 return os
.path
.isfile(self
.dest
)
251 image_cache_dir
= explode_video_to_png(self
.source
)
252 image_dir
= apply_image_effects(image_cache_dir
, self
.crop_start
, self
.crop_end
, self
.color_matrix
)
254 audio_cache_file
= explode_video_to_wav(self
.source
)
255 audio_file
= os
.path
.join(TMP_DIR
, "audio_processed.wav")
256 apply_audio_effects(audio_cache_file
, audio_file
, self
.crop_start
, self
.crop_end
, self
.audio_normalize
)
258 combine_audio_video(audio_file
, image_dir
, self
.dest
+".tmp")
259 os
.rename(self
.dest
+".tmp", self
.dest
)
262 return "Job :: %s (%s)" % (self
.dest
, self
.source
)
276 job
.validate(count
, unique
)
278 print "Skipping", job
285 if line
.startswith("#"):
295 cmd
, arg
= line
.split(" ", 1)
296 f
= getattr(job
, "set_"+cmd
, None)
298 fail(count
, "Invalid command: " + cmd
)
307 # optimise image and audio cache usage, use the current cache first if it exists
308 current_image_cache
= read_file_contents(os
.path
.join(TMP_DIR
, "image_cache.txt"))
309 jobs
.sort(key
=lambda job
: (job
.source
if job
.source
!= current_image_cache
else "", job
.dest
))
311 print "\n\n\nStarted job:", job
, "\n\n"
314 if __name__
== "__main__":
317 SOURCE_DIR
= sys
.argv
[2]
318 DEST_DIR
= sys
.argv
[3]
319 TMP_DIR
= sys
.argv
[4]
321 print >>sys
.stderr
, "Usage: %s frames.txt source_dir dest_dir tmp_dir" % sys
.argv
[0]