Port of audio to GStreamer 1.0
Includes: - transcoders - thumbs - tests
This commit is contained in:
parent
91f5f5e791
commit
57d8212a79
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
104
mediagoblin/tests/test_audio.py
Normal file
104
mediagoblin/tests/test_audio.py
Normal 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'
|
Loading…
x
Reference in New Issue
Block a user