Audio media handler, media sniffing, video fixes
authorJoar Wandborg <git@wandborg.com>
Tue, 14 Feb 2012 22:26:07 +0000 (23:26 +0100)
committerJoar Wandborg <git@wandborg.com>
Tue, 14 Feb 2012 22:26:07 +0000 (23:26 +0100)
* Added audio processing code
* Added audio display template
* Added audio configuration setting
* Changed video docstring

mediagoblin/config_spec.ini
mediagoblin/media_types/__init__.py
mediagoblin/media_types/audio/__init__.py [new file with mode: 0644]
mediagoblin/media_types/audio/processing.py [new file with mode: 0644]
mediagoblin/media_types/audio/transcoder.py [new file with mode: 0644]
mediagoblin/media_types/audio/transcoders.py [new file with mode: 0644]
mediagoblin/media_types/video/processing.py
mediagoblin/media_types/video/transcoders.py
mediagoblin/templates/mediagoblin/media_displays/audio.html [new file with mode: 0644]

index 2d410899a232e6f123f599c059189db611834f7b..452d974571cd489b07ccb570a3c39beaa6b16133 100644 (file)
@@ -65,11 +65,14 @@ base_url = string(default="/mgoblin_media/")
 storage_class = string(default="mediagoblin.storage.filestorage:BasicFileStorage")
 base_dir = string(default="%(here)s/user_dev/media/queue")
 
-
-# Should we keep the original file?
 [media_type:mediagoblin.media_types.video]
+# Should we keep the original file?
 keep_original = boolean(default=False)
 
+[media_type:mediagoblin.media_types.audio]
+# vorbisenc qualiy
+quality = float(default=0.3)
+
 
 [beaker.cache]
 type = string(default="file")
index 5128826b9cbd18ae8cd212e208a0c3de454ffab8..c53ef1ab32c4f9801ff9e76810bb3d4f9855ce85 100644 (file)
@@ -28,6 +28,14 @@ class InvalidFileType(Exception):
     pass
 
 
+def sniff_media(media):
+    '''
+    Iterate through the enabled media types and find those suited
+    for a certain file.
+    '''
+    pass
+
+
 def get_media_types():
     """
     Generator, yields the available media types
diff --git a/mediagoblin/media_types/audio/__init__.py b/mediagoblin/media_types/audio/__init__.py
new file mode 100644 (file)
index 0000000..9b33f9e
--- /dev/null
@@ -0,0 +1,25 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 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/>.
+
+from mediagoblin.media_types.audio.processing import process_audio, \
+    sniff_handler
+
+MEDIA_MANAGER = {
+    'human_readable': 'Audio',
+    'processor': process_audio,
+    'sniff_handler': sniff_handler,
+    'display_template': 'mediagoblin/media_displays/audio.html',
+    'accepted_extensions': ['mp3', 'flac', 'ogg', 'wav', 'm4a']}
diff --git a/mediagoblin/media_types/audio/processing.py b/mediagoblin/media_types/audio/processing.py
new file mode 100644 (file)
index 0000000..beb1239
--- /dev/null
@@ -0,0 +1,75 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 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 logging
+import tempfile
+import os
+
+from mediagoblin import mg_globals as mgg
+from mediagoblin.processing import create_pub_filepath
+
+from mediagoblin.media_types.audio.transcoders import AudioTranscoder
+
+_log = logging.getLogger()
+
+def sniff_handler(media):
+    return True
+
+def process_audio(entry):
+    audio_config = mgg.global_config['media_type:mediagoblin.media_types.audio']
+
+    workbench = mgg.workbench_manager.create_workbench()
+
+    queued_filepath = entry.queued_media_file
+    queued_filename = workbench.localized_file(
+        mgg.queue_store, queued_filepath,
+        'source')
+
+    ogg_filepath = create_pub_filepath(
+        entry,
+        '{original}.webm'.format(
+            original=os.path.splitext(
+                queued_filepath[-1])[0]))
+
+    ogg_tmp = tempfile.NamedTemporaryFile()
+
+    with ogg_tmp:
+        transcoder = AudioTranscoder()
+
+        transcoder.transcode(
+            queued_filename,
+            ogg_tmp.name,
+            quality=audio_config['quality'])
+
+        data = transcoder.discover(ogg_tmp.name)
+
+        _log.debug('Saving medium...')
+        mgg.public_store.get_file(ogg_filepath, 'wb').write(
+            ogg_tmp.read())
+
+        entry.media_files['ogg'] = ogg_filepath
+
+        entry.media_data['audio'] = {
+            u'length': int(data.audiolength)}
+
+    thumbnail_tmp = tempfile.NamedTemporaryFile()
+
+    with thumbnail_tmp:
+        entry.media_files['thumb'] = ['fake', 'thumb', 'path.jpg']
+
+    mgg.queue_store.delete_file(queued_filepath)
+
+    entry.save()
diff --git a/mediagoblin/media_types/audio/transcoder.py b/mediagoblin/media_types/audio/transcoder.py
new file mode 100644 (file)
index 0000000..e1000fa
--- /dev/null
@@ -0,0 +1,18 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 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/>.
+
+class AudioTranscoder(object):
+    
diff --git a/mediagoblin/media_types/audio/transcoders.py b/mediagoblin/media_types/audio/transcoders.py
new file mode 100644 (file)
index 0000000..e59214b
--- /dev/null
@@ -0,0 +1,150 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 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 pdb
+import logging
+
+from mediagoblin.processing import BadMediaFail
+
+
+_log = logging.getLogger(__name__)
+
+CPU_COUNT = 2 # Just assuming for now
+
+# IMPORT MULTIPROCESSING
+try:
+    import multiprocessing
+    try:
+        CPU_COUNT = multiprocessing.cpu_count()
+    except NotImplementedError:
+        _log.warning('multiprocessing.cpu_count not implemented!\n'
+                     'Assuming 2 CPU cores')
+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')
+
+    import gst
+
+    import gst.extend.discoverer
+except ImportError:
+    raise Exception('gst/pygst >= 0.10 could not be imported')
+
+class AudioTranscoder(object):
+    def __init__(self):
+        _log.info('Initializing {0}'.format(self.__class__.__name__))
+
+        # Instantiate MainLoop
+        self._loop = gobject.MainLoop()
+
+    def discover(self, 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
+
+        # Once MainLoop has returned, return discovery data
+        return self._discovery_data
+
+    def __on_discovered(self, data, is_media):
+        if not is_media:
+            self.halt()
+            _log.error('Could not discover {0}'.format(self._src_path))
+            raise BadMediaFail()
+
+        _log.debug('Discovered: {0}'.format(data.__dict__))
+
+        self._discovery_data = data
+
+        # Gracefully shut down MainLoop
+        self.halt()
+
+    def transcode(self, src, dst, **kw):
+        self._discovery_data = kw.get('data', self.discover(src))
+
+        self.__on_progress = kw.get('progress_callback')
+
+        quality = kw.get('quality', 0.3)
+
+        # Set up pipeline
+        self.pipeline = gst.parse_launch(
+            'filesrc location="{src}" ! ' 
+            'decodebin2 ! queue ! audiorate tolerance={tolerance} ! '
+            'audioconvert ! audio/x-raw-float,channels=2 ! '
+            'vorbisenc quality={quality} ! webmmux ! '
+            'progressreport silent=true ! '
+            'filesink location="{dst}"'.format(
+                src=src,
+                tolerance=80000000,
+                quality=quality,
+                dst=dst))
+
+        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)
+
+        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)
+
+            _log.info('{0}% done...'.format(
+                    data.get('percent')))
+        elif message.type == gst.MESSAGE_EOS:
+            _log.info('Done')
+            self.halt()
+
+    def halt(self):
+        _log.info('Quitting MainLoop gracefully...')
+        gobject.idle_add(self._loop.quit)
+
+if __name__ == '__main__':
+    import sys
+    logging.basicConfig()
+    _log.setLevel(logging.INFO)
+
+    transcoder = AudioTranscoder()
+    data = transcoder.discover(sys.argv[1])
+    res = transcoder.transcode(*sys.argv[1:3])
+
+    pdb.set_trace()
index 9dc23c55bb495ef54ab7af7d3858fce27fb5303e..089d96fbf801e4b20605e89ece6dc028b57d759e 100644 (file)
@@ -31,15 +31,8 @@ _log.setLevel(logging.DEBUG)
 
 def process_video(entry):
     """
