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 (
|
from mediagoblin.media_types.audio.transcoders import (
|
||||||
AudioTranscoder, AudioThumbnailer)
|
AudioTranscoder, AudioThumbnailer)
|
||||||
|
from mediagoblin.media_types.tools import discover
|
||||||
|
|
||||||
_log = logging.getLogger(__name__)
|
_log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -35,16 +36,9 @@ MEDIA_TYPE = 'mediagoblin.media_types.audio'
|
|||||||
|
|
||||||
def sniff_handler(media_file, filename):
|
def sniff_handler(media_file, filename):
|
||||||
_log.info('Sniffing {0}'.format(MEDIA_TYPE))
|
_log.info('Sniffing {0}'.format(MEDIA_TYPE))
|
||||||
try:
|
data = discover(media_file.name)
|
||||||
transcoder = AudioTranscoder()
|
if data and data.get_audio_streams() and not data.get_video_streams():
|
||||||
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:
|
|
||||||
return MEDIA_TYPE
|
return MEDIA_TYPE
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -126,8 +120,6 @@ class CommonAudioProcessor(MediaProcessor):
|
|||||||
quality=quality,
|
quality=quality,
|
||||||
progress_callback=progress_callback)
|
progress_callback=progress_callback)
|
||||||
|
|
||||||
self.transcoder.discover(webm_audio_tmp)
|
|
||||||
|
|
||||||
self._keep_best()
|
self._keep_best()
|
||||||
|
|
||||||
_log.debug('Saving medium...')
|
_log.debug('Saving medium...')
|
||||||
@ -145,21 +137,14 @@ class CommonAudioProcessor(MediaProcessor):
|
|||||||
if self._skip_processing('spectrogram', max_width=max_width,
|
if self._skip_processing('spectrogram', max_width=max_width,
|
||||||
fft_size=fft_size):
|
fft_size=fft_size):
|
||||||
return
|
return
|
||||||
|
|
||||||
wav_tmp = os.path.join(self.workbench.dir, self.name_builder.fill(
|
wav_tmp = os.path.join(self.workbench.dir, self.name_builder.fill(
|
||||||
'{basename}.ogg'))
|
'{basename}.ogg'))
|
||||||
|
|
||||||
_log.info('Creating OGG source for spectrogram')
|
_log.info('Creating OGG source for spectrogram')
|
||||||
self.transcoder.transcode(
|
self.transcoder.transcode(self.process_filename, wav_tmp,
|
||||||
self.process_filename,
|
mux_name='oggmux')
|
||||||
wav_tmp,
|
|
||||||
mux_string='vorbisenc quality={0} ! oggmux'.format(
|
|
||||||
self.audio_config['quality']))
|
|
||||||
|
|
||||||
spectrogram_tmp = os.path.join(self.workbench.dir,
|
spectrogram_tmp = os.path.join(self.workbench.dir,
|
||||||
self.name_builder.fill(
|
self.name_builder.fill(
|
||||||
'{basename}-spectrogram.jpg'))
|
'{basename}-spectrogram.jpg'))
|
||||||
|
|
||||||
self.thumbnailer.spectrogram(
|
self.thumbnailer.spectrogram(
|
||||||
wav_tmp,
|
wav_tmp,
|
||||||
spectrogram_tmp,
|
spectrogram_tmp,
|
||||||
|
@ -20,10 +20,8 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
import Image
|
import Image
|
||||||
|
|
||||||
from mediagoblin.processing import BadMediaFail
|
|
||||||
from mediagoblin.media_types.audio import audioprocessing
|
from mediagoblin.media_types.audio import audioprocessing
|
||||||
|
|
||||||
|
|
||||||
_log = logging.getLogger(__name__)
|
_log = logging.getLogger(__name__)
|
||||||
|
|
||||||
CPU_COUNT = 2 # Just assuming for now
|
CPU_COUNT = 2 # Just assuming for now
|
||||||
@ -39,26 +37,13 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
_log.warning('Could not import multiprocessing, assuming 2 CPU cores')
|
_log.warning('Could not import multiprocessing, assuming 2 CPU cores')
|
||||||
|
|
||||||
# IMPORT GOBJECT
|
# uncomment this to get a lot of logs from gst
|
||||||
try:
|
# import os;os.environ['GST_DEBUG'] = '5,python:5'
|
||||||
import gobject
|
|
||||||
gobject.threads_init()
|
|
||||||
except ImportError:
|
|
||||||
raise Exception('gobject could not be found')
|
|
||||||
|
|
||||||
# IMPORT PYGST
|
import gi
|
||||||
try:
|
gi.require_version('Gst', '1.0')
|
||||||
import pygst
|
from gi.repository import GObject, Gst
|
||||||
|
Gst.init(None)
|
||||||
# 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 numpy
|
import numpy
|
||||||
|
|
||||||
@ -72,7 +57,6 @@ class AudioThumbnailer(object):
|
|||||||
height = int(kw.get('height', float(width) * 0.3))
|
height = int(kw.get('height', float(width) * 0.3))
|
||||||
fft_size = kw.get('fft_size', 2048)
|
fft_size = kw.get('fft_size', 2048)
|
||||||
callback = kw.get('progress_callback')
|
callback = kw.get('progress_callback')
|
||||||
|
|
||||||
processor = audioprocessing.AudioProcessor(
|
processor = audioprocessing.AudioProcessor(
|
||||||
src,
|
src,
|
||||||
fft_size,
|
fft_size,
|
||||||
@ -132,95 +116,87 @@ class AudioTranscoder(object):
|
|||||||
_log.info('Initializing {0}'.format(self.__class__.__name__))
|
_log.info('Initializing {0}'.format(self.__class__.__name__))
|
||||||
|
|
||||||
# Instantiate MainLoop
|
# Instantiate MainLoop
|
||||||
self._loop = gobject.MainLoop()
|
self._loop = GObject.MainLoop()
|
||||||
self._failed = None
|
self._failed = None
|
||||||
|
|
||||||
def discover(self, src):
|
def transcode(self, src, dst, mux_name='webmmux',quality=0.3,
|
||||||
self._src_path = src
|
progress_callback=None, **kw):
|
||||||
_log.info('Discovering {0}'.format(src))
|
def _on_pad_added(element, pad, connect_to):
|
||||||
self._discovery_path = src
|
caps = pad.query_caps(None)
|
||||||
|
name = caps.to_string()
|
||||||
self._discoverer = gst.extend.discoverer.Discoverer(
|
_log.debug('on_pad_added: {0}'.format(name))
|
||||||
self._discovery_path)
|
if name.startswith('audio') and not connect_to.is_linked():
|
||||||
self._discoverer.connect('discovered', self.__on_discovered)
|
pad.link(connect_to)
|
||||||
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):
|
|
||||||
_log.info('Transcoding {0} into {1}'.format(src, dst))
|
_log.info('Transcoding {0} into {1}'.format(src, dst))
|
||||||
self._discovery_data = kw.get('data', self.discover(src))
|
self.__on_progress = progress_callback
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
# Set up pipeline
|
# Set up pipeline
|
||||||
self.pipeline = gst.parse_launch(
|
tolerance = 80000000
|
||||||
'filesrc location="{src}" ! '
|
self.pipeline = Gst.Pipeline()
|
||||||
'decodebin2 ! queue ! audiorate tolerance={tolerance} ! '
|
filesrc = Gst.ElementFactory.make('filesrc', 'filesrc')
|
||||||
'audioconvert ! audio/x-raw-float,channels=2 ! '
|
filesrc.set_property('location', src)
|
||||||
'{mux_string} ! '
|
decodebin = Gst.ElementFactory.make('decodebin', 'decodebin')
|
||||||
'progressreport silent=true ! '
|
queue = Gst.ElementFactory.make('queue', 'queue')
|
||||||
'filesink location="{dst}"'.format(
|
decodebin.connect('pad-added', _on_pad_added,
|
||||||
src=src,
|
queue.get_static_pad('sink'))
|
||||||
tolerance=80000000,
|
audiorate = Gst.ElementFactory.make('audiorate', 'audiorate')
|
||||||
mux_string=mux_string,
|
audiorate.set_property('tolerance', tolerance)
|
||||||
dst=dst))
|
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 = self.pipeline.get_bus()
|
||||||
self.bus.add_signal_watch()
|
self.bus.add_signal_watch()
|
||||||
self.bus.connect('message', self.__on_bus_message)
|
self.bus.connect('message', self.__on_bus_message)
|
||||||
|
# run
|
||||||
self.pipeline.set_state(gst.STATE_PLAYING)
|
self.pipeline.set_state(Gst.State.PLAYING)
|
||||||
|
|
||||||
self._loop.run()
|
self._loop.run()
|
||||||
|
|
||||||
def __on_bus_message(self, bus, message):
|
def __on_bus_message(self, bus, message):
|
||||||
_log.debug(message)
|
_log.debug(message.type)
|
||||||
|
if (message.type == Gst.MessageType.ELEMENT
|
||||||
if (message.type == gst.MESSAGE_ELEMENT
|
and message.has_name('progress')):
|
||||||
and message.structure.get_name() == 'progress'):
|
structure = message.get_structure()
|
||||||
data = dict(message.structure)
|
(success, percent) = structure.get_int('percent')
|
||||||
|
if self.__on_progress and success:
|
||||||
if self.__on_progress:
|
self.__on_progress(percent)
|
||||||
self.__on_progress(data.get('percent'))
|
_log.info('{0}% done...'.format(percent))
|
||||||
|
elif message.type == Gst.MessageType.EOS:
|
||||||
_log.info('{0}% done...'.format(
|
|
||||||
data.get('percent')))
|
|
||||||
elif message.type == gst.MESSAGE_EOS:
|
|
||||||
_log.info('Done')
|
_log.info('Done')
|
||||||
self.halt()
|
self.halt()
|
||||||
|
elif message.type == Gst.MessageType.ERROR:
|
||||||
|
_log.error(message.parse_error())
|
||||||
|
self.halt()
|
||||||
|
|
||||||
def halt(self):
|
def halt(self):
|
||||||
if getattr(self, 'pipeline', False):
|
if getattr(self, 'pipeline', False):
|
||||||
self.pipeline.set_state(gst.STATE_NULL)
|
self.pipeline.set_state(Gst.State.NULL)
|
||||||
del self.pipeline
|
del self.pipeline
|
||||||
_log.info('Quitting MainLoop gracefully...')
|
_log.info('Quitting MainLoop gracefully...')
|
||||||
gobject.idle_add(self._loop.quit)
|
GObject.idle_add(self._loop.quit)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import sys
|
import sys
|
||||||
|
@ -239,7 +239,6 @@ class VideoTranscoder(object):
|
|||||||
|
|
||||||
self.audioconvert = Gst.ElementFactory.make('audioconvert', 'audioconvert')
|
self.audioconvert = Gst.ElementFactory.make('audioconvert', 'audioconvert')
|
||||||
self.pipeline.add(self.audioconvert)
|
self.pipeline.add(self.audioconvert)
|
||||||
|
|
||||||
self.audiocapsfilter = Gst.ElementFactory.make('capsfilter',
|
self.audiocapsfilter = Gst.ElementFactory.make('capsfilter',
|
||||||
'audiocapsfilter')
|
'audiocapsfilter')
|
||||||
audiocaps = Gst.Caps.new_empty()
|
audiocaps = Gst.Caps.new_empty()
|
||||||
@ -288,8 +287,7 @@ class VideoTranscoder(object):
|
|||||||
self.capsfilter.link(self.vp8enc)
|
self.capsfilter.link(self.vp8enc)
|
||||||
self.vp8enc.link(self.webmmux)
|
self.vp8enc.link(self.webmmux)
|
||||||
|
|
||||||
if self.data.is_audio:
|
if self.data.get_audio_streams():
|
||||||
# Link all the audio elements in a row to webmmux
|
|
||||||
self.audioqueue.link(self.audiorate)
|
self.audioqueue.link(self.audiorate)
|
||||||
self.audiorate.link(self.audioconvert)
|
self.audiorate.link(self.audioconvert)
|
||||||
self.audioconvert.link(self.audiocapsfilter)
|
self.audioconvert.link(self.audiocapsfilter)
|
||||||
@ -310,6 +308,7 @@ class VideoTranscoder(object):
|
|||||||
if (self.videorate.get_static_pad('sink').get_pad_template()
|
if (self.videorate.get_static_pad('sink').get_pad_template()
|
||||||
.get_caps().intersect(pad.query_caps()).is_empty()):
|
.get_caps().intersect(pad.query_caps()).is_empty()):
|
||||||
# It is NOT a video src pad.
|
# It is NOT a video src pad.
|
||||||
|
_log.debug('linking audio to the pad dynamically')
|
||||||
pad.link(self.audioqueue.get_static_pad('sink'))
|
pad.link(self.audioqueue.get_static_pad('sink'))
|
||||||
else:
|
else:
|
||||||
# It IS a video src pad.
|
# 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