11c922aad8bac33823492055b2facb306eef4657
[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, extension=None, 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         if extension == None:
121             ext = self.tr_extension(container, (v_codec is not None))
122         else:
123             ext = extension
124         if ext is not None:
125             self.output_file += '.' + ext
126
127         return self._transcode(self.tr_container(container), ext,
128                 self.tr_a_codec(a_codec), self.tr_v_codec(v_codec),
129                 a_bitrate, a_samplingrate, a_channels,
130                 v_bitrate, v_framerate, v_resolution, v_dar)
131
132     def _transcode(self, container, extension=None, a_codec=None, v_codec=None,
133             a_bitrate=None, a_samplingrate=None, a_channels=None,
134             v_bitrate=None, v_framerate=None, v_resolution=None, v_dar=None):
135         """
136         Called by transcode; must be overridden by a child class which
137         effectively transcodes the input file.
138
139         @return output file name
140         """
141         pass
142
143     def tr_container(self, name):
144         """ Translates container API name into external program identifier."""
145
146         if not self.containers.has_key(name) or self.containers[name] is None:
147             raise cis_exceptions.NotImplementedException("Container " + name \
148                     + "not implemented")
149
150         return self.containers[name]
151
152     def tr_extension(self, name, video=True):
153         """ Translates container API name into file extension."""
154
155         if video is True:
156             i = 1
157         else:
158             i = 0
159
160         if not self.extensions.has_key(name) or self.extensions[name] is None:
161             return None
162
163         return self.extensions[name][i]
164
165     def tr_a_codec(self, name):
166         """ Translates audio codec API name into external program identifier."""
167
168         if not self.a_codecs.has_key(name) or self.a_codecs[name] is None:
169             raise cis_exceptions.NotImplementedException("Audio Codec " + name \
170                     + "not implemented")
171
172         return self.a_codecs[name]
173
174     def tr_v_codec(self, name):
175         """ Translates video codec API name into external program identifier."""
176
177         if not self.v_codecs.has_key(name) or self.v_codecs[name] is None:
178             raise cis_exceptions.NotImplementedException("Video Codec " + name \
179                     + "not implemented")
180
181         return self.v_codecs[name]
182
183
184 class BaseThumbExtractor:
185     """
186     Abstraction of the API class for the thumbnail extraction program. 
187
188     Thumbnail extracted are in JPEG format.
189     """
190
191     prog_bin = None
192     input_file = None
193     dest_path = ''
194     name = None
195     
196     def __init__(self, input_file, name=None, prog_bin=None):
197         self.input_file = input_file
198         if prog_bin is not None:
199             self.prog_bin = prog_bin
200         
201         if name is None:
202             name = cis_util.get_name(input_file)
203
204         self.name = name
205
206     def extract_thumb(self, seek_pos, resolution="120x90", index=0):
207         """
208         Extracts a thumbnail from the video from a specified position
209         expressed in seconds (int/float).
210
211         index: an index appended to the image name in order to avoid
212         overwriting.
213         """
214         pass
215
216     def extract_random_thumb(self, resolution="120x90", index=0):
217         """
218         Extracts a thumbnail from the video from a random position.
219         """
220         duration = self.get_video_duration()
221         seek_pos = random.random() * duration
222         self.extract_thumb(seek_pos, resolution, index)
223
224     def extract_summary_thumbs(self, count, resolution="120x90"):
225         """
226         Extracts a series of thumbnails from a video by taking several
227         snapshots.
228
229         The snapshots are taken from equally spaced positions such that
230         `count` thumbs are extracted.
231         """
232         duration = self.get_video_duration()
233         interval = duration / (count + 1)
234         
235         n_thumbs_extracted = 0
236         seek_pos = interval
237         for index in range (0, count):
238             thumb_extracted = True
239             try:
240                 self.extract_thumb(seek_pos, resolution, n_thumbs_extracted)
241             except cis_exceptions.ThumbExtractionException as e:
242                 thumb_extracted = False
243
244             if thumb_extracted:
245                 n_thumbs_extracted += 1
246
247             seek_pos += interval
248
249         return n_thumbs_extracted
250
251     def get_output_file_name(self, index):
252         """ Returns the name required as output file name based on index. """
253         output_file_name = os.path.join(self.dest_path, self.name) \
254                 + '_t' + ("%02d" % index) + '.jpg'
255                 
256         if os.path.exists(output_file_name):
257             raise cis_exceptions.FileAlreadyExistsException( \
258                     'file "%s" already exists' % output_file_name)
259         
260         return output_file_name
261
262     def get_video_duration(self):
263         pass
264
265
266 class BaseFileTransferer:
267     """
268     Ensures file transfer from the Web Server to the CIS (here).
269     
270     Several implementations can be done by extending this class for
271     file transfer protocol such as FTP, SCP, RSYNC, HTTP etc.
272     """
273     
274     local_path = ''
275     remote_path = ''
276     
277     def __init__(self, local_path='', remote_path=''):
278         """ Initialize by setting local and remote paths for file transfer. """
279         self.local_path = local_path
280         self.remote_path = remote_path
281
282     def __del__(self):
283         self.close()
284
285     def get(self, files):
286         """
287         Transfers files locally from the Web Server.
288
289         files: a list of file name strings
290         """
291         pass
292
293     def put(self, files):
294         """
295         Transfers files from the Web Server locally.
296
297         files: a list of file name strings
298         """
299         pass
300
301     def close(self):
302         """
303         This method should be called when the instance is no longer required.
304
305         Class's destructor calls this method.
306         """
307         pass
308
309
310 class BaseAVInfo:
311     @staticmethod
312     def get_video_duration(input_file, formated=False):
313         """
314         Returns the number of seconds of a video (int/float) if formated is
315         False and a string for duration formated as [HH:]:mm:ss otherwise.
316         """
317         pass