cis notified web server of a job completion; upload form interface and validation...
[living-lab-site.git] / cis / api / ffmpeg.py
1 #!/usr/bin/env python
2
3
4 """
5 Classes derived from BaseTranscoder and BaseThumbExtractor for transcoding of
6 videos and thumbnail extraction from videos using FFmpeg CLI program.
7 """
8
9 import base
10 import cis_exceptions
11 import subprocess
12 import re
13 import os
14 import math
15
16 class FFmpegTranscoder(base.BaseTranscoder):
17     """
18     FFmpeg CLI API for video transcoding.
19     """
20
21     prog_bin = "ffmpeg"
22
23     log_file = 'log/FFmpegTranscoder.log'
24
25     containers = {
26         "avi": "avi",
27         "flv": "flv",
28         "mp4": "mp4",
29         "ogg": "ogg",
30         "webm": "webm",
31         "mpegts": "mpegts"
32     }
33     a_codecs = {
34         "mp3": "libmp3lame",
35         "vorbis": "libvorbis"
36     }
37     v_codecs = {
38         "h264": "libx264",
39         "theora": "libtheora",
40         "vp8": "libvpx"
41     }
42
43     def _transcode(self, container, a_codec=None, v_codec=None,
44             a_bitrate=None, a_samplingrate=None, a_channels=None,
45             v_bitrate=None, v_framerate=None, v_resolution=None, v_dar=None):
46
47         args = self.prog_bin + ' -y -i "' + self.input_file + '" -f ' + container
48         
49         # Audio
50         if a_codec != None:
51             args += ' -acodec ' + a_codec
52             if a_bitrate != None:
53                 args += ' -ab ' + str(a_bitrate)
54             if a_samplingrate != None:
55                 args += ' -ar ' + str(a_samplingrate)
56             if a_channels != None:
57                 args += ' -ac ' + str(a_channels)
58         
59         # Video
60         if v_codec != None:
61             args += ' -vcodec ' + v_codec
62             # Video codec specific options.
63             if v_codec == 'libx264':
64                 args += ' -vpre normal'
65             if v_bitrate != None:
66                 args += ' -b ' + str(v_bitrate)
67             if v_framerate != None:
68                 args += ' -r ' + str(v_framerate)
69             if v_resolution != None:
70                 args += ' -s ' + v_resolution
71             if v_dar != None:
72                 args += ' -aspect ' + v_dar
73         
74         # Output file.
75         args += ' "' + self.output_file + '"'
76             
77         # READ handler for process's output.
78         p = subprocess.Popen(args, shell=True, 
79                 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
80         pipe = p.stdout
81
82         # WRITE handler for logging.
83         log = open(self.log_file, 'w')
84         log.write(args + '\n')
85
86         while True:
87             line = pipe.readline()
88             if len(line) == 0:
89                 break
90             log.write(line)
91
92         exit_code = p.wait()
93         if exit_code > 0:
94             raise cis_exceptions.TranscodingException( \
95                     'FFmpeg exited with code ' + str(exit_code) + '.')
96
97         log.close()
98
99         return self.output_file
100
101
102 class FFmpegThumbExtractor(base.BaseThumbExtractor):
103     """
104     FFmpeg CLI API for video thumbnail extraction.
105     """
106
107     prog_bin = "ffmpeg"
108
109     log_file = 'log/FFmpegThumbExtractor.log'
110
111     def extract_thumb(self, seek_pos, resolution="120x90", index=0):
112         output_file = self.get_output_file_name(index)
113
114         args = self.prog_bin + ' -y -i "' + self.input_file \
115                 + '" -f rawvideo -vcodec mjpeg' + (' -ss ' + str(seek_pos)) \
116                 + " -vframes 1 -an -s " + resolution + ' "' \
117                 + output_file + '"'
118
119         # READ handler for process's output.
120         p = subprocess.Popen(args, shell=True,
121                 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
122         pipe = p.stdout
123         
124         # WRITE handler for logging.
125         log = open(self.log_file, 'w')
126         log.write(args + '\n')
127
128         while True:
129             line = pipe.readline()
130             if len(line) == 0:
131                 break
132             log.write(line)
133
134         exit_code = p.wait()
135         if exit_code > 0:
136             raise cis_exceptions.ThumbExtractionException( \
137                     'FFmpeg exited with code ' + str(exit_code) + '.')
138
139         # FFmpeg bug: when no key frame is found from seek_pos to the
140         # end of file an empty image file is created.
141         if os.path.getsize(output_file) == 0L:
142             os.unlink(output_file)
143             raise cis_exceptions.ThumbExtractionException( \
144                     'FFmpeg created an empty file.')
145
146     def get_video_duration(self):
147         return FFprobeAVInfo.get_video_duration(self.input_file)
148
149
150 class FFprobeAVInfo(base.BaseAVInfo):
151     
152     prog_bin = "ffprobe"
153
154     log_file = 'log/FFprobeAVInfo.log'
155
156     @staticmethod
157     def get_video_duration(input_file, formated=False):
158         args = FFprobeAVInfo.prog_bin + ' -show_format "' \
159                 + input_file + '"'
160
161         # READ handler for process's output.
162         p = subprocess.Popen(args, shell=True,
163                 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
164         pipe = p.stdout
165
166         # WRITE handler for logging.
167         log = open(FFprobeAVInfo.log_file, 'w')
168         log.write(args + '\n')
169
170         # Parse ffprobe's output.
171         while True:
172             line = pipe.readline()
173             if len(line) == 0:
174                 break
175             log.write(line)
176             
177             # Search for the line which contains duration information.
178             m = re.match(r"duration=([\d\.]+)", line)
179             if m is not None:
180                 seconds = float(m.group(1))
181                 if not formated:
182                     return seconds
183                 else:
184                     seconds = math.floor(seconds)
185                     minutes = math.floor(seconds / 60)
186                     seconds = seconds % 60
187                     if minutes >= 60:
188                         hours = math.floor(minutes / 60)
189                         minutes = minutes % 60
190                         
191                         return "%02d:%02d:%02d" % (hours, minutes, seconds)
192                     else:
193                         return "%02d:%02d" % (minutes, seconds)
194
195         exit_code = p.wait()
196         if exit_code > 0:
197             raise cis_exceptions.AVInfoException( \
198                     'ffprobe exited with code ' + str(exit_code) + '.')