Port of audio to GStreamer 1.0

Includes:
 - transcoders
 - thumbs
 - tests
This commit is contained in:
Boris Bobrov 2014-06-13 10:02:10 +04:00
parent 91f5f5e791
commit 57d8212a79
4 changed files with 181 additions and 117 deletions

View 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,

View 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')
# uncomment this to get a lot of logs from gst
# import os;os.environ['GST_DEBUG'] = '5,python:5'
# 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')
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

View 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.

View File

@ -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'