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