Commit | Line | Data |
---|---|---|
26729e02 | 1 | # GNU MediaGoblin -- federated, autonomous media hosting |
cf29e8a8 | 2 | # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. |
26729e02 JW |
3 | # |
4 | # This program is free software: you can redistribute it and/or modify | |
5 | # it under the terms of the GNU Affero General Public License as published by | |
6 | # the Free Software Foundation, either version 3 of the License, or | |
7 | # (at your option) any later version. | |
8 | # | |
9 | # This program is distributed in the hope that it will be useful, | |
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | # GNU Affero General Public License for more details. | |
13 | # | |
14 | # You should have received a copy of the GNU Affero General Public License | |
15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
16 | ||
a249b6d3 | 17 | from __future__ import division |
206ef749 JW |
18 | |
19 | import os | |
26729e02 JW |
20 | import sys |
21 | import logging | |
206ef749 | 22 | import urllib |
ff3136d0 JW |
23 | import multiprocessing |
24 | import gobject | |
9fb336fd JW |
25 | import pygst |
26 | pygst.require('0.10') | |
ff3136d0 JW |
27 | import gst |
28 | import struct | |
29 | import Image | |
a249b6d3 | 30 | |
ff3136d0 JW |
31 | from gst.extend import discoverer |
32 | ||
ab0d5b59 JW |
33 | _log = logging.getLogger(__name__) |
34 | ||
ff3136d0 | 35 | gobject.threads_init() |
26729e02 | 36 | |
64fd0462 | 37 | CPU_COUNT = 2 |
64fd0462 | 38 | |
315266b4 | 39 | try: |
ff3136d0 JW |
40 | CPU_COUNT = multiprocessing.cpu_count() |
41 | except NotImplementedError: | |
42 | _log.warning('multiprocessing.cpu_count not implemented') | |
206ef749 | 43 | |
ff3136d0 JW |
44 | os.putenv('GST_DEBUG_DUMP_DOT_DIR', '/tmp') |
45 | ||
46 | ||
47 | def pixbuf_to_pilbuf(buf): | |
48 | data = list() | |
49 | for i in range(0, len(buf), 3): | |
ab0d5b59 | 50 | r, g, b = struct.unpack('BBB', buf[i:i + 3]) |
ff3136d0 JW |
51 | data.append((r, g, b)) |
52 | ||
53 | return data | |
26729e02 | 54 | |
89d764cd | 55 | |
e4a1b6d2 JW |
56 | class VideoThumbnailerMarkII(object): |
57 | ''' | |
58 | Creates a thumbnail from a video file. Rewrite of VideoThumbnailer. | |
59 | ||
60 | Large parts of the functionality and overall architectue contained within | |
61 | this object is taken from Participatory Culture Foundation's | |
62 | `gst_extractor.Extractor` object last seen at | |
63 | https://github.com/pculture/miro/blob/master/tv/lib/frontends/widgets/gst/gst_extractor.py | |
64 | in the `miro` codebase. | |
65 | ||
66 | The `miro` codebase and the gst_extractor.py are licensed under the GNU | |
67 | General Public License v2 or later. | |
68 | ''' | |
69 | STATE_NULL = 0 | |
70 | STATE_HALTING = 1 | |
71 | STATE_PROCESSING = 2 | |
72 | STATE_PROCESSING_THUMBNAIL = 3 | |
73 | ||
74 | def __init__(self, source_path, dest_path, width=None, height=None, | |
75 | position_callback=None): | |
76 | self.state = self.STATE_NULL | |
77 | ||
78 | self.has_reached_playbin_pause = False | |
79 | ||
80 | self.thumbnail_pipeline = None | |
81 | ||
82 | self.permission_to_take_picture = False | |
83 | ||
84 | self.buffer_probes = {} | |
85 | ||
86 | self.errors = [] | |
87 | ||
88 | self.source_path = os.path.abspath(source_path) | |
89 | self.dest_path = os.path.abspath(dest_path) | |
90 | ||
91 | self.width = width | |
92 | self.height = height | |
93 | self.position_callback = position_callback \ | |
94 | or self.wadsworth_position_callback | |
95 | ||
96 | self.mainloop = gobject.MainLoop() | |
97 | ||
98 | self.playbin = gst.element_factory_make('playbin') | |
99 | ||
100 | self.videosink = gst.element_factory_make('fakesink', 'videosink') | |
101 | self.audiosink = gst.element_factory_make('fakesink', 'audiosink') | |
102 | ||
103 | self.playbin.set_property('video-sink', self.videosink) | |
104 | self.playbin.set_property('audio-sink', self.audiosink) | |
105 | ||
106 | self.playbin_message_bus = self.playbin.get_bus() | |
107 | ||
108 | self.playbin_message_bus.add_signal_watch() | |
109 | self.playbin_bus_watch_id = self.playbin_message_bus.connect( | |
110 | 'message', | |
111 | self.on_playbin_message) | |
112 | ||
113 | self.playbin.set_property( | |
114 | 'uri', | |
115 | 'file:{0}'.format( | |
116 | urllib.pathname2url(self.source_path))) | |
117 | ||
118 | self.playbin.set_state(gst.STATE_PAUSED) | |
119 | ||
120 | try: | |
121 | self.run() | |
122 | except Exception as exc: | |
123 | _log.critical( | |
b06ea4ab JW |
124 | 'Exception "{0}" caught, shutting down mainloop and re-raising'\ |
125 | .format(exc)) | |
e4a1b6d2 JW |
126 | self.disconnect() |
127 | raise | |
128 | ||
129 | def wadsworth_position_callback(self, duration, gst): | |
130 | return self.duration / 100 * 30 | |
131 | ||
132 | def run(self): | |
133 | self.mainloop.run() | |
134 | ||
135 | def on_playbin_message(self, message_bus, message): | |
b06ea4ab JW |
136 | # Silenced to prevent clobbering of output |
137 | #_log.debug('playbin message: {0}'.format(message)) | |
e4a1b6d2 JW |
138 | |
139 | if message.type == gst.MESSAGE_ERROR: | |
140 | _log.error('playbin error: {0}'.format(message)) | |
141 | gobject.idle_add(self.on_playbin_error) | |
142 | ||
143 | if message.type == gst.MESSAGE_STATE_CHANGED: | |
144 | prev_state, cur_state, pending_state = \ | |
145 | message.parse_state_changed() | |
146 | ||
147 | _log.debug('playbin state changed: \nprev: {0}\ncur: {1}\n \ | |
148 | pending: {2}'.format( | |
149 | prev_state, | |
150 | cur_state, | |
151 | pending_state)) | |
152 | ||
153 | if cur_state == gst.STATE_PAUSED: | |
154 | if message.src == self.playbin: | |
155 | _log.info('playbin ready') | |
156 | gobject.idle_add(self.on_playbin_paused) | |
157 | ||
158 | def on_playbin_paused(self): | |
159 | if self.has_reached_playbin_pause: | |
b06ea4ab | 160 | _log.warn('Has already reached on_playbin_paused. Aborting \ |
e4a1b6d2 JW |
161 | without doing anything this time.') |
162 | return False | |
163 | ||
164 | self.has_reached_playbin_pause = True | |
165 | ||
b06ea4ab | 166 | # XXX: Why is this even needed at this point? |
e4a1b6d2 JW |
167 | current_video = self.playbin.get_property('current-video') |
168 | ||
169 | if not current_video: | |
b06ea4ab | 170 | _log.critical('Could not get any video data \ |
e4a1b6d2 | 171 | from playbin') |
b06ea4ab JW |
172 | else: |
173 | _log.info('Got video data from playbin') | |
e4a1b6d2 JW |
174 | |
175 | self.duration = self.get_duration(self.playbin) | |
176 | self.permission_to_take_picture = True | |
177 | self.buffer_probes = {} | |
178 | ||
179 | pipeline = ''.join([ | |
180 | 'filesrc location="%s" ! decodebin ! ' % self.source_path, | |
181 | 'ffmpegcolorspace ! videoscale ! ', | |
182 | 'video/x-raw-rgb,depth=24,bpp=24,pixel-aspect-ratio=1/1', | |
183 | ',width={0}'.format(self.width) if self.width else '', | |
184 | ',height={0}'.format(self.height) if self.height else '', | |
185 | ' ! ', | |
186 | 'fakesink signal-handoffs=True']) | |
187 | ||
188 | _log.debug('thumbnail_pipeline: {0}'.format(pipeline)) | |
189 | ||
190 | self.thumbnail_pipeline = gst.parse_launch(pipeline) | |
191 | self.thumbnail_message_bus = self.thumbnail_pipeline.get_bus() | |
192 | self.thumbnail_message_bus.add_signal_watch() | |
193 | self.thumbnail_bus_watch_id = self.thumbnail_message_bus.connect( | |
194 | 'message', | |
195 | self.on_thumbnail_message) | |
196 | ||
197 | self.thumbnail_pipeline.set_state(gst.STATE_PAUSED) | |
198 | ||
199 | gobject.timeout_add(3000, self.on_gobject_timeout) | |
200 | ||
201 | return False | |
202 | ||
203 | def on_thumbnail_message(self, message_bus, message): | |
b06ea4ab JW |
204 | # This is silenced to prevent clobbering of the terminal window |
205 | #_log.debug('thumbnail message: {0}'.format(message)) | |
e4a1b6d2 JW |
206 | |
207 | if message.type == gst.MESSAGE_ERROR: | |
34c35c8c | 208 | _log.error('thumbnail error: {0}'.format(message.parse_error())) |
209 | gobject.idle_add(self.on_thumbnail_error, message) | |
e4a1b6d2 JW |
210 | |
211 | if message.type == gst.MESSAGE_STATE_CHANGED: | |
212 | prev_state, cur_state, pending_state = \ | |
213 | message.parse_state_changed() | |
214 | ||
215 | _log.debug('thumbnail state changed: \nprev: {0}\ncur: {1}\n \ | |
216 | pending: {2}'.format( | |
217 | prev_state, | |
218 | cur_state, | |
219 | pending_state)) | |
220 | ||
b06ea4ab JW |
221 | if cur_state == gst.STATE_PAUSED and \ |
222 | not self.state == self.STATE_PROCESSING_THUMBNAIL: | |
e4a1b6d2 JW |
223 | # Find the fakesink sink pad and attach the on_buffer_probe |
224 | # handler to it. | |
e4a1b6d2 JW |
225 | seek_amount = self.position_callback(self.duration, gst) |
226 | ||
227 | seek_result = self.thumbnail_pipeline.seek( | |
228 | 1.0, | |
229 | gst.FORMAT_TIME, | |
230 | gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_ACCURATE, | |
231 | gst.SEEK_TYPE_SET, | |
232 | seek_amount, | |
233 | gst.SEEK_TYPE_NONE, | |
234 | 0) | |
235 | ||
236 | if not seek_result: | |
b06ea4ab JW |
237 | _log.info('Could not seek.') |
238 | else: | |
239 | _log.info('Seek successful, attaching buffer probe') | |
240 | self.state = self.STATE_PROCESSING_THUMBNAIL | |
241 | for sink in self.thumbnail_pipeline.sinks(): | |
242 | sink_name = sink.get_name() | |
243 | sink_factory_name = sink.get_factory().get_name() | |
244 | ||
245 | if sink_factory_name == 'fakesink': | |
246 | sink_pad = sink.get_pad('sink') | |
247 | ||
248 | self.buffer_probes[sink_name] = sink_pad\ | |
249 | .add_buffer_probe( | |
250 | self.on_pad_buffer_probe, | |
251 | sink_name) | |
252 | ||
253 | _log.info('Attached buffer probes: {0}'.format( | |
254 | self.buffer_probes)) | |
255 | ||
256 | break | |
257 | ||
e4a1b6d2 JW |
258 | |
259 | elif self.state == self.STATE_PROCESSING_THUMBNAIL: | |
b06ea4ab | 260 | _log.info('Already processing thumbnail') |
e4a1b6d2 JW |
261 | |
262 | def on_pad_buffer_probe(self, *args): | |
263 | _log.debug('buffer probe handler: {0}'.format(args)) | |
264 | gobject.idle_add(lambda: self.take_snapshot(*args)) | |
265 | ||
266 | def take_snapshot(self, pad, buff, name): | |
267 | if self.state == self.STATE_HALTING: | |
268 | _log.debug('Pipeline is halting, will not take snapshot') | |
269 | return False | |
270 | ||
271 | _log.info('Taking snapshot! ({0})'.format( | |
272 | (pad, buff, name))) | |
273 | try: | |
274 | caps = buff.caps | |
275 | if caps is None: | |
276 | _log.error('No buffer caps present /take_snapshot') | |
277 | self.disconnect() | |
278 | ||
279 | _log.debug('caps: {0}'.format(caps)) | |
280 | ||
281 | filters = caps[0] | |
282 | width = filters['width'] | |
283 | height = filters['height'] | |
284 | ||
285 | im = Image.new('RGB', (width, height)) | |
286 | ||
287 | data = pixbuf_to_pilbuf(buff.data) | |
288 | ||
289 | im.putdata(data) | |
290 | ||
291 | im.save(self.dest_path) | |
292 | ||
293 | _log.info('Saved snapshot!') | |
294 | ||
295 | self.disconnect() | |
296 | ||
297 | except gst.QueryError as exc: | |
298 | _log.error('take_snapshot - QueryError: {0}'.format(exc)) | |
299 | ||
300 | return False | |
301 | ||
34c35c8c | 302 | def on_thumbnail_error(self, message): |
8d355df6 JW |
303 | scaling_failed = False |
304 | ||
305 | if 'Error calculating the output scaled size - integer overflow' \ | |
306 | in message.parse_error()[1]: | |
307 | # GStreamer videoscale sometimes fails to calculate the dimensions | |
308 | # given only one of the destination dimensions and the source | |
309 | # dimensions. This is a workaround in case videoscale returns an | |
310 | # error that indicates this has happened. | |
311 | scaling_failed = True | |
312 | _log.error('Thumbnailing failed because of videoscale integer' | |
313 | ' overflow. Will retry with fallback.') | |
314 | else: | |
315 | _log.error('Thumbnailing failed: {0}'.format(message.parse_error())) | |
316 | ||
317 | # Kill the current mainloop | |
e4a1b6d2 JW |
318 | self.disconnect() |
319 | ||
8d355df6 JW |
320 | if scaling_failed: |
321 | # Manually scale the destination dimensions | |
322 | _log.info('Retrying with manually set sizes...') | |
323 | ||
34c35c8c | 324 | info = VideoTranscoder().discover(self.source_path) |
8d355df6 | 325 | |
34c35c8c | 326 | h = info['videoheight'] |
327 | w = info['videowidth'] | |
328 | ratio = 180 / int(w) | |
329 | h = int(h * ratio) | |
8d355df6 | 330 | |
34c35c8c | 331 | self.__init__(self.source_path, self.dest_path, 180, h) |
e4a1b6d2 JW |
332 | |
333 | def disconnect(self): | |
334 | self.state = self.STATE_HALTING | |
335 | ||
336 | if self.playbin is not None: | |
337 | self.playbin.set_state(gst.STATE_NULL) | |
338 | ||
339 | for sink in self.playbin.sinks(): | |
340 | sink_name = sink.get_name() | |
341 | sink_factory_name = sink.get_factory().get_name() | |
342 | ||
343 | if sink_factory_name == 'fakesink': | |
344 | sink_pad = sink.get_pad('sink') | |
345 | sink_pad.remove_buffer_probe(self.buffer_probes[sink_name]) | |
346 | del self.buffer_probes[sink_name] | |
347 | ||
348 | self.playbin = None | |
349 | ||
350 | if self.thumbnail_pipeline is not None: | |
351 | self.thumbnail_pipeline.set_state(gst.STATE_NULL) | |
352 | self.thumbnail_pipeline = None | |
353 | ||
354 | if self.playbin_message_bus is not None: | |
355 | self.playbin_message_bus.disconnect(self.playbin_bus_watch_id) | |
356 | self.playbin_message_bus = None | |
357 | ||
358 | self.halt() | |
359 | ||
360 | def halt(self): | |
361 | gobject.idle_add(self.mainloop.quit) | |
362 | ||
363 | def on_gobject_timeout(self): | |
364 | _log.critical('Reached gobject timeout') | |
365 | self.disconnect() | |
366 | ||
367 | def get_duration(self, pipeline, attempt=1): | |
368 | if attempt == 5: | |
369 | _log.critical('Pipeline duration query retry limit reached.') | |
370 | return 0 | |
371 | ||
372 | try: | |
373 | return pipeline.query_duration(gst.FORMAT_TIME)[0] | |
374 | except gst.QueryError as exc: | |
375 | _log.error('Could not get duration on attempt {0}: {1}'.format( | |
376 | attempt, | |
377 | exc)) | |
378 | return self.get_duration(pipeline, attempt + 1) | |
379 | ||
380 | ||
b06ea4ab | 381 | class VideoTranscoder(object): |
a249b6d3 JW |
382 | ''' |
383 | Video transcoder | |
384 | ||
e9c1b938 JW |
385 | Transcodes the SRC video file to a VP8 WebM video file at DST |
386 | ||
206ef749 JW |
387 | - Does the same thing as VideoThumbnailer, but produces a WebM vp8 |
388 | and vorbis video file. | |
389 | - The VideoTranscoder exceeds the VideoThumbnailer in the way | |
390 | that it was refined afterwards and therefore is done more | |
391 | correctly. | |
a249b6d3 | 392 | ''' |
10085b77 | 393 | def __init__(self): |
a249b6d3 | 394 | _log.info('Initializing VideoTranscoder...') |
0efc4e4d | 395 | self.progress_percentage = None |
a249b6d3 | 396 | self.loop = gobject.MainLoop() |
10085b77 JW |
397 | |
398 | def transcode(self, src, dst, **kwargs): | |
399 | ''' | |
400 | Transcode a video file into a 'medium'-sized version. | |
401 | ''' | |
a249b6d3 JW |
402 | self.source_path = src |
403 | self.destination_path = dst | |
404 | ||
196a5181 JW |
405 | # vp8enc options |
406 | self.destination_dimensions = kwargs.get('dimensions', (640, 640)) | |
407 | self.vp8_quality = kwargs.get('vp8_quality', 8) | |
408 | # Number of threads used by vp8enc: | |
409 | # number of real cores - 1 as per recommendation on | |
410 | # <http://www.webmproject.org/tools/encoder-parameters/#6-multi-threaded-encode-and-decode> | |
411 | self.vp8_threads = kwargs.get('vp8_threads', CPU_COUNT - 1) | |
412 | ||
413 | # 0 means auto-detect, but dict.get() only falls back to CPU_COUNT | |
414 | # if value is None, this will correct our incompatibility with | |
415 | # dict.get() | |
416 | # This will also correct cases where there's only 1 CPU core, see | |
417 | # original self.vp8_threads assignment above. | |
418 | if self.vp8_threads == 0: | |
419 | self.vp8_threads = CPU_COUNT | |
420 | ||
421 | # vorbisenc options | |
422 | self.vorbis_quality = kwargs.get('vorbis_quality', 0.3) | |
423 | ||
206ef749 | 424 | self._progress_callback = kwargs.get('progress_callback') or None |
a249b6d3 JW |
425 | |
426 | if not type(self.destination_dimensions) == tuple: | |
427 | raise Exception('dimensions must be tuple: (width, height)') | |
428 | ||
429 | self._setup() | |
430 | self._run() | |
431 | ||
5c754fda | 432 | # XXX: This could be a static method. |
10085b77 JW |
433 | def discover(self, src): |
434 | ''' | |
435 | Discover properties about a media file | |
436 | ''' | |
437 | _log.info('Discovering {0}'.format(src)) | |
438 | ||
439 | self.source_path = src | |
440 | self._setup_discover(discovered_callback=self.__on_discovered) | |
441 | ||
442 | self.discoverer.discover() | |
443 | ||
444 | self.loop.run() | |
445 | ||
4f4f2531 JW |
446 | if hasattr(self, '_discovered_data'): |
447 | return self._discovered_data.__dict__ | |
448 | else: | |
449 | return None | |
10085b77 JW |
450 | |
451 | def __on_discovered(self, data, is_media): | |
4f4f2531 | 452 | _log.debug('Discovered: {0}'.format(data)) |
10085b77 JW |
453 | if not is_media: |
454 | self.__stop() | |
455 | raise Exception('Could not discover {0}'.format(self.source_path)) | |
456 | ||
457 | self._discovered_data = data | |
458 | ||
459 | self.__stop_mainloop() | |
460 | ||
a249b6d3 | 461 | def _setup(self): |
a249b6d3 | 462 | self._setup_discover() |
206ef749 | 463 | self._setup_pipeline() |
a249b6d3 JW |
464 | |
465 | def _run(self): | |
466 | _log.info('Discovering...') | |
467 | self.discoverer.discover() | |
468 | _log.info('Done') | |
469 | ||
470 | _log.debug('Initializing MainLoop()') | |
471 | self.loop.run() | |
472 | ||
10085b77 | 473 | def _setup_discover(self, **kw): |
206ef749 | 474 | _log.debug('Setting up discoverer') |
a249b6d3 JW |
475 | self.discoverer = discoverer.Discoverer(self.source_path) |
476 | ||
477 | # Connect self.__discovered to the 'discovered' event | |
10085b77 JW |
478 | self.discoverer.connect( |
479 | 'discovered', | |
480 | kw.get('discovered_callback', self.__discovered)) | |
a249b6d3 JW |
481 | |
482 | def __discovered(self, data, is_media): | |
483 | ''' | |
484 | Callback for media discoverer. | |
485 | ''' | |
486 | if not is_media: | |
487 | self.__stop() | |
488 | raise Exception('Could not discover {0}'.format(self.source_path)) | |
489 | ||
206ef749 | 490 | _log.debug('__discovered, data: {0}'.format(data.__dict__)) |
a249b6d3 JW |
491 | |
492 | self.data = data | |
493 | ||
206ef749 JW |
494 | # Launch things that should be done after discovery |
495 | self._link_elements() | |
496 | self.__setup_videoscale_capsfilter() | |
e9c1b938 | 497 | |
a249b6d3 JW |
498 | # Tell the transcoding pipeline to start running |
499 | self.pipeline.set_state(gst.STATE_PLAYING) | |
500 | _log.info('Transcoding...') | |
501 | ||
206ef749 JW |
502 | def _setup_pipeline(self): |
503 | _log.debug('Setting up transcoding pipeline') | |
504 | # Create the pipeline bin. | |
a249b6d3 JW |
505 | self.pipeline = gst.Pipeline('VideoTranscoderPipeline') |
506 | ||
206ef749 JW |
507 | # Create all GStreamer elements, starting with |
508 | # filesrc & decoder | |
a249b6d3 JW |
509 | self.filesrc = gst.element_factory_make('filesrc', 'filesrc') |
510 | self.filesrc.set_property('location', self.source_path) | |
511 | self.pipeline.add(self.filesrc) | |
512 | ||
513 | self.decoder = gst.element_factory_make('decodebin2', 'decoder') | |
a249b6d3 JW |
514 | self.decoder.connect('new-decoded-pad', self._on_dynamic_pad) |
515 | self.pipeline.add(self.decoder) | |
516 | ||
206ef749 | 517 | # Video elements |
b33701b8 JW |
518 | self.videoqueue = gst.element_factory_make('queue', 'videoqueue') |
519 | self.pipeline.add(self.videoqueue) | |
520 | ||
64fd0462 JW |
521 | self.videorate = gst.element_factory_make('videorate', 'videorate') |
522 | self.pipeline.add(self.videorate) | |
523 | ||
206ef749 JW |
524 | self.ffmpegcolorspace = gst.element_factory_make( |
525 | 'ffmpegcolorspace', 'ffmpegcolorspace') | |
a249b6d3 | 526 | self.pipeline.add(self.ffmpegcolorspace) |
c56d4b55 | 527 | |
b33701b8 JW |
528 | self.videoscale = gst.element_factory_make('ffvideoscale', 'videoscale') |
529 | #self.videoscale.set_property('method', 2) # I'm not sure this works | |
530 | #self.videoscale.set_property('add-borders', 0) | |
a249b6d3 JW |
531 | self.pipeline.add(self.videoscale) |
532 | ||
533 | self.capsfilter = gst.element_factory_make('capsfilter', 'capsfilter') | |
534 | self.pipeline.add(self.capsfilter) | |
535 | ||
536 | self.vp8enc = gst.element_factory_make('vp8enc', 'vp8enc') | |
196a5181 JW |
537 | self.vp8enc.set_property('quality', self.vp8_quality) |
538 | self.vp8enc.set_property('threads', self.vp8_threads) | |
539 | self.vp8enc.set_property('max-latency', 25) | |
e9c1b938 JW |
540 | self.pipeline.add(self.vp8enc) |
541 | ||
206ef749 | 542 | # Audio elements |
b33701b8 JW |
543 | self.audioqueue = gst.element_factory_make('queue', 'audioqueue') |
544 | self.pipeline.add(self.audioqueue) | |
545 | ||
64fd0462 | 546 | self.audiorate = gst.element_factory_make('audiorate', 'audiorate') |
359781f0 | 547 | self.audiorate.set_property('tolerance', 80000000) |
64fd0462 JW |
548 | self.pipeline.add(self.audiorate) |
549 | ||
e9c1b938 JW |
550 | self.audioconvert = gst.element_factory_make('audioconvert', 'audioconvert') |
551 | self.pipeline.add(self.audioconvert) | |
552 | ||
5c754fda JW |
553 | self.audiocapsfilter = gst.element_factory_make('capsfilter', |
554 | 'audiocapsfilter') | |
64fd0462 JW |
555 | audiocaps = ['audio/x-raw-float'] |
556 | self.audiocapsfilter.set_property( | |
557 | 'caps', | |
558 | gst.caps_from_string( | |
559 | ','.join(audiocaps))) | |
560 | self.pipeline.add(self.audiocapsfilter) | |
561 | ||
e9c1b938 | 562 | self.vorbisenc = gst.element_factory_make('vorbisenc', 'vorbisenc') |
196a5181 | 563 | self.vorbisenc.set_property('quality', self.vorbis_quality) |
e9c1b938 JW |
564 | self.pipeline.add(self.vorbisenc) |
565 | ||
206ef749 | 566 | # WebMmux & filesink |
a249b6d3 JW |
567 | self.webmmux = gst.element_factory_make('webmmux', 'webmmux') |
568 | self.pipeline.add(self.webmmux) | |
569 | ||
570 | self.filesink = gst.element_factory_make('filesink', 'filesink') | |
e9c1b938 JW |
571 | self.filesink.set_property('location', self.destination_path) |
572 | self.pipeline.add(self.filesink) | |
a249b6d3 | 573 | |
206ef749 JW |
574 | # Progressreport |
575 | self.progressreport = gst.element_factory_make( | |
576 | 'progressreport', 'progressreport') | |
577 | # Update every second | |
578 | self.progressreport.set_property('update-freq', 1) | |
579 | self.progressreport.set_property('silent', True) | |
580 | self.pipeline.add(self.progressreport) | |
581 | ||
582 | def _link_elements(self): | |
583 | ''' | |
584 | Link all the elements | |
585 | ||
586 | This code depends on data from the discoverer and is called | |
587 | from __discovered | |
588 | ''' | |
589 | _log.debug('linking elements') | |
590 | # Link the filesrc element to the decoder. The decoder then emits | |
591 | # 'new-decoded-pad' which links decoded src pads to either a video | |
592 | # or audio sink | |
a249b6d3 | 593 | self.filesrc.link(self.decoder) |
206ef749 | 594 | |
c875bb74 JW |
595 | # Link all the video elements in a row to webmmux |
596 | gst.element_link_many( | |
597 | self.videoqueue, | |
598 | self.videorate, | |
599 | self.ffmpegcolorspace, | |
600 | self.videoscale, | |
601 | self.capsfilter, | |
602 | self.vp8enc, | |
603 | self.webmmux) | |
e9c1b938 | 604 | |
206ef749 | 605 | if self.data.is_audio: |
c875bb74 JW |
606 | # Link all the audio elements in a row to webmux |
607 | gst.element_link_many( | |
608 | self.audioqueue, | |
609 | self.audiorate, | |
610 | self.audioconvert, | |
611 | self.audiocapsfilter, | |
612 | self.vorbisenc, | |
613 | self.webmmux) | |
614 | ||
615 | gst.element_link_many( | |
616 | self.webmmux, | |
617 | self.progressreport, | |
618 | self.filesink) | |
a249b6d3 | 619 | |
206ef749 | 620 | # Setup the message bus and connect _on_message to the pipeline |
a249b6d3 JW |
621 | self._setup_bus() |
622 | ||
623 | def _on_dynamic_pad(self, dbin, pad, islast): | |
624 | ''' | |
625 | Callback called when ``decodebin2`` has a pad that we can connect to | |
626 | ''' | |
206ef749 JW |
627 | # Intersect the capabilities of the video sink and the pad src |
628 | # Then check if they have no common capabilities. | |
e9c1b938 JW |
629 | if self.ffmpegcolorspace.get_pad_template('sink')\ |
630 | .get_caps().intersect(pad.get_caps()).is_empty(): | |
206ef749 | 631 | # It is NOT a video src pad. |
b33701b8 | 632 | pad.link(self.audioqueue.get_pad('sink')) |
e9c1b938 | 633 | else: |
206ef749 | 634 | # It IS a video src pad. |
b33701b8 | 635 | pad.link(self.videoqueue.get_pad('sink')) |
a249b6d3 JW |
636 | |
637 | def _setup_bus(self): | |
638 | self.bus = self.pipeline.get_bus() | |
639 | self.bus.add_signal_watch() | |
640 | self.bus.connect('message', self._on_message) | |
641 | ||
e9c1b938 | 642 | def __setup_videoscale_capsfilter(self): |
206ef749 JW |
643 | ''' |
644 | Sets up the output format (width, height) for the video | |
645 | ''' | |
64fd0462 | 646 | caps = ['video/x-raw-yuv', 'pixel-aspect-ratio=1/1', 'framerate=30/1'] |
a249b6d3 JW |
647 | |
648 | if self.data.videoheight > self.data.videowidth: | |
e9c1b938 JW |
649 | # Whoa! We have ourselves a portrait video! |
650 | caps.append('height={0}'.format( | |
651 | self.destination_dimensions[1])) | |
a249b6d3 | 652 | else: |
e9c1b938 JW |
653 | # It's a landscape, phew, how normal. |
654 | caps.append('width={0}'.format( | |
655 | self.destination_dimensions[0])) | |
a249b6d3 | 656 | |
e9c1b938 JW |
657 | self.capsfilter.set_property( |
658 | 'caps', | |
659 | gst.caps_from_string( | |
64fd0462 | 660 | ','.join(caps))) |
a249b6d3 JW |
661 | |
662 | def _on_message(self, bus, message): | |
206ef749 | 663 | _log.debug((bus, message, message.type)) |
a249b6d3 JW |
664 | |
665 | t = message.type | |
666 | ||
c56d4b55 | 667 | if message.type == gst.MESSAGE_EOS: |
e9c1b938 | 668 | self._discover_dst_and_stop() |
a249b6d3 | 669 | _log.info('Done') |
206ef749 | 670 | |
c56d4b55 | 671 | elif message.type == gst.MESSAGE_ELEMENT: |
206ef749 | 672 | if message.structure.get_name() == 'progress': |
8e5f9746 | 673 | data = dict(message.structure) |
0efc4e4d SS |
674 | # Update progress state if it has changed |
675 | if self.progress_percentage != data.get('percent'): | |
676 | self.progress_percentage = data.get('percent') | |
677 | if self._progress_callback: | |
678 | self._progress_callback(data.get('percent')) | |
679 | ||
680 | _log.info('{percent}% done...'.format( | |
681 | percent=data.get('percent'))) | |
206ef749 JW |
682 | _log.debug(data) |
683 | ||
a249b6d3 JW |
684 | elif t == gst.MESSAGE_ERROR: |
685 | _log.error((bus, message)) | |
686 | self.__stop() | |
687 | ||
e9c1b938 JW |
688 | def _discover_dst_and_stop(self): |
689 | self.dst_discoverer = discoverer.Discoverer(self.destination_path) | |
690 | ||
691 | self.dst_discoverer.connect('discovered', self.__dst_discovered) | |
692 | ||
693 | self.dst_discoverer.discover() | |
694 | ||
e9c1b938 JW |
695 | def __dst_discovered(self, data, is_media): |
696 | self.dst_data = data | |
697 | ||
698 | self.__stop() | |
699 | ||
a249b6d3 | 700 | def __stop(self): |
26729e02 JW |
701 | _log.debug(self.loop) |
702 | ||
4f4f2531 JW |
703 | if hasattr(self, 'pipeline'): |
704 | # Stop executing the pipeline | |
705 | self.pipeline.set_state(gst.STATE_NULL) | |
26729e02 | 706 | |
206ef749 JW |
707 | # This kills the loop, mercifully |
708 | gobject.idle_add(self.__stop_mainloop) | |
709 | ||
710 | def __stop_mainloop(self): | |
711 | ''' | |
712 | Wrapper for gobject.MainLoop.quit() | |
713 | ||
714 | This wrapper makes us able to see if self.loop.quit has been called | |
715 | ''' | |
716 | _log.info('Terminating MainLoop') | |
717 | ||
718 | self.loop.quit() | |
26729e02 JW |
719 | |
720 | ||
721 | if __name__ == '__main__': | |
206ef749 | 722 | os.nice(19) |
10085b77 | 723 | logging.basicConfig() |
a249b6d3 JW |
724 | from optparse import OptionParser |
725 | ||
726 | parser = OptionParser( | |
10085b77 | 727 | usage='%prog [-v] -a [ video | thumbnail | discover ] SRC [ DEST ]') |
a249b6d3 JW |
728 | |
729 | parser.add_option('-a', '--action', | |
730 | dest='action', | |
10085b77 | 731 | help='One of "video", "discover" or "thumbnail"') |
a249b6d3 JW |
732 | |
733 | parser.add_option('-v', | |
734 | dest='verbose', | |
735 | action='store_true', | |
736 | help='Output debug information') | |
737 | ||
738 | parser.add_option('-q', | |
739 | dest='quiet', | |
740 | action='store_true', | |
741 | help='Dear program, please be quiet unless *error*') | |
742 | ||
b06ea4ab JW |
743 | parser.add_option('-w', '--width', |
744 | type=int, | |
745 | default=180) | |
746 | ||
a249b6d3 JW |
747 | (options, args) = parser.parse_args() |
748 | ||
749 | if options.verbose: | |
750 | _log.setLevel(logging.DEBUG) | |
751 | else: | |
752 | _log.setLevel(logging.INFO) | |
753 | ||
754 | if options.quiet: | |
755 | _log.setLevel(logging.ERROR) | |
756 | ||
757 | _log.debug(args) | |
758 | ||
10085b77 | 759 | if not len(args) == 2 and not options.action == 'discover': |
a249b6d3 JW |
760 | parser.print_help() |
761 | sys.exit() | |
762 | ||
10085b77 JW |
763 | transcoder = VideoTranscoder() |
764 | ||
a249b6d3 | 765 | if options.action == 'thumbnail': |
b06ea4ab | 766 | args.append(options.width) |
e4a1b6d2 | 767 | VideoThumbnailerMarkII(*args) |
a249b6d3 | 768 | elif options.action == 'video': |
206ef749 JW |
769 | def cb(data): |
770 | print('I\'m a callback!') | |
10085b77 JW |
771 | transcoder.transcode(*args, progress_callback=cb) |
772 | elif options.action == 'discover': | |
34c35c8c | 773 | print transcoder.discover(*args) |