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 | |
26729e02 | 56 | class 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 | 336 | class 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 | ||
672 | if __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__ |