FFmpeg API for thumbnail extraction and transcoding in CIS implemented
[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 cis_exceptions
8 import re
9 import cis_util
10 import random
11
12 class BaseTranscoder:
13     """
14     Abstraction of the API class for the transcoder program. 
15     """
16
17     prog_bin = None
18     input_file = None
19     output_file = None
20     dest_path = ''
21     name = None
22
23     # Recommended formats.
24     containers = {
25         "avi": None,
26         "flv": None,
27         "mp4": None,
28         "ogg": None,
29         "webm": None,
30         "mpegts": None
31     }
32     # File extensions by container. First value is for audio files and the
33     # second one is for (audio-)video files.
34     extensions = {
35         "avi": ["avi", "avi"],
36         "flv": ["flv", "flv"],
37         "mp4": ["mp4", "mp4"],
38         "ogg": ["oga", "ogv"],
39         "webm": ["webm", "webm"],
40         "mpegts": ["mts", "mts"]
41     }
42     a_codecs = {
43         "mp3": None,
44         "vorbis": None
45     }
46     v_codecs = {
47         "h264": None,
48         "theora": None,
49         "vp8": None
50     }
51
52     def __init__(self, input_file, name=None, prog_bin=None):
53         self.input_file = input_file
54         if prog_bin is not None:
55             self.prog_bin = prog_bin
56         
57         if name is None:
58             name = cis_util.get_name(input_file)
59
60         self.name = name
61
62     def transcode(self, container, a_codec=None, v_codec=None,
63             a_bitrate=None, a_samplingrate=None, a_channels=None,
64             v_bitrate=None, v_framerate=None, v_resolution=None, v_dar=None):
65         """
66         Transcodes the input file to an audio-video file.
67
68         container: possible values are listed in containers member as keys
69         a_codec: possible values are listed in a_codecs member as keys
70         v_codec: possible values are listed in v_codecs member as keys
71         a_bitrate: (numeric) audio bit rate
72         a_samplingrate: (numeric) audio sampling rate in Hz
73         a_channels: (numeric) number of audio channels
74         v_bitrate: (numeric) video bit rate
75         v_framerate: (numeric) number of frames per second for a video
76         v_resolution: (string) video image size as <width>x<height>
77         v_dar: video display aspect ratio as <den>x<num> or float
78         """
79
80         # Check parameters.
81         if a_codec is None and v_codec is None:
82             raise ValueError('No audio or video codec specified.')
83
84         if a_codec is not None and type(a_codec) is not str:
85             raise TypeError('Audio codec must be string.')
86
87         if v_codec is not None and type(v_codec) is not str:
88             raise TypeError('Video codec must be string.')
89
90         if a_samplingrate is not None and type(a_samplingrate) is not int:
91             raise TypeError('Audio sampling rate must be an integer.')
92
93         if a_channels is not None and type(a_channels) is not int:
94             raise TypeError('Audio channels parameter must be an integer.')
95
96         if v_framerate is not None and type(v_framerate) is not int:
97             raise TypeError('Video frate rate must be an integer.')
98
99         if v_resolution is not None \
100                 and re.match('[\d]+x[\d]+', v_resolution) is None:
101             raise ValueError('Video resolution must be a string like <width>x<height>.')
102
103         if v_dar is not None and (type(v_dar) is not float \
104                 and re.match('[\d]+:[\d]+', v_dar) is None):
105             raise ValueError('Video display aspect ratio must be a float or a string like <den>:<num>.')
106
107         self.output_file = self.dest_path + self.name
108         if v_resolution is not None:
109             self.output_file += '_'
110             self.output_file += v_resolution[(v_resolution.rindex('x')+1):]
111             self.output_file += 'p'
112         ext = self.tr_extension(container, (v_codec is not None))
113         if ext is not None:
114             self.output_file += '.' + ext
115
116         self._transcode(self.tr_container(container),
117                 self.tr_a_codec(a_codec), self.tr_v_codec(v_codec),
118                 a_bitrate, a_samplingrate, a_channels,
119                 v_bitrate, v_framerate, v_resolution, v_dar)
120
121     def _transcode(self, container, a_codec=None, v_codec=None,
122             a_bitrate=None, a_samplingrate=None, a_channels=None,
123             v_bitrate=None, v_framerate=None, v_resolution=None, v_dar=None):
124         """
125         Called by transcode; must be overridden by a child class which
126         effectively transcodes the input file.
127         """
128         pass
129
130     def tr_container(self, name):
131         """ Translates container API name into external program identifier."""
132
133         if not self.containers.has_key(name) or self.containers[name] is None:
134             raise cis_exceptions.NotImplementedException("Container " + name)
135
136         return self.containers[name]
137
138     def tr_extension(self, name, video=True):
139         """ Translates container API name into file extension."""
140
141         if video is True:
142             i = 1
143         else:
144             i = 0
145
146         if not self.extensions.has_key(name) or self.extensions[name] is None:
147             return None
148
149         return self.extensions[name][i]
150
151     def tr_a_codec(self, name):
152         """ Translates audio codec API name into external program identifier."""
153
154         if not self.a_codecs.has_key(name) or self.a_codecs[name] is None:
155             raise cis_exceptions.NotImplementedException("Audio Codec " + name)
156
157         return self.a_codecs[name]
158
159     def tr_v_codec(self, name):
160         """ Translates video codec API name into external program identifier."""
161
162         if not self.v_codecs.has_key(name) or self.v_codecs[name] is None:
163             raise cis_exceptions.NotImplementedException("Video Codec " + name)
164
165         return self.v_codecs[name]
166
167
168 class BaseThumbExtractor:
169     """
170     Abstraction of the API class for the thumbnail extraction program. 
171
172     Thumbnail extracted are in JPEG format.
173     """
174
175     prog_bin = None
176     input_file = None
177     dest_path = ''
178     name = None
179     
180     def __init__(self, input_file, name=None, prog_bin=None):
181         self.input_file = input_file
182         if prog_bin is not None:
183             self.prog_bin = prog_bin
184         
185         if name is None:
186             name = cis_util.get_name(input_file)
187
188         self.name = name
189
190     def extract_thumb(self, seek_pos, resolution="120x90", index=0):
191         """
192         Extracts a thumbnail from the video from a specified position
193         expressed in seconds (int/float).
194
195         index: an index appended to the image name in order to avoid
196         overwriting.
197         """
198         pass
199
200     def extract_random_thumb(self, resolution="120x90", index=0):
201         """
202         Extracts a thumbnail from the video from a random position.
203         """
204         duration = self.get_video_duration()
205         seek_pos = random.random() * duration
206         self.extract_thumb(seek_pos, resolution, index)
207
208     def extract_summary_thumbs(self, count, resolution="120x90"):
209         """
210         Extracts a series of thumbnails from a video by taking several
211         snapshots.
212
213         The snapshots are taken from equally spaced positions such that
214         `count` thumbs are extracted.
215         """
216         duration = self.get_video_duration()
217         interval = duration / (count + 1)
218         
219         n_thumbs_extracted = 0
220         seek_pos = interval
221         for index in range (0, count):
222             thumb_extracted = True
223             try:
224                 self.extract_thumb(seek_pos, resolution, index)
225             except cis_exceptions.ThumbExtractionException as e:
226                 thumb_extracted = False
227
228             if thumb_extracted:
229                 n_thumbs_extracted += 1
230
231             seek_pos += interval
232
233         return n_thumbs_extracted
234
235     def get_video_duration(self):
236         """
237         Returns the number of seconds of a video (int/float).
238         """
239         pass
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 = self.dest_path + self.name \
244                 + '_t' + ("%02d" % index) + '.jpg'
245         return output_file_name