Port of audio to GStreamer 1.0
authorBoris Bobrov <breton@cynicmansion.ru>
Fri, 13 Jun 2014 06:02:10 +0000 (10:02 +0400)
committerBoris Bobrov <breton@cynicmansion.ru>
Mon, 16 Feb 2015 10:41:04 +0000 (13:41 +0300)
Includes:
 - transcoders
 - thumbs
 - tests

mediagoblin/media_types/audio/processing.py
mediagoblin/media_types/audio/transcoders.py
mediagoblin/media_types/video/transcoders.py
mediagoblin/tests/test_audio.py [new file with mode: 0644]

index de6fa9ca3af9cc64f1bc24cf79f7ced99c392c1d..770342ffcd041c9f9682292386c408659164858f 100644 (file)
@@ -27,6 +27,7 @@ from mediagoblin.processing import (
 
 from mediagoblin.media_types.audio.transcoders import (
     AudioTranscoder, AudioThumbnailer)
+from mediagoblin.media_types.tools import discover
 
 _log = logging.getLogger(__name__)
 
@@ -35,16 +36,9 @@ MEDIA_TYPE = 'mediagoblin.media_types.audio'
 
 def sniff_handler(media_file, filename):
     _log.info('Sniffing {0}'.format(MEDIA_TYPE))
-    try:
-        transcoder = AudioTranscoder()
-        data = transcoder.discover(media_file.name)
-    except BadMediaFail:
-        _log.debug('Audio discovery raised BadMediaFail')
-        return None
-
-    if data.is_audio is True and data.is_video is False:
+    data = discover(media_file.name)
+    if data and data.get_audio_streams() and not data.get_video_streams():
         return MEDIA_TYPE
-
     return None
 
 
@@ -126,8 +120,6 @@ class CommonAudioProcessor(MediaProcessor):
             quality=quality,
             progress_callback=progress_callback)
 
-        self.transcoder.discover(webm_audio_tmp)
-
         self._keep_best()
 
         _log.debug('Saving medium...')
@@ -145,21 +137,14 @@ class CommonAudioProcessor(MediaProcessor):
         if self._skip_processing('spectrogram', max_width=max_width,
                                  fft_size=fft_size):
             return
-
         wav_tmp = os.path.join(self.workbench.dir, self.name_builder.fill(
             '{basename}.ogg'))
-
         _log.info('Creating OGG source for spectrogram')
-        self.transcoder.transcode(
-            self.process_filename,
-            wav_tmp,
-            mux_string='vorbisenc quality={0} ! oggmux'.format(
-                self.audio_config['quality']))
-
+        self.transcoder.transcode(self.process_filename, wav_tmp,
+                                  mux_name='oggmux')
         spectrogram_tmp = os.path.join(self.workbench.dir,
                                        self.name_builder.fill(
                                            '{basename}-spectrogram.jpg'))
