From 57d8212a796e4952955c047efa61616aad006040 Mon Sep 17 00:00:00 2001 From: Boris Bobrov Date: Fri, 13 Jun 2014 10:02:10 +0400 Subject: [PATCH] Port of audio to GStreamer 1.0 Includes: - transcoders - thumbs - tests --- mediagoblin/media_types/audio/processing.py | 25 +--- mediagoblin/media_types/audio/transcoders.py | 164 ++++++++++++--------------- mediagoblin/media_types/video/transcoders.py | 5 +- mediagoblin/tests/test_audio.py | 104 +++++++++++++++++ 4 files changed, 181 insertions(+), 117 deletions(-) create mode 100644 mediagoblin/tests/test_audio.py diff --git a/mediagoblin/media_types/audio/processing.py b/mediagoblin/media_types/audio/processing.py index de6fa9ca..770342ff 100644 --- a/mediagoblin/media_types/audio/processing.py +++ b/mediagoblin/media_types/audio/processing.py @@ -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, diff --git a/mediagoblin/media_types/audio/transcoders.py b/mediagoblin/media_types/audio/transcoders.py index 150dad8e..f86528de 100644 --- a/mediagoblin/media_types/audio/transcoders.py +++ b/mediagoblin/media_types/audio/transcoders.py @@ -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 diff --git a/mediagoblin/media_types/video/transcoders.py b/mediagoblin/media_types/video/transcoders.py index d53cabc6..20f21697 100644 --- a/mediagoblin/media_types/video/transcoders.py +++ b/mediagoblin/media_types/video/transcoders.py @@ -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 index 00000000..740d9cdd --- /dev/null +++ b/mediagoblin/tests/test_audio.py @@ -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 . + +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' -- 2.11.4.GIT