cis notified web server of a job completion; upload form interface and validation...
[living-lab-site.git] / cis / api / base.py
1 #!/usr/bin/env python
2
3 """
4 Base classes for the external programs API.
5 """
6
7 import os
8 import re
9 import random
10
11 import cis_exceptions
12 import cis_util
13
14 class BaseTranscoder:
15     """
16     Abstraction of the API class for the transcoder program. 
17     """
18
19     prog_bin = None
20     input_file = None
21     output_file = None
22     dest_path = ''
23     name = None
24
25     # Recommended formats.
26     containers = {
27         "avi": None,
28         "flv": None,
29         "mp4": None,
30         "ogg": None,
31         "webm": None,
32         "mpegts": None
33     }
34     # File extensions by container. First value is for audio files and the
35     # second one is for (audio-)video files.
36     extensions = {
37         "avi": ["avi", "avi"],
38         "flv": ["flv", "flv"],
39         "mp4": ["mp4", "mp4"],
40         "ogg": ["oga", "ogv"],
41         "webm": ["webm", "webm"],
42         "mpegts": ["mts", "mts"]
43     }
44     a_codecs = {
45         "mp3": None,
46         "vorbis": None
47     }
48     v_codecs = {
49         "h264": None,
50         "theora": None,
51         "vp8": None
52     }
53
54     def __init__(self, input_file, name=None, prog_bin=None):
55         self.input_file = input_file
56         if prog_bin is not None:
57             self.prog_bin = prog_bin
58         
59         if name is None:
60             name = cis_util.get_name(input_file)
61
62         self.name = name
63
64     def transcode(self, container, a_codec=None, v_codec=None,
65             a_bitrate=None, a_samplingrate=None, a_channels=None,
66             v_bitrate=None, v_framerate=None, v_resolution=None, v_dar=None):
67         """
68         Transcodes the input file to an audio-video file.
69
70         @param container: possible values are listed in containers member 
71         as keys
72         @param a_codec possible values are listed in a_codecs member as keys
73         @param v_codec possible values are listed in v_codecs member as keys
74         @param a_bitrate (numeric) audio bit rate
75         @param a_samplingrate (numeric) audio sampling rate in Hz
76         @param a_channels (numeric) number of audio channels
77         @param v_bitrate (numeric) video bit rate
78         @param v_framerate (numeric) number of frames per second for a video
79         @param v_resolution (string) video image size as <width>x<height>
80         @param v_dar video display aspect ratio as <den>x<num> or float
81         @return output file name
82         """
83
84         # Check parameters.
85         if a_codec is None and v_codec is None:
86             raise ValueError('No audio or video codec specified.')
87
88         if a_codec is not None and type(a_codec) not in [str, unicode]:
89             raise TypeError('Audio codec must be string.')
90
91         if v_codec is not None and type(v_codec) not in [str, unicode]:
92             raise TypeError('Video codec must be string.')
93
94         if a_samplingrate is not None and type(a_samplingrate) is not int:
95             raise TypeError('Audio sampling rate must be an integer.')
96
97         if a_channels is not None and type(a_channels) is not int:
98             raise TypeError('Audio channels parameter must be an integer.')
99
100         if v_framerate is not None and type(v_framerate) is not int:
101             raise TypeError('Video frate rate must be an integer.')
102
103         if v_resolution is not None \
104                 and re.match('[\d]+x[\d]+', v_resolution) is None:
105             raise ValueError('Video resolution must be a string like <width>x<height>.')
106
107         if v_dar is not None and (type(v_dar) is not float \
108                 and re.match('[\d]+:[\d]+', v_dar) is None):
109             raise ValueError('Video display aspect ratio must be a float or a string like <den>:<num>.')
110
111         self.output_file = os.path.join(self.dest_path, self.name)
112         if os.path.exists(self.output_file):
113             raise cis_exceptions.FileAlreadyExistsException( \
114                     'file "%s" already exists' % self.output_file)
115         
116         if v_resolution is not None:
117             self.output_file += '_'
118             self.output_file += v_resolution[(v_resolution.rindex('x')+1):]
119             self.output_file += 'p'
120         ext = self.tr_extension(container, (v_codec is not None))
121         if ext is not None:
122             self.output_file += '.' + ext
123
124         return self._transcode(self.tr_container(container),
125                 self.tr_a_codec(a_codec), self.tr_v_codec(v_codec),
126                 a_bitrate, a_samplingrate, a_channels,
127                 v_bitrate, v_framerate, v_resolution, v_dar)
128
129     def _transcode(self, container, a_codec=None, v_codec=None,
130             a_bitrate=None, a_samplingrate=None, a_channels=None,
131             v_bitrate=None, v_framerate=None, v_resolution=None, v_dar=None):
132         """
133         Called by transcode; must be overridden by a child class which
134         effectively transcodes the input file.
135
136         @return output file name
137         """
138         pass
139
140     def tr_container(self, name):
141         """ Translates container API name into external program identifier."""
142
143         if not self.containers.has_key(name) or self.containers[name] is None:
144             raise cis_exceptions.NotImplementedException("Container " + name \
145                     + "not implemented")
146
147         return self.containers[name]
148
149     def tr_extension(self, name, video=True):
150         """ Translates container API name into file extension."""
151
152         if video is True:
153             i = 1
154         else:
155             i = 0
156
157         if not self.extensions.has_key(name) or self.extensions[name] is None:
158             return None
159
160         return self.extensions[name][i]
161
162     def tr_a_codec(self, name):
163         """ Translates audio codec API name into external program identifier."""
164
165         if not self.a_codecs.has_key(name) or self.a_codecs[name] is None:
166             raise cis_exceptions.NotImplementedException("Audio Codec " + name \
167                     + "not implemented")
168
169         return self.a_codecs[name]
170
171     def tr_v_codec(self, name):
172         """ Translates video codec API name into external program identifier."""
173
174         if not self.v_codecs.has_key(name) or self.v_codecs[name] is None:
175             raise cis_exceptions.NotImplementedException("Video Codec " + name \
176                     + "not implemented")
177
178         return self.v_codecs[name]
179
180
181 class BaseThumbExtractor:
182     """
183     Abstraction of the API class for the thumbnail extraction program. 
184
185     Thumbnail extracted are in JPEG format.
186     """
187
188     prog_bin = None
189     input_file = None
190     dest_path = ''
191     name = None
192     
193     def __init__(self, input_file, name=None, prog_bin=None):
194         self.input_file = input_file
195         if prog_bin is not None:
196             self.prog_bin = prog_bin
197         
198         if name is None:
199             name = cis_util.get_name(input_file)
200
201         self.name = name
202
203     def extract_thumb(self, seek_pos, resolution="120x90", index=0):
204         """
205         Extracts a thumbnail from the video from a specified position
206         expressed in seconds (int/float).
207
208         index: an index appended to the image name in order to avoid
209         overwriting.
210         """
211         pass
212
213     def extract_random_thumb(self, resolution="120x90", index=0):
214         """
215         Extracts a thumbnail from the video from a random position.
216         """
217         duration = self.get_video_duration()
218         seek_pos = random.random() * duration
219         self.extract_thumb(seek_pos, resolution, index)
220
221     def extract_summary_thumbs(self, count, resolution="120x90"):
222         """
223         Extracts a series of thumbnails from a video by taking several
224         snapshots.
225
226         The snapshots are taken from equally spaced positions such that
227         `count` thumbs are extracted.
228         """
229         duration = self.get_video_duration()
230         interval = duration / (count + 1)
231         
232         n_thumbs_extracted = 0
233         seek_pos = interval
234         for index in range (0, count):
235             thumb_extracted = True
236             try:
237                 self.extract_thumb(seek_pos, resolution, n_thumbs_extracted)
238             except cis_exceptions.ThumbExtractionException as e:
239                 thumb_extracted = False
240
241             if thumb_extracted:
242                 n_thumbs_extracted += 1
243
244             seek_pos += interval
245
246         return n_thumbs_extracted
247
248     def get_output_file_name(self, index):
249         """ Returns the name required as output file name based on index. """
250         output_file_name = os.path.join(self.dest_path, self.name) \
251                 + '_t' + ("%02d" % index) + '.jpg'
252                 
253         if os.path.exists(output_file_name):
254             raise cis_exceptions.FileAlreadyExistsException( \
255                     'file "%s" already exists' % output_file_name)
256         
257         return output_file_name
258
259     def get_video_duration(self):
260         pass
261
262
263 class BaseFileTransferer:
264     """
265     Ensures file transfer from the Web Server to the CIS (here).
266     
267     Several implementations can be done by extending this class for
268     file transfer protocol such as FTP, SCP, RSYNC, HTTP etc.
269     """
270     
271     local_path = ''
272     remote_path = ''
273     
274     def __init__(self, local_path='', remote_path=''):
275         """ Initialize by setting local and remote paths for file transfer. """
276         self.local_path = local_path
277         self.remote_path = remote_path
278
279     def __del__(self):
280         self.close()
281
282     def get(self, files):
283         """
284         Transfers files locally from the Web Server.
285
286         files: a list of file name strings
287         """
288         pass
289
290     def put(self, files):
291         """
292         Transfers files from the Web Server locally.
293
294         files: a list of file name strings
295         """
296         pass
297
298     def close(self):
299         """
300         This method should be called when the instance is no longer required.
301
302         Class's destructor calls this method.
303         """
304         pass
305
306
307 class BaseAVInfo:
308     @staticmethod
309     def get_video_duration(input_file, formated=False):
310         """
311         Returns the number of seconds of a video (int/float) if formated is
312         False and a string for duration formated as [HH:]:mm:ss otherwise.
313         """
314         pass