-    Code to process a video
-
-    Much of this code is derived from the arista-transcoder script in
-    the arista PyPI package and changed to match the needs of
-    MediaGoblin
-
-    This function sets up the arista video encoder in some kind of new thread
-    and attaches callbacks to that child process, hopefully, the
-    entry-complete callback will be called when the video is done.
+    Process a video entry, transcode the queued media files (originals) and
+    create a thumbnail for the entry.
     """
     video_config = mgg.global_config['media_type:mediagoblin.media_types.video']
 
@@ -54,7 +47,7 @@ def process_video(entry):
         entry,
         '{original}-640p.webm'.format(
             original=os.path.splitext(
-                queued_filepath[-1])[0]  # Select the 
+                queued_filepath[-1])[0] # Select the file name without .ext
             ))
 
     thumbnail_filepath = create_pub_filepath(
index 6137c3bf1c6e649cba793f58b3e49164d09a9931..903bd81028dbee8280b6bcdd4eb9847e0585a4c2 100644 (file)
@@ -38,17 +38,16 @@ try:
         pass
 except ImportError:
     _log.warning('Could not import multiprocessing, defaulting to 2 CPU cores')
-    pass
 
 try:
     import gtk
-except:
+except ImportError:
     raise Exception('Could not find pygtk')
 
 try:
     import gobject
     gobject.threads_init()
-except:
+except ImportError:
     raise Exception('gobject could not be found')
 
 try:
@@ -56,7 +55,7 @@ try:
     pygst.require('0.10')
     import gst
     from gst.extend import discoverer
-except:
+except ImportError:
     raise Exception('gst/pygst 0.10 could not be found')
 
 
diff --git a/mediagoblin/templates/mediagoblin/media_displays/audio.html b/mediagoblin/templates/mediagoblin/media_displays/audio.html
new file mode 100644 (file)
index 0000000..802a85c
--- /dev/null
@@ -0,0 +1,47 @@
+{#
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 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/>.
+#}
+
+{% extends 'mediagoblin/user_pages/media.html' %}
+
+{% block mediagoblin_media %}
+  <div class="audio-media">
+    <audio controls="controls"
+          preload="metadata">
+      <source src="{{ request.app.public_store.file_url(
+                      media.media_files.ogg) }}" type="video/webm; encoding=&quot;vorbis&quot;" />
+      <div class="no_html5">
+       {%- trans -%}Sorry, this audio will not work because 
+       your web browser does not support HTML5 
+       audio.{%- endtrans -%}<br/>
+       {%- trans -%}You can get a modern web browser that 
+       can play the audio at <a href="http://getfirefox.com">
+         http://getfirefox.com</a>!{%- endtrans -%}
+      </div>
+    </audio>
+  </div>
+  {% if 'original' in media.media_files %}
+  <p>
+    <a href="{{ request.app.public_store.file_url(
+            media.media_files['original']) }}">
+      {%- trans -%}
+        Original
+      {%- endtrans -%}
+    </a>
+  </p>
+  {% endif %}
+{% endblock %}