Media processing, transcoding, display fixes
[mediagoblin.git] / mediagoblin / media_types / video / transcoders.py
CommitLineData
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 17from __future__ import division
206ef749
JW
18
19import os
26729e02
JW
20import sys
21import logging
206ef749 22import urllib
ff3136d0
JW
23import multiprocessing
24import gobject
9fb336fd
JW
25import pygst
26pygst.require('0.10')
ff3136d0
JW
27import gst
28import struct
29import Image
a249b6d3 30
ff3136d0
JW
31from gst.extend import discoverer
32
ab0d5b59
JW
33_log = logging.getLogger(__name__)
34
ff3136d0 35gobject.threads_init()
26729e02 36
64fd0462 37CPU_COUNT = 2
64fd0462 38
315266b4 39try:
ff3136d0
JW
40 CPU_COUNT = multiprocessing.cpu_count()
41except NotImplementedError:
42 _log.warning('multiprocessing.cpu_count not implemented')
206ef749 43
ff3136d0
JW
44os.putenv('GST_DEBUG_DUMP_DOT_DIR', '/tmp')
45
46
47def 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
26729e02 56class VideoThumbnailer:
206ef749
JW
57 # Declaration of thumbnailer states
58 STATE_NULL = 0
59 STATE_HALTING = 1
60 STATE_PROCESSING = 2
61
62 # The current thumbnailer state
63 state = STATE_NULL
64
65 # This will contain the thumbnailing pipeline
66 thumbnail_pipeline = None
67
68 buffer_probes = {}
69
206ef749
JW
70 def __init__(self, source_path, dest_path):
71 '''
72 Set up playbin pipeline in order to get video properties.
73
74 Initializes and runs the gobject.MainLoop()
196a5181
JW
75
76 Abstract
77 - Set up a playbin with a fake audio sink and video sink. Load the video
78 into the playbin
79 - Initialize
206ef749 80 '''
4535f759
JW
81 self.errors = []
82
206ef749
JW
83 self.source_path = source_path
84 self.dest_path = dest_path
85
86 self.loop = gobject.MainLoop()
87
88 # Set up the playbin. It will be used to discover certain
89 # properties of the input file
90 self.playbin = gst.element_factory_make('playbin')
91
92 self.videosink = gst.element_factory_make('fakesink', 'videosink')
93 self.playbin.set_property('video-sink', self.videosink)
94
6506b1e2
JW
95 self.audiosink = gst.element_factory_make('fakesink', 'audiosink')
96 self.playbin.set_property('audio-sink', self.audiosink)
206ef749
JW
97
98 self.bus = self.playbin.get_bus()
99 self.bus.add_signal_watch()
100 self.watch_id = self.bus.connect('message', self._on_bus_message)
101
102 self.playbin.set_property('uri', 'file:{0}'.format(
103 urllib.pathname2url(self.source_path)))
104
105 self.playbin.set_state(gst.STATE_PAUSED)
106
107 self.run()
108
109 def run(self):
110 self.loop.run()
111
112 def _on_bus_message(self, bus, message):
196a5181 113 _log.debug(' thumbnail playbin: {0}'.format(message))
206ef749
JW
114
115 if message.type == gst.MESSAGE_ERROR:
196a5181 116 _log.error('thumbnail playbin: {0}'.format(message))
206ef749
JW
117 gobject.idle_add(self._on_bus_error)
118
119 elif message.type == gst.MESSAGE_STATE_CHANGED:
120 # The pipeline state has changed
121 # Parse state changing data
122 _prev, state, _pending = message.parse_state_changed()
123
124 _log.debug('State changed: {0}'.format(state))
125
126 if state == gst.STATE_PAUSED:
127 if message.src == self.playbin:
128 gobject.idle_add(self._on_bus_paused)
129
130 def _on_bus_paused(self):
131 '''
132 Set up thumbnailing pipeline
133 '''
134 current_video = self.playbin.get_property('current-video')
135
136 if current_video == 0:
137 _log.debug('Found current video from playbin')
138 else:
139 _log.error('Could not get any current video from playbin!')
140
141 self.duration = self._get_duration(self.playbin)
142 _log.info('Video length: {0}'.format(self.duration / gst.SECOND))
143
144 _log.info('Setting up thumbnailing pipeline')
145 self.thumbnail_pipeline = gst.parse_launch(
146 'filesrc location="{0}" ! decodebin ! '
147 'ffmpegcolorspace ! videoscale ! '
148 'video/x-raw-rgb,depth=24,bpp=24,pixel-aspect-ratio=1/1,width=180 ! '
149 'fakesink signal-handoffs=True'.format(self.source_path))
150
151 self.thumbnail_bus = self.thumbnail_pipeline.get_bus()
152 self.thumbnail_bus.add_signal_watch()
153 self.thumbnail_watch_id = self.thumbnail_bus.connect(
154 'message', self._on_thumbnail_bus_message)
155
156 self.thumbnail_pipeline.set_state(gst.STATE_PAUSED)
157
158 #gobject.timeout_add(3000, self._on_timeout)
159
160 return False
161
162 def _on_thumbnail_bus_message(self, bus, message):
196a5181 163 _log.debug('thumbnail: {0}'.format(message))
206ef749
JW
164
165 if message.type == gst.MESSAGE_ERROR:
166 _log.error(message)
167 gobject.idle_add(self._on_bus_error)
168
169 if message.type == gst.MESSAGE_STATE_CHANGED:
196a5181 170 _log.debug('State changed')
206ef749
JW
171 _prev, state, _pending = message.parse_state_changed()
172
173 if (state == gst.STATE_PAUSED and
174 not self.state == self.STATE_PROCESSING and
175 message.src == self.thumbnail_pipeline):
176 _log.info('Pipeline paused, processing')
177 self.state = self.STATE_PROCESSING
178
179 for sink in self.thumbnail_pipeline.sinks():
180 name = sink.get_name()
181 factoryname = sink.get_factory().get_name()
182
183 if factoryname == 'fakesink':
184 sinkpad = sink.get_pad('sink')
185
186 self.buffer_probes[name] = sinkpad.add_buffer_probe(
187 self.buffer_probe_handler, name)
188
189 _log.info('Added buffer probe')
190
191 break
192
193 # Apply the wadsworth constant, fallback to 1 second
196a5181 194 # TODO: Will break if video is shorter than 1 sec
206ef749
JW
195 seek_amount = max(self.duration / 100 * 30, 1 * gst.SECOND)
196
197 _log.debug('seek amount: {0}'.format(seek_amount))
198
206ef749
JW
199 seek_result = self.thumbnail_pipeline.seek(
200 1.0,
201 gst.FORMAT_TIME,
202 gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_ACCURATE,
203 gst.SEEK_TYPE_SET,
204 seek_amount,
205 gst.SEEK_TYPE_NONE,
206 0)
206ef749
JW
207
208 if not seek_result:
209 self.errors.append('COULD_NOT_SEEK')
210 _log.error('Couldn\'t seek! result: {0}'.format(
211 seek_result))
212 _log.info(message)
213 self.shutdown()
214 else:
196a5181
JW
215 _log.debug('Seek successful')
216 self.thumbnail_pipeline.set_state(gst.STATE_PAUSED)
206ef749 217 #pdb.set_trace()
196a5181
JW
218 else:
219 _log.debug('Won\'t seek: \t{0}\n\t{1}'.format(
220 self.state,
221 message.src))
206ef749
JW
222
223 def buffer_probe_handler_real(self, pad, buff, name):
224 '''
225 Capture buffers as gdk_pixbufs when told to.
226 '''
196a5181 227 _log.info('Capturing frame')
206ef749
JW
228 try:
229 caps = buff.caps
230 if caps is None:
231 _log.error('No caps passed to buffer probe handler!')
232 self.shutdown()
233 return False
234
235 _log.debug('caps: {0}'.format(caps))
236
237 filters = caps[0]
238 width = filters["width"]
239 height = filters["height"]
240
ff3136d0 241 im = Image.new('RGB', (width, height))
206ef749 242
ff3136d0 243 data = pixbuf_to_pilbuf(buff.data)
206ef749 244
ff3136d0
JW
245 im.putdata(data)
246
ab0d5b59 247 im.save(self.dest_path)
206ef749 248
206ef749 249 _log.info('Saved thumbnail')
ff3136d0 250
206ef749 251 self.shutdown()
ff3136d0 252
196a5181
JW
253 except gst.QueryError as e:
254 _log.error('QueryError: {0}'.format(e))
255
206ef749
JW
256 return False
257
258 def buffer_probe_handler(self, pad, buff, name):
259 '''
260 Proxy function for buffer_probe_handler_real
261 '''
196a5181 262 _log.debug('Attaching real buffer handler to gobject idle event')
206ef749
JW
263 gobject.idle_add(
264 lambda: self.buffer_probe_handler_real(pad, buff, name))
265
266 return True
267
268 def _get_duration(self, pipeline, retries=0):
269 '''
270 Get the duration of a pipeline.
271
272 Retries 5 times.
273 '''
274 if retries == 5:
275 return 0
276
277 try:
c56d4b55 278 return pipeline.query_duration(gst.FORMAT_TIME)[0]
206ef749
JW
279 except gst.QueryError:
280 return self._get_duration(pipeline, retries + 1)
281
282 def _on_timeout(self):
196a5181 283 _log.error('Timeout in thumbnailer!')
206ef749
JW
284 self.shutdown()
285
286 def _on_bus_error(self, *args):
287 _log.error('AHAHAHA! Error! args: {0}'.format(args))
288
289 def shutdown(self):
290 '''
291 Tell gobject to call __halt when the mainloop is idle.
292 '''
293 _log.info('Shutting down')
294 self.__halt()
295
296 def __halt(self):
297 '''
298 Halt all pipelines and shut down the main loop
299 '''
300 _log.info('Halting...')
301 self.state = self.STATE_HALTING
302
303 self.__disconnect()
304
305 gobject.idle_add(self.__halt_final)
306
307 def __disconnect(self):
308 _log.debug('Disconnecting...')
309 if not self.playbin is None:
310 self.playbin.set_state(gst.STATE_NULL)
311 for sink in self.playbin.sinks():
312 name = sink.get_name()
313 factoryname = sink.get_factory().get_name()
314
315 _log.debug('Disconnecting {0}'.format(name))
316
317 if factoryname == "fakesink":
318 pad = sink.get_pad("sink")
319 pad.remove_buffer_probe(self.buffer_probes[name])
320 del self.buffer_probes[name]
321
322 self.playbin = None
323
324 if self.bus is not None:
325 self.bus.disconnect(self.watch_id)
326 self.bus = None
327
206ef749
JW
328 def __halt_final(self):
329 _log.info('Done')
330 if self.errors:
331 _log.error(','.join(self.errors))
c56d4b55 332
206ef749
JW
333 self.loop.quit()
334
335
e9c1b938 336class VideoTranscoder:
a249b6d3
JW
337 '''
338 Video transcoder
339
e9c1b938
JW
340 Transcodes the SRC video file to a VP8 WebM video file at DST
341
206ef749
JW
342 - Does the same thing as VideoThumbnailer, but produces a WebM vp8
343 and vorbis video file.
344 - The VideoTranscoder exceeds the VideoThumbnailer in the way
345 that it was refined afterwards and therefore is done more
346 correctly.
a249b6d3 347 '''
10085b77 348 def __init__(self):
a249b6d3
JW
349 _log.info('Initializing VideoTranscoder...')
350
351 self.loop = gobject.MainLoop()
10085b77
JW
352
353 def transcode(self, src, dst, **kwargs):
354 '''
355 Transcode a video file into a 'medium'-sized version.
356 '''
a249b6d3
JW
357 self.source_path = src
358 self.destination_path = dst
359
196a5181
JW
360 # vp8enc options
361 self.destination_dimensions = kwargs.get('dimensions', (640, 640))
362 self.vp8_quality = kwargs.get('vp8_quality', 8)
363 # Number of threads used by vp8enc:
364 # number of real cores - 1 as per recommendation on
365 # <http://www.webmproject.org/tools/encoder-parameters/#6-multi-threaded-encode-and-decode>
366 self.vp8_threads = kwargs.get('vp8_threads', CPU_COUNT - 1)
367
368 # 0 means auto-detect, but dict.get() only falls back to CPU_COUNT
369 # if value is None, this will correct our incompatibility with
370 # dict.get()
371 # This will also correct cases where there's only 1 CPU core, see
372 # original self.vp8_threads assignment above.
373 if self.vp8_threads == 0:
374 self.vp8_threads = CPU_COUNT
375
376 # vorbisenc options
377 self.vorbis_quality = kwargs.get('vorbis_quality', 0.3)
378
206ef749 379 self._progress_callback = kwargs.get('progress_callback') or None
a249b6d3
JW
380
381 if not type(self.destination_dimensions) == tuple:
382 raise Exception('dimensions must be tuple: (width, height)')
383
384 self._setup()
385 self._run()
386
10085b77
JW
387 def discover(self, src):
388 '''
389 Discover properties about a media file
390 '''
391 _log.info('Discovering {0}'.format(src))
392
393 self.source_path = src
394 self._setup_discover(discovered_callback=self.__on_discovered)
395
396 self.discoverer.discover()
397
398 self.loop.run()
399
4f4f2531
JW
400 if hasattr(self, '_discovered_data'):
401 return self._discovered_data.__dict__
402 else:
403 return None
10085b77
JW
404
405 def __on_discovered(self, data, is_media):
4f4f2531 406 _log.debug('Discovered: {0}'.format(data))
10085b77
JW
407 if not is_media:
408 self.__stop()
409 raise Exception('Could not discover {0}'.format(self.source_path))
410
411 self._discovered_data = data
412
413 self.__stop_mainloop()
414
a249b6d3 415 def _setup(self):
a249b6d3 416 self._setup_discover()
206ef749 417 self._setup_pipeline()
a249b6d3
JW
418
419 def _run(self):
420 _log.info('Discovering...')
421 self.discoverer.discover()
422 _log.info('Done')
423
424 _log.debug('Initializing MainLoop()')
425 self.loop.run()
426
10085b77 427 def _setup_discover(self, **kw):
206ef749 428 _log.debug('Setting up discoverer')
a249b6d3
JW
429 self.discoverer = discoverer.Discoverer(self.source_path)
430
431 # Connect self.__discovered to the 'discovered' event
10085b77
JW
432 self.discoverer.connect(
433 'discovered',
434 kw.get('discovered_callback', self.__discovered))
a249b6d3
JW
435
436 def __discovered(self, data, is_media):
437 '''
438 Callback for media discoverer.
439 '''
440 if not is_media:
441 self.__stop()
442 raise Exception('Could not discover {0}'.format(self.source_path))
443
206ef749 444 _log.debug('__discovered, data: {0}'.format(data.__dict__))
a249b6d3
JW
445
446 self.data = data
447
206ef749
JW
448 # Launch things that should be done after discovery
449 self._link_elements()
450 self.__setup_videoscale_capsfilter()
e9c1b938 451
a249b6d3
JW
452 # Tell the transcoding pipeline to start running
453 self.pipeline.set_state(gst.STATE_PLAYING)
454 _log.info('Transcoding...')
455
206ef749
JW
456 def _setup_pipeline(self):
457 _log.debug('Setting up transcoding pipeline')
458 # Create the pipeline bin.
a249b6d3
JW
459 self.pipeline = gst.Pipeline('VideoTranscoderPipeline')
460
206ef749
JW
461 # Create all GStreamer elements, starting with
462 # filesrc & decoder
a249b6d3
JW
463 self.filesrc = gst.element_factory_make('filesrc', 'filesrc')
464 self.filesrc.set_property('location', self.source_path)
465 self.pipeline.add(self.filesrc)
466
467 self.decoder = gst.element_factory_make('decodebin2', 'decoder')
a249b6d3
JW
468 self.decoder.connect('new-decoded-pad', self._on_dynamic_pad)
469 self.pipeline.add(self.decoder)
470
206ef749 471 # Video elements
b33701b8
JW
472 self.videoqueue = gst.element_factory_make('queue', 'videoqueue')
473 self.pipeline.add(self.videoqueue)
474
64fd0462
JW
475 self.videorate = gst.element_factory_make('videorate', 'videorate')
476 self.pipeline.add(self.videorate)
477
206ef749
JW
478 self.ffmpegcolorspace = gst.element_factory_make(
479 'ffmpegcolorspace', 'ffmpegcolorspace')
a249b6d3 480 self.pipeline.add(self.ffmpegcolorspace)
c56d4b55 481
b33701b8
JW
482 self.videoscale = gst.element_factory_make('ffvideoscale', 'videoscale')
483 #self.videoscale.set_property('method', 2) # I'm not sure this works
484 #self.videoscale.set_property('add-borders', 0)
a249b6d3
JW
485 self.pipeline.add(self.videoscale)
486
487 self.capsfilter = gst.element_factory_make('capsfilter', 'capsfilter')
488 self.pipeline.add(self.capsfilter)
489
490 self.vp8enc = gst.element_factory_make('vp8enc', 'vp8enc')
196a5181
JW
491 self.vp8enc.set_property('quality', self.vp8_quality)
492 self.vp8enc.set_property('threads', self.vp8_threads)
493 self.vp8enc.set_property('max-latency', 25)
e9c1b938
JW
494 self.pipeline.add(self.vp8enc)
495
206ef749 496 # Audio elements
b33701b8
JW
497 self.audioqueue = gst.element_factory_make('queue', 'audioqueue')
498 self.pipeline.add(self.audioqueue)
499
64fd0462 500 self.audiorate = gst.element_factory_make('audiorate', 'audiorate')
359781f0 501 self.audiorate.set_property('tolerance', 80000000)
64fd0462
JW
502 self.pipeline.add(self.audiorate)
503
e9c1b938
JW
504 self.audioconvert = gst.element_factory_make('audioconvert', 'audioconvert')
505 self.pipeline.add(self.audioconvert)
506
64fd0462
JW
507 self.audiocapsfilter = gst.element_factory_make('capsfilter', 'audiocapsfilter')
508 audiocaps = ['audio/x-raw-float']
509 self.audiocapsfilter.set_property(
510 'caps',
511 gst.caps_from_string(
512 ','.join(audiocaps)))
513 self.pipeline.add(self.audiocapsfilter)
514
e9c1b938 515 self.vorbisenc = gst.element_factory_make('vorbisenc', 'vorbisenc')
196a5181 516 self.vorbisenc.set_property('quality', self.vorbis_quality)
e9c1b938
JW
517 self.pipeline.add(self.vorbisenc)
518
206ef749 519 # WebMmux & filesink
a249b6d3
JW
520 self.webmmux = gst.element_factory_make('webmmux', 'webmmux')
521 self.pipeline.add(self.webmmux)
522
523 self.filesink = gst.element_factory_make('filesink', 'filesink')
e9c1b938
JW
524 self.filesink.set_property('location', self.destination_path)
525 self.pipeline.add(self.filesink)
a249b6d3 526
206ef749
JW
527 # Progressreport
528 self.progressreport = gst.element_factory_make(
529 'progressreport', 'progressreport')
530 # Update every second
531 self.progressreport.set_property('update-freq', 1)
532 self.progressreport.set_property('silent', True)
533 self.pipeline.add(self.progressreport)
534
535 def _link_elements(self):
536 '''
537 Link all the elements
538
539 This code depends on data from the discoverer and is called
540 from __discovered
541 '''
542 _log.debug('linking elements')
543 # Link the filesrc element to the decoder. The decoder then emits
544 # 'new-decoded-pad' which links decoded src pads to either a video
545 # or audio sink
a249b6d3 546 self.filesrc.link(self.decoder)
206ef749 547
c875bb74
JW
548 # Link all the video elements in a row to webmmux
549 gst.element_link_many(
550 self.videoqueue,
551 self.videorate,
552 self.ffmpegcolorspace,
553 self.videoscale,
554 self.capsfilter,
555 self.vp8enc,
556 self.webmmux)
e9c1b938 557
206ef749 558 if self.data.is_audio:
c875bb74
JW
559 # Link all the audio elements in a row to webmux
560 gst.element_link_many(
561 self.audioqueue,
562 self.audiorate,
563 self.audioconvert,
564 self.audiocapsfilter,
565 self.vorbisenc,
566 self.webmmux)
567
568 gst.element_link_many(
569 self.webmmux,
570 self.progressreport,
571 self.filesink)
a249b6d3 572
206ef749 573 # Setup the message bus and connect _on_message to the pipeline
a249b6d3
JW
574 self._setup_bus()
575
576 def _on_dynamic_pad(self, dbin, pad, islast):
577 '''
578 Callback called when ``decodebin2`` has a pad that we can connect to
579 '''
206ef749
JW
580 # Intersect the capabilities of the video sink and the pad src
581 # Then check if they have no common capabilities.
e9c1b938
JW
582 if self.ffmpegcolorspace.get_pad_template('sink')\
583 .get_caps().intersect(pad.get_caps()).is_empty():
206ef749 584 # It is NOT a video src pad.
b33701b8 585 pad.link(self.audioqueue.get_pad('sink'))
e9c1b938 586 else:
206ef749 587 # It IS a video src pad.
b33701b8 588 pad.link(self.videoqueue.get_pad('sink'))
a249b6d3
JW
589
590 def _setup_bus(self):
591 self.bus = self.pipeline.get_bus()
592 self.bus.add_signal_watch()
593 self.bus.connect('message', self._on_message)
594
e9c1b938 595 def __setup_videoscale_capsfilter(self):
206ef749
JW
596 '''
597 Sets up the output format (width, height) for the video
598 '''
64fd0462 599 caps = ['video/x-raw-yuv', 'pixel-aspect-ratio=1/1', 'framerate=30/1']
a249b6d3
JW
600
601 if self.data.videoheight > self.data.videowidth:
e9c1b938
JW
602 # Whoa! We have ourselves a portrait video!
603 caps.append('height={0}'.format(
604 self.destination_dimensions[1]))
a249b6d3 605 else:
e9c1b938
JW
606 # It's a landscape, phew, how normal.
607 caps.append('width={0}'.format(
608 self.destination_dimensions[0]))
a249b6d3 609
e9c1b938
JW
610 self.capsfilter.set_property(
611 'caps',
612 gst.caps_from_string(
64fd0462 613 ','.join(caps)))
a249b6d3
JW
614
615 def _on_message(self, bus, message):
206ef749 616 _log.debug((bus, message, message.type))
a249b6d3
JW
617
618 t = message.type
619
c56d4b55 620 if message.type == gst.MESSAGE_EOS:
e9c1b938 621 self._discover_dst_and_stop()
a249b6d3 622 _log.info('Done')
206ef749 623
c56d4b55 624 elif message.type == gst.MESSAGE_ELEMENT:
206ef749 625 if message.structure.get_name() == 'progress':
8e5f9746 626 data = dict(message.structure)
206ef749
JW
627
628 if self._progress_callback:
629 self._progress_callback(data)
630
631 _log.info('{percent}% done...'.format(
8e5f9746 632 percent=data.get('percent')))
206ef749
JW
633 _log.debug(data)
634
a249b6d3
JW
635 elif t == gst.MESSAGE_ERROR:
636 _log.error((bus, message))
637 self.__stop()
638
e9c1b938
JW
639 def _discover_dst_and_stop(self):
640 self.dst_discoverer = discoverer.Discoverer(self.destination_path)
641
642 self.dst_discoverer.connect('discovered', self.__dst_discovered)
643
644 self.dst_discoverer.discover()
645
e9c1b938
JW
646 def __dst_discovered(self, data, is_media):
647 self.dst_data = data
648
649 self.__stop()
650
a249b6d3 651 def __stop(self):
26729e02
JW
652 _log.debug(self.loop)
653
4f4f2531
JW
654 if hasattr(self, 'pipeline'):
655 # Stop executing the pipeline
656 self.pipeline.set_state(gst.STATE_NULL)
26729e02 657
206ef749
JW
658 # This kills the loop, mercifully
659 gobject.idle_add(self.__stop_mainloop)
660
661 def __stop_mainloop(self):
662 '''
663 Wrapper for gobject.MainLoop.quit()
664
665 This wrapper makes us able to see if self.loop.quit has been called
666 '''
667 _log.info('Terminating MainLoop')
668
669 self.loop.quit()
26729e02
JW
670
671
672if __name__ == '__main__':
206ef749 673 os.nice(19)
10085b77 674 logging.basicConfig()
a249b6d3
JW
675 from optparse import OptionParser
676
677 parser = OptionParser(
10085b77 678 usage='%prog [-v] -a [ video | thumbnail | discover ] SRC [ DEST ]')
a249b6d3
JW
679
680 parser.add_option('-a', '--action',
681 dest='action',
10085b77 682 help='One of "video", "discover" or "thumbnail"')
a249b6d3
JW
683
684 parser.add_option('-v',
685 dest='verbose',
686 action='store_true',
687 help='Output debug information')
688
689 parser.add_option('-q',
690 dest='quiet',
691 action='store_true',
692 help='Dear program, please be quiet unless *error*')
693
694 (options, args) = parser.parse_args()
695
696 if options.verbose:
697 _log.setLevel(logging.DEBUG)
698 else:
699 _log.setLevel(logging.INFO)
700
701 if options.quiet:
702 _log.setLevel(logging.ERROR)
703
704 _log.debug(args)
705
10085b77 706 if not len(args) == 2 and not options.action == 'discover':
a249b6d3
JW
707 parser.print_help()
708 sys.exit()
709
10085b77
JW
710 transcoder = VideoTranscoder()
711
a249b6d3
JW
712 if options.action == 'thumbnail':
713 VideoThumbnailer(*args)
714 elif options.action == 'video':
206ef749
JW
715 def cb(data):
716 print('I\'m a callback!')
10085b77
JW
717 transcoder.transcode(*args, progress_callback=cb)
718 elif options.action == 'discover':
719 print transcoder.discover(*args).__dict__