Updated VideoThumbnailerMarkII, removed old
[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
e4a1b6d2
JW
56class 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 \
148pending: {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
161without 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 171from 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 \
216pending: {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 381class 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
721if __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)