-
         self.thumbnailer.spectrogram(
             wav_tmp,
             spectrogram_tmp,
index 150dad8eb1eda35d8d78bc0eb0d44297acce8f89..f86528dee3d6f6e72a532eac0547d83026299218 100644 (file)
@@ -20,10 +20,8 @@ try:
 except ImportError:
     import Image
 
-from mediagoblin.processing import BadMediaFail
 from mediagoblin.media_types.audio import audioprocessing
 
-
 _log = logging.getLogger(__name__)
 
 CPU_COUNT = 2  # Just assuming for now
@@ -39,26 +37,13 @@ try:
 except ImportError:
     _log.warning('Could not import multiprocessing, assuming 2 CPU cores')
 
-# IMPORT GOBJECT
-try:
-    import gobject
-    gobject.threads_init()
-except ImportError:
-    raise Exception('gobject could not be found')
-
-# IMPORT PYGST
-try:
-    import pygst
-
-    # We won't settle for less. For now, this is an arbitrary limit
-    # as we have not tested with > 0.10
-    pygst.require('0.10')
+# uncomment this to get a lot of logs from gst
+# import os;os.environ['GST_DEBUG'] = '5,python:5'
 
-    import gst
-
-    import gst.extend.discoverer
-except ImportError:
-    raise Exception('gst/pygst >= 0.10 could not be imported')
+import gi
+gi.require_version('Gst', '1.0')
+from gi.repository import GObject, Gst
+Gst.init(None)
 
 import numpy
 
@@ -72,7 +57,6 @@ class AudioThumbnailer(object):
         height = int(kw.get('height', float(width) * 0.3))
         fft_size = kw.get('fft_size', 2048)
         callback = kw.get('progress_callback')
-
         processor = audioprocessing.AudioProcessor(
             src,
             fft_size,
@@ -132,95 +116,87 @@ class AudioTranscoder(object):
         _log.info('Initializing {0}'.format(self.__class__.__name__))
 
         # Instantiate MainLoop
-        self._loop = gobject.MainLoop()
+        self._loop = GObject.MainLoop()
         self._failed = None
 
-    def discover(self, src):
-        self._src_path = src
-        _log.info('Discovering {0}'.format(src))
-        self._discovery_path = src
-
-        self._discoverer = gst.extend.discoverer.Discoverer(
-            self._discovery_path)
-        self._discoverer.connect('discovered', self.__on_discovered)
-        self._discoverer.discover()
-
-        self._loop.run()  # Run MainLoop
-
-        if self._failed:
-            raise self._failed
-
-        # Once MainLoop has returned, return discovery data
-        return getattr(self, '_discovery_data', False)
-
-    def __on_discovered(self, data, is_media):
-        if not is_media:
-            self._failed = BadMediaFail()
-            _log.error('Could not discover {0}'.format(self._src_path))
-            self.halt()
-
-        _log.debug('Discovered: {0}'.format(data.__dict__))
-
-        self._discovery_data = data
-
-        # Gracefully shut down MainLoop
-        self.halt()
-
-    def transcode(self, src, dst, **kw):
+    def transcode(self, src, dst, mux_name='webmmux',quality=0.3,
+                  progress_callback=None, **kw):
+        def _on_pad_added(element, pad, connect_to):
+            caps = pad.query_caps(None)
+            name = caps.to_string()
+            _log.debug('on_pad_added: {0}'.format(name))
+            if name.startswith('audio') and not connect_to.is_linked():
+                pad.link(connect_to)
         _log.info('Transcoding {0} into {1}'.format(src, dst))
-        self._discovery_data = kw.get('data', self.discover(src))
-
-        self.__on_progress = kw.get('progress_callback')
-
-        quality = kw.get('quality', 0.3)
-
-        mux_string = kw.get(
-            'mux_string',
-            'vorbisenc quality={0} ! webmmux'.format(quality))
-
+        self.__on_progress = progress_callback
         # Set up pipeline
-        self.pipeline = gst.parse_launch(
-            'filesrc location="{src}" ! '
-            'decodebin2 ! queue ! audiorate tolerance={tolerance} ! '
-            'audioconvert ! audio/x-raw-float,channels=2 ! '
-            '{mux_string} ! '
-            'progressreport silent=true ! '
-            'filesink location="{dst}"'.format(
-                src=src,
-                tolerance=80000000,
-                mux_string=mux_string,
-                dst=dst))
-
+        tolerance = 80000000
+        self.pipeline = Gst.Pipeline()
+        filesrc = Gst.ElementFactory.make('filesrc', 'filesrc')
+        filesrc.set_property('location', src)
+        decodebin = Gst.ElementFactory.make('decodebin', 'decodebin')
+        queue = Gst.ElementFactory.make('queue', 'queue')
+        decodebin.connect('pad-added', _on_pad_added,
+                          queue.get_static_pad('sink'))
+        audiorate = Gst.ElementFactory.make('audiorate', 'audiorate')
+        audiorate.set_property('tolerance', tolerance)
+        audioconvert = Gst.ElementFactory.make('audioconvert', 'audioconvert')
+        caps_struct = Gst.Structure.new_empty('audio/x-raw')
+        caps_struct.set_value('channels', 2)
+        caps = Gst.Caps.new_empty()
+        caps.append_structure(caps_struct)
+        capsfilter = Gst.ElementFactory.make('capsfilter', 'capsfilter')
+        capsfilter.set_property('caps', caps)
+        enc = Gst.ElementFactory.make('vorbisenc', 'enc')
+        enc.set_property('quality', quality)
+        mux = Gst.ElementFactory.make(mux_name, 'mux')
+        progressreport = Gst.ElementFactory.make('progressreport', 'progress')
+        progressreport.set_property('silent', True)
+        sink = Gst.ElementFactory.make('filesink', 'sink')
+        sink.set_property('location', dst)
+        # add to pipeline
+        for e in [filesrc, decodebin, queue, audiorate, audioconvert,
+                  capsfilter, enc, mux, progressreport, sink]:
+            self.pipeline.add(e)
+        # link elements
+        filesrc.link(decodebin)
+        decodebin.link(queue)
+        queue.link(audiorate)
+        audiorate.link(audioconvert)
+        audioconvert.link(capsfilter)
+        capsfilter.link(enc)
+        enc.link(mux)
+        mux.link(progressreport)
+        progressreport.link(sink)
         self.bus = self.pipeline.get_bus()
         self.bus.add_signal_watch()
         self.bus.connect('message', self.__on_bus_message)
-
-        self.pipeline.set_state(gst.STATE_PLAYING)
-
+        # run
+        self.pipeline.set_state(Gst.State.PLAYING)
         self._loop.run()
 
     def __on_bus_message(self, bus, message):
-        _log.debug(message)
-
-        if (message.type == gst.MESSAGE_ELEMENT
-            and message.structure.get_name() == 'progress'):
-            data = dict(message.structure)
-
-            if self.__on_progress:
-                self.__on_progress(data.get('percent'))
-
-            _log.info('{0}% done...'.format(
-                    data.get('percent')))
-        elif message.type == gst.MESSAGE_EOS:
+        _log.debug(message.type)
+        if (message.type == Gst.MessageType.ELEMENT
+                and message.has_name('progress')):
+            structure = message.get_structure()
+            (success, percent) = structure.get_int('percent')
+            if self.__on_progress and success:
+                self.__on_progress(percent)
+            _log.info('{0}% done...'.format(percent))
+        elif message.type == Gst.MessageType.EOS:
             _log.info('Done')
             self.halt()
+        elif message.type == Gst.MessageType.ERROR:
+            _log.error(message.parse_error())
+            self.halt()
 
     def halt(self):
         if getattr(self, 'pipeline', False):
-            self.pipeline.set_state(gst.STATE_NULL)
+            self.pipeline.set_state(Gst.State.NULL)
             del self.pipeline
         _log.info('Quitting MainLoop gracefully...')
-        gobject.idle_add(self._loop.quit)
+        GObject.idle_add(self._loop.quit)
 
 if __name__ == '__main__':
     import sys
index d53cabc6c25c520fa88477247eb968724cab75bc..20f2169703931aff00e41f30ae521c955aa3714a 100644 (file)
@@ -239,7 +239,6 @@ class VideoTranscoder(object):
 
         self.audioconvert = Gst.ElementFactory.make('audioconvert', 'audioconvert')
         self.pipeline.add(self.audioconvert)
-
         self.audiocapsfilter = Gst.ElementFactory.make('capsfilter',
                                                        'audiocapsfilter')
         audiocaps = Gst.Caps.new_empty()
@@ -288,8 +287,7 @@ class VideoTranscoder(object):
         self.capsfilter.link(self.vp8enc)
         self.vp8enc.link(self.webmmux)
 
-        if self.data.is_audio:
-            # Link all the audio elements in a row to webmmux
+        if self.data.get_audio_streams():
             self.audioqueue.link(self.audiorate)
             self.audiorate.link(self.audioconvert)
             self.audioconvert.link(self.audiocapsfilter)
@@ -310,6 +308,7 @@ class VideoTranscoder(object):
         if (self.videorate.get_static_pad('sink').get_pad_template()
                 .get_caps().intersect(pad.query_caps()).is_empty()):
             # It is NOT a video src pad.
+            _log.debug('linking audio to the pad dynamically')
             pad.link(self.audioqueue.get_static_pad('sink'))
         else:
             # It IS a video src pad.
diff --git a/mediagoblin/tests/test_audio.py b/mediagoblin/tests/test_audio.py
new file mode 100644 (file)
index 0000000..740d9cd
--- /dev/null
@@ -0,0 +1,104 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2013 MediaGoblin contributors.  See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import tempfile
+import shutil
+import os
+import pytest
+from contextlib import contextmanager
+import logging
+import imghdr
+
+#os.environ['GST_DEBUG'] = '4,python:4'
+
+#TODO: this should be skipped if video plugin is not enabled
+import gi
+gi.require_version('Gst', '1.0')
+from gi.repository import Gst
+Gst.init(None)
+
+from mediagoblin.media_types.audio.transcoders import (AudioTranscoder,
+        AudioThumbnailer)
+from mediagoblin.media_types.tools import discover
+
+
+@contextmanager
+def create_audio():
+    audio = tempfile.NamedTemporaryFile()
+    src = Gst.ElementFactory.make('audiotestsrc', None)
+    src.set_property('num-buffers', 50)
+    enc = Gst.ElementFactory.make('flacenc', None)
+    dst = Gst.ElementFactory.make('filesink', None)
+    dst.set_property('location', audio.name)
+    pipeline = Gst.Pipeline()
+    pipeline.add(src)
+    pipeline.add(enc)
+    pipeline.add(dst)
+    src.link(enc)
+    enc.link(dst)
+    pipeline.set_state(Gst.State.PLAYING)
+    state = pipeline.get_state(3 * Gst.SECOND)
+    assert state[0] == Gst.StateChangeReturn.SUCCESS
+    bus = pipeline.get_bus()
+    bus.timed_pop_filtered(
+            3 * Gst.SECOND,
+            Gst.MessageType.ERROR | Gst.MessageType.EOS)
+    pipeline.set_state(Gst.State.NULL)
+    yield (audio.name)
+
+
+@contextmanager
+def create_data_for_test():
+    with create_audio() as audio_name:
+        second_file = tempfile.NamedTemporaryFile()
+        yield (audio_name, second_file.name)
+
+
+def test_transcoder():
+    '''
+    Tests AudioTransocder's transcode method
+    '''
+    transcoder = AudioTranscoder()
+    with create_data_for_test() as (audio_name, result_name):
+        transcoder.transcode(audio_name, result_name, quality=0.3,
+                             progress_callback=None)
+        info = discover(result_name)
+        assert len(info.get_audio_streams()) == 1
+        transcoder.transcode(audio_name, result_name, quality=0.3,
+                             mux_name='oggmux', progress_callback=None)
+        info = discover(result_name)
+        assert len(info.get_audio_streams()) == 1
+
+
+def test_thumbnails():
+    '''Test thumbnails generation.
+
+    The code below heavily repeats
+    audio.processing.CommonAudioProcessor.create_spectrogram
+    1. Create test audio
+    2. Convert it to OGG source for spectogram using transcoder
+    3. Create spectogram in jpg
+
+    '''
+    thumbnailer = AudioThumbnailer()
+    transcoder = AudioTranscoder()
+    with create_data_for_test() as (audio_name, new_name):
+        transcoder.transcode(audio_name, new_name, mux_name='oggmux')
+        thumbnail = tempfile.NamedTemporaryFile(suffix='.jpg')
+        # fft_size below is copypasted from config_spec.ini
+        thumbnailer.spectrogram(new_name, thumbnail.name, width=100,
+                                fft_size=4096)
+        assert imghdr.what(thumbnail.name) == 'jpeg'