diff options
author | Joar Wandborg <git@wandborg.com> | 2012-03-27 12:05:09 +0200 |
---|---|---|
committer | Joar Wandborg <git@wandborg.com> | 2012-03-27 12:05:09 +0200 |
commit | deea3f6661df68a62d56317915ca1e71240061d4 (patch) | |
tree | 6b7708610db3500f07f494519d57eec7a0799eee /mediagoblin/media_types | |
parent | d0cb752992ce6cea2b8581dead5481591ddbb82b (diff) | |
parent | c56d4b55a169d67a3e5e5aba4271a67f0cb79c6f (diff) | |
download | mediagoblin-deea3f6661df68a62d56317915ca1e71240061d4.tar.lz mediagoblin-deea3f6661df68a62d56317915ca1e71240061d4.tar.xz mediagoblin-deea3f6661df68a62d56317915ca1e71240061d4.zip |
Merge remote-tracking branch 'joar/audio+sniffing'
Conflicts:
mediagoblin/media_types/image/processing.py
mediagoblin/media_types/video/__init__.py
mediagoblin/media_types/video/processing.py
mediagoblin/tests/test_submission.py
Diffstat (limited to 'mediagoblin/media_types')
-rw-r--r-- | mediagoblin/media_types/__init__.py | 60 | ||||
-rw-r--r-- | mediagoblin/media_types/ascii/__init__.py | 4 | ||||
-rw-r--r-- | mediagoblin/media_types/ascii/asciitoimage.py | 1 | ||||
-rw-r--r-- | mediagoblin/media_types/ascii/processing.py | 22 | ||||
-rw-r--r-- | mediagoblin/media_types/audio/__init__.py | 25 | ||||
l--------- | mediagoblin/media_types/audio/audioprocessing.py | 1 | ||||
-rw-r--r-- | mediagoblin/media_types/audio/processing.py | 131 | ||||
-rw-r--r-- | mediagoblin/media_types/audio/transcoders.py | 237 | ||||
-rw-r--r-- | mediagoblin/media_types/image/__init__.py | 4 | ||||
-rw-r--r-- | mediagoblin/media_types/image/processing.py | 55 | ||||
-rw-r--r-- | mediagoblin/media_types/video/__init__.py | 12 | ||||
-rw-r--r-- | mediagoblin/media_types/video/processing.py | 33 | ||||
-rw-r--r-- | mediagoblin/media_types/video/transcoders.py | 82 |
13 files changed, 595 insertions, 72 deletions
diff --git a/mediagoblin/media_types/__init__.py b/mediagoblin/media_types/__init__.py index 5128826b..93d2319f 100644 --- a/mediagoblin/media_types/__init__.py +++ b/mediagoblin/media_types/__init__.py @@ -16,10 +16,13 @@ import os import sys +import logging +import tempfile from mediagoblin import mg_globals from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ +_log = logging.getLogger(__name__) class FileTypeNotSupported(Exception): pass @@ -28,6 +31,35 @@ class InvalidFileType(Exception): pass +def sniff_media(media): + ''' + Iterate through the enabled media types and find those suited + for a certain file. + ''' + + try: + return get_media_type_and_manager(media.filename) + except FileTypeNotSupported: + _log.info('No media handler found by file extension. Doing it the expensive way...') + # Create a temporary file for sniffers suchs as GStreamer-based + # Audio video + media_file = tempfile.NamedTemporaryFile() + media_file.write(media.file.read()) + media.file.seek(0) + + for media_type, manager in get_media_managers(): + _log.info('Sniffing {0}'.format(media_type)) + if manager['sniff_handler'](media_file, media=media): + _log.info('{0} accepts the file'.format(media_type)) + return media_type, manager + else: + _log.debug('{0} did not accept the file'.format(media_type)) + + raise FileTypeNotSupported( + # TODO: Provide information on which file types are supported + _(u'Sorry, I don\'t support that file type :(')) + + def get_media_types(): """ Generator, yields the available media types @@ -42,7 +74,7 @@ def get_media_managers(): ''' for media_type in get_media_types(): __import__(media_type) - + yield media_type, sys.modules[media_type].MEDIA_MANAGER @@ -67,22 +99,22 @@ def get_media_manager(_media_type): def get_media_type_and_manager(filename): ''' - Get the media type and manager based on a filename + Try to find the media type based on the file name, extension + specifically. This is used as a speedup, the sniffing functionality + then falls back on more in-depth bitsniffing of the source file. ''' if filename.find('.') > 0: # Get the file extension ext = os.path.splitext(filename)[1].lower() - else: - raise InvalidFileType( - _(u'Could not extract any file extension from "{filename}"').format( - filename=filename)) - for media_type, manager in get_media_managers(): - # Omit the dot from the extension and match it against - # the media manager - if ext[1:] in manager['accepted_extensions']: - return media_type, manager + for media_type, manager in get_media_managers(): + # Omit the dot from the extension and match it against + # the media manager + if ext[1:] in manager['accepted_extensions']: + return media_type, manager else: - raise FileTypeNotSupported( - # TODO: Provide information on which file types are supported - _(u'Sorry, I don\'t support that file type :(')) + _log.info('File {0} has no file extension, let\'s hope the sniffers get it.'.format( + filename)) + + raise FileTypeNotSupported( + _(u'Sorry, I don\'t support that file type :(')) diff --git a/mediagoblin/media_types/ascii/__init__.py b/mediagoblin/media_types/ascii/__init__.py index 1c8ca562..856d1d7b 100644 --- a/mediagoblin/media_types/ascii/__init__.py +++ b/mediagoblin/media_types/ascii/__init__.py @@ -14,13 +14,15 @@ # 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.ascii.processing import process_ascii +from mediagoblin.media_types.ascii.processing import process_ascii, \ + sniff_handler MEDIA_MANAGER = { "human_readable": "ASCII", "processor": process_ascii, # alternately a string, # 'mediagoblin.media_types.image.processing'? + "sniff_handler": sniff_handler, "display_template": "mediagoblin/media_displays/ascii.html", "default_thumb": "images/media_thumbs/ascii.jpg", "accepted_extensions": [ diff --git a/mediagoblin/media_types/ascii/asciitoimage.py b/mediagoblin/media_types/ascii/asciitoimage.py index e1c4fb44..3017d2ad 100644 --- a/mediagoblin/media_types/ascii/asciitoimage.py +++ b/mediagoblin/media_types/ascii/asciitoimage.py @@ -23,6 +23,7 @@ import os _log = logging.getLogger(__name__) + class AsciiToImage(object): ''' Converter of ASCII art into image files, preserving whitespace diff --git a/mediagoblin/media_types/ascii/processing.py b/mediagoblin/media_types/ascii/processing.py index 83b5ea33..a2a52e9d 100644 --- a/mediagoblin/media_types/ascii/processing.py +++ b/mediagoblin/media_types/ascii/processing.py @@ -19,11 +19,25 @@ import Image import logging from mediagoblin import mg_globals as mgg -from mediagoblin.processing import create_pub_filepath, THUMB_SIZE +from mediagoblin.processing import create_pub_filepath from mediagoblin.media_types.ascii import asciitoimage _log = logging.getLogger(__name__) +SUPPORTED_EXTENSIONS = ['txt', 'asc', 'nfo'] + + +def sniff_handler(media_file, **kw): + if kw.get('media') is not None: + name, ext = os.path.splitext(kw['media'].filename) + clean_ext = ext[1:].lower() + + if clean_ext in SUPPORTED_EXTENSIONS: + return True + + return False + + def process_ascii(entry): ''' Code to process a txt file @@ -69,7 +83,10 @@ def process_ascii(entry): queued_file.read()) with file(tmp_thumb_filename, 'w') as thumb_file: - thumb.thumbnail(THUMB_SIZE, Image.ANTIALIAS) + thumb.thumbnail( + (mgg.global_config['media:thumb']['max_width'], + mgg.global_config['media:thumb']['max_height']), + Image.ANTIALIAS) thumb.save(thumb_file) _log.debug('Copying local file to public storage') @@ -84,7 +101,6 @@ def process_ascii(entry): as original_file: original_file.write(queued_file.read()) - queued_file.seek(0) # Rewind *again* unicode_filepath = create_pub_filepath(entry, 'ascii-portable.txt') diff --git a/mediagoblin/media_types/audio/__init__.py b/mediagoblin/media_types/audio/__init__.py new file mode 100644 index 00000000..9b33f9e3 --- /dev/null +++ b/mediagoblin/media_types/audio/__init__.py @@ -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/audioprocessing.py b/mediagoblin/media_types/audio/audioprocessing.py new file mode 120000 index 00000000..c5e3c52c --- /dev/null +++ b/mediagoblin/media_types/audio/audioprocessing.py @@ -0,0 +1 @@ +../../../extlib/freesound/audioprocessing.py
\ No newline at end of file diff --git a/mediagoblin/media_types/audio/processing.py b/mediagoblin/media_types/audio/processing.py new file mode 100644 index 00000000..62daf412 --- /dev/null +++ b/mediagoblin/media_types/audio/processing.py @@ -0,0 +1,131 @@ +# 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, BadMediaFail + +from mediagoblin.media_types.audio.transcoders import AudioTranscoder, \ + AudioThumbnailer + +_log = logging.getLogger(__name__) + +def sniff_handler(media_file, **kw): + try: + transcoder = AudioTranscoder() + data = transcoder.discover(media_file.name) + except BadMediaFail: + _log.debug('Audio discovery raised BadMediaFail') + return False + + if data.is_audio == True and data.is_video == False: + return True + + return False + +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])) + + transcoder = AudioTranscoder() + + with tempfile.NamedTemporaryFile() as ogg_tmp: + + 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)} + + if audio_config['create_spectrogram']: + spectrogram_filepath = create_pub_filepath( + entry, + '{original}-spectrogram.jpg'.format( + original=os.path.splitext( + queued_filepath[-1])[0])) + + with tempfile.NamedTemporaryFile(suffix='.wav') as wav_tmp: + _log.info('Creating WAV source for spectrogram') + transcoder.transcode( + queued_filename, + wav_tmp.name, + mux_string='wavenc') + + thumbnailer = AudioThumbnailer() + + with tempfile.NamedTemporaryFile(suffix='.jpg') as spectrogram_tmp: + thumbnailer.spectrogram( + wav_tmp.name, + spectrogram_tmp.name, + width=mgg.global_config['media:medium']['max_width']) + + _log.debug('Saving spectrogram...') + mgg.public_store.get_file(spectrogram_filepath, 'wb').write( + spectrogram_tmp.read()) + + entry.media_files['spectrogram'] = spectrogram_filepath + + with tempfile.NamedTemporaryFile(suffix='.jpg') as thumb_tmp: + thumbnailer.thumbnail_spectrogram( + spectrogram_tmp.name, + thumb_tmp.name, + (mgg.global_config['media:thumb']['max_width'], + mgg.global_config['media:thumb']['max_height'])) + + thumb_filepath = create_pub_filepath( + entry, + '{original}-thumbnail.jpg'.format( + original=os.path.splitext( + queued_filepath[-1])[0])) + + mgg.public_store.get_file(thumb_filepath, 'wb').write( + thumb_tmp.read()) + + entry.media_files['thumb'] = thumb_filepath + else: + entry.media_files['thumb'] = ['fake', 'thumb', 'path.jpg'] + + mgg.queue_store.delete_file(queued_filepath) + + entry.save() + + # clean up workbench + workbench.destroy_self() diff --git a/mediagoblin/media_types/audio/transcoders.py b/mediagoblin/media_types/audio/transcoders.py new file mode 100644 index 00000000..f84ab7f2 --- /dev/null +++ b/mediagoblin/media_types/audio/transcoders.py @@ -0,0 +1,237 @@ +# 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 PIL 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 + +# 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') + +import numpy + + +class AudioThumbnailer(object): + def __init__(self): + _log.info('Initializing {0}'.format(self.__class__.__name__)) + + def spectrogram(self, src, dst, **kw): + width = kw['width'] + 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, + numpy.hanning) + + samples_per_pixel = processor.audio_file.nframes / float(width) + + spectrogram = audioprocessing.SpectrogramImage(width, height, fft_size) + + for x in range(width): + if callback and x % (width / 10) == 0: + callback((x * 100) / width) + + seek_point = int(x * samples_per_pixel) + + (spectral_centroid, db_spectrum) = processor.spectral_centroid( + seek_point) + + spectrogram.draw_spectrum(x, db_spectrum) + + if callback: + callback(100) + + spectrogram.save(dst) + + def thumbnail_spectrogram(self, src, dst, thumb_size): + ''' + Takes a spectrogram and creates a thumbnail from it + ''' + if not (type(thumb_size) == tuple and len(thumb_size) == 2): + raise Exception('thumb_size argument should be a tuple(width, height)') + + im = Image.open(src) + + im_w, im_h = [float(i) for i in im.size] + th_w, th_h = [float(i) for i in thumb_size] + + wadsworth_position = im_w * 0.3 + + start_x = max(( + wadsworth_position - ((im_h * (th_w / th_h)) / 2.0), + 0.0)) + + stop_x = start_x + (im_h * (th_w / th_h)) + + th = im.crop(( + int(start_x), 0, + int(stop_x), int(im_h))) + + if th.size[0] > th_w or th.size[1] > th_h: + th.thumbnail(thumb_size, Image.ANTIALIAS) + + th.save(dst) + + +class AudioTranscoder(object): + def __init__(self): + _log.info('Initializing {0}'.format(self.__class__.__name__)) + + # Instantiate 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): + _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)) + + # 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)) + + 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): + if getattr(self, 'pipeline', False): + self.pipeline.set_state(gst.STATE_NULL) + del self.pipeline + _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]) + + thumbnailer = AudioThumbnailer() + + thumbnailer.spectrogram(*sys.argv[1:], width=640) + + pdb.set_trace() diff --git a/mediagoblin/media_types/image/__init__.py b/mediagoblin/media_types/image/__init__.py index 98e0c32a..d4720fab 100644 --- a/mediagoblin/media_types/image/__init__.py +++ b/mediagoblin/media_types/image/__init__.py @@ -14,13 +14,15 @@ # 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.image.processing import process_image +from mediagoblin.media_types.image.processing import process_image, \ + sniff_handler MEDIA_MANAGER = { "human_readable": "Image", "processor": process_image, # alternately a string, # 'mediagoblin.media_types.image.processing'? + "sniff_handler": sniff_handler, "display_template": "mediagoblin/media_displays/image.html", "default_thumb": "images/media_thumbs/image.jpg", "accepted_extensions": ["jpg", "jpeg", "png", "gif", "tiff"]} diff --git a/mediagoblin/media_types/image/processing.py b/mediagoblin/media_types/image/processing.py index 43a4a484..bbfcd32d 100644 --- a/mediagoblin/media_types/image/processing.py +++ b/mediagoblin/media_types/image/processing.py @@ -16,14 +16,18 @@ import Image import os +import logging from mediagoblin import mg_globals as mgg from mediagoblin.processing import BadMediaFail, \ - create_pub_filepath, THUMB_SIZE, MEDIUM_SIZE, FilenameBuilder + create_pub_filepath, FilenameBuilder from mediagoblin.tools.exif import exif_fix_image_orientation, \ extract_exif, clean_exif, get_gps_data, get_useful, \ exif_image_needs_rotation +_log = logging.getLogger(__name__) + + def resize_image(entry, filename, new_path, exif_tags, workdir, new_size, size_limits=(0, 0)): """Store a resized version of an image and return its pathname. @@ -35,18 +39,13 @@ def resize_image(entry, filename, new_path, exif_tags, workdir, new_size, exif_tags -- EXIF data for the original image workdir -- directory path for storing converted image files new_size -- 2-tuple size for the resized image - size_limits (optional) -- image is only resized if it exceeds this size - """ try: resized = Image.open(filename) except IOError: raise BadMediaFail() resized = exif_fix_image_orientation(resized, exif_tags) # Fix orientation - - if ((resized.size[0] > size_limits[0]) or - (resized.size[1] > size_limits[1])): - resized.thumbnail(new_size, Image.ANTIALIAS) + resized.thumbnail(new_size, Image.ANTIALIAS) # Copy the new file to the conversion subdir, then remotely. tmp_resized_filename = os.path.join(workdir, new_path[-1]) @@ -54,6 +53,33 @@ def resize_image(entry, filename, new_path, exif_tags, workdir, new_size, resized.save(resized_file) mgg.public_store.copy_local_to_storage(tmp_resized_filename, new_path) + +SUPPORTED_FILETYPES = ['png', 'gif', 'jpg', 'jpeg'] + + +def sniff_handler(media_file, **kw): + if kw.get('media') is not None: # That's a double negative! + name, ext = os.path.splitext(kw['media'].filename) + clean_ext = ext[1:].lower() # Strip the . from ext and make lowercase + + _log.debug('name: {0}\next: {1}\nlower_ext: {2}'.format( + name, + ext, + clean_ext)) + + if clean_ext in SUPPORTED_FILETYPES: + _log.info('Found file extension in supported filetypes') + return True + else: + _log.debug('Media present, extension not found in {0}'.format( + SUPPORTED_FILETYPES)) + else: + _log.warning('Need additional information (keyword argument \'media\')' + ' to be able to handle sniffing') + + return False + + def process_image(entry): """ Code to process an image @@ -77,19 +103,24 @@ def process_image(entry): thumb_filepath = create_pub_filepath( entry, name_builder.fill('{basename}.thumbnail{ext}')) resize_image(entry, queued_filename, thumb_filepath, - exif_tags, conversions_subdir, THUMB_SIZE) + exif_tags, conversions_subdir, + (mgg.global_config['media:thumb']['max_width'], + mgg.global_config['media:thumb']['max_height'])) # If the size of the original file exceeds the specified size of a `medium` # file, a `.medium.jpg` files is created and later associated with the media # entry. medium = Image.open(queued_filename) - if medium.size[0] > MEDIUM_SIZE[0] or medium.size[1] > MEDIUM_SIZE[1] \ - or exif_image_needs_rotation(exif_tags): + if medium.size[0] > mgg.global_config['media:medium']['max_width'] \ + or medium.size[1] > mgg.global_config['media:medium']['max_height'] \ + or exif_image_needs_rotation(exif_tags): medium_filepath = create_pub_filepath( entry, name_builder.fill('{basename}.medium{ext}')) resize_image( entry, queued_filename, medium_filepath, - exif_tags, conversions_subdir, MEDIUM_SIZE, MEDIUM_SIZE) + exif_tags, conversions_subdir, + (mgg.global_config['media:medium']['max_width'], + mgg.global_config['media:medium']['max_height'])) else: medium_filepath = None @@ -99,7 +130,7 @@ def process_image(entry): with queued_file: original_filepath = create_pub_filepath( - entry, name_builder.fill('{basename}{ext}') ) + entry, name_builder.fill('{basename}{ext}')) with mgg.public_store.get_file(original_filepath, 'wb') \ as original_file: diff --git a/mediagoblin/media_types/video/__init__.py b/mediagoblin/media_types/video/__init__.py index 579fdc6a..3faa5b9f 100644 --- a/mediagoblin/media_types/video/__init__.py +++ b/mediagoblin/media_types/video/__init__.py @@ -14,16 +14,16 @@ # 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.video.processing import process_video +from mediagoblin.media_types.video.processing import process_video, \ + sniff_handler MEDIA_MANAGER = { "human_readable": "Video", - "processor": process_video, # alternately a string, - # 'mediagoblin.media_types.image.processing'? + "processor": process_video, # alternately a string, + # 'mediagoblin.media_types.image.processing'? + "sniff_handler": sniff_handler, "display_template": "mediagoblin/media_displays/video.html", "default_thumb": "images/media_thumbs/video.jpg", - # TODO: This list should be autogenerated based on gst plugins "accepted_extensions": [ - "mp4", "mov", "webm", "avi", "3gp", "3gpp", "mkv", "ogv", "ogg", - "m4v"]} + "mp4", "mov", "webm", "avi", "3gp", "3gpp", "mkv", "ogv", "m4v"]} diff --git a/mediagoblin/media_types/video/processing.py b/mediagoblin/media_types/video/processing.py index 24c03648..6a5ce364 100644 --- a/mediagoblin/media_types/video/processing.py +++ b/mediagoblin/media_types/video/processing.py @@ -20,7 +20,7 @@ import os from mediagoblin import mg_globals as mgg from mediagoblin.processing import mark_entry_failed, \ - THUMB_SIZE, MEDIUM_SIZE, create_pub_filepath, FilenameBuilder + create_pub_filepath, FilenameBuilder from . import transcoders logging.basicConfig() @@ -29,17 +29,27 @@ _log = logging.getLogger(__name__) _log.setLevel(logging.DEBUG) -def process_video(entry): - """ - Code to process a video +def sniff_handler(media_file, **kw): + transcoder = transcoders.VideoTranscoder() + data = transcoder.discover(media_file.name) + + _log.debug('Discovered: {0}'.format(data)) + + if not data: + _log.error('Could not discover {0}'.format( + kw.get('media'))) + return False - Much of this code is derived from the arista-transcoder script in - the arista PyPI package and changed to match the needs of - MediaGoblin + if data['is_video'] == True: + return True - 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. + return False + + +def process_video(entry): + """ + 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'] @@ -62,7 +72,8 @@ def process_video(entry): with tmp_dst: # Transcode queued file to a VP8/vorbis file that fits in a 640x640 square - transcoder = transcoders.VideoTranscoder(queued_filename, tmp_dst.name) + transcoder = transcoders.VideoTranscoder() + transcoder.transcode(queued_filename, tmp_dst.name) # Push transcoded video to public storage _log.debug('Saving medium...') diff --git a/mediagoblin/media_types/video/transcoders.py b/mediagoblin/media_types/video/transcoders.py index 6137c3bf..e0bd0d3d 100644 --- a/mediagoblin/media_types/video/transcoders.py +++ b/mediagoblin/media_types/video/transcoders.py @@ -21,12 +21,9 @@ os.putenv('GST_DEBUG_DUMP_DOT_DIR', '/tmp') import sys import logging -import pdb import urllib _log = logging.getLogger(__name__) -logging.basicConfig() -_log.setLevel(logging.DEBUG) CPU_COUNT = 2 try: @@ -38,17 +35,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 +52,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') @@ -270,7 +266,7 @@ class VideoThumbnailer: return 0 try: - return pipeline.query_duration(gst.FORMAT_TIME)[0] + return pipeline.query_duration(gst.FORMAT_TIME)[0] except gst.QueryError: return self._get_duration(pipeline, retries + 1) @@ -320,12 +316,11 @@ class VideoThumbnailer: self.bus.disconnect(self.watch_id) self.bus = None - def __halt_final(self): _log.info('Done') if self.errors: _log.error(','.join(self.errors)) - + self.loop.quit() @@ -341,10 +336,15 @@ class VideoTranscoder: that it was refined afterwards and therefore is done more correctly. ''' - def __init__(self, src, dst, **kwargs): + def __init__(self): _log.info('Initializing VideoTranscoder...') self.loop = gobject.MainLoop() + + def transcode(self, src, dst, **kwargs): + ''' + Transcode a video file into a 'medium'-sized version. + ''' self.source_path = src self.destination_path = dst @@ -358,6 +358,34 @@ class VideoTranscoder: self._setup() self._run() + def discover(self, src): + ''' + Discover properties about a media file + ''' + _log.info('Discovering {0}'.format(src)) + + self.source_path = src + self._setup_discover(discovered_callback=self.__on_discovered) + + self.discoverer.discover() + + self.loop.run() + + if hasattr(self, '_discovered_data'): + return self._discovered_data.__dict__ + else: + return None + + def __on_discovered(self, data, is_media): + _log.debug('Discovered: {0}'.format(data)) + if not is_media: + self.__stop() + raise Exception('Could not discover {0}'.format(self.source_path)) + + self._discovered_data = data + + self.__stop_mainloop() + def _setup(self): self._setup_discover() self._setup_pipeline() @@ -370,12 +398,14 @@ class VideoTranscoder: _log.debug('Initializing MainLoop()') self.loop.run() - def _setup_discover(self): + def _setup_discover(self, **kw): _log.debug('Setting up discoverer') self.discoverer = discoverer.Discoverer(self.source_path) # Connect self.__discovered to the 'discovered' event - self.discoverer.connect('discovered', self.__discovered) + self.discoverer.connect( + 'discovered', + kw.get('discovered_callback', self.__discovered)) def __discovered(self, data, is_media): ''' @@ -422,7 +452,7 @@ class VideoTranscoder: self.ffmpegcolorspace = gst.element_factory_make( 'ffmpegcolorspace', 'ffmpegcolorspace') self.pipeline.add(self.ffmpegcolorspace) - + self.videoscale = gst.element_factory_make('ffvideoscale', 'videoscale') #self.videoscale.set_property('method', 2) # I'm not sure this works #self.videoscale.set_property('add-borders', 0) @@ -516,7 +546,6 @@ class VideoTranscoder: # Setup the message bus and connect _on_message to the pipeline self._setup_bus() - def _on_dynamic_pad(self, dbin, pad, islast): ''' Callback called when ``decodebin2`` has a pad that we can connect to @@ -561,11 +590,11 @@ class VideoTranscoder: t = message.type - if t == gst.MESSAGE_EOS: + if message.type == gst.MESSAGE_EOS: self._discover_dst_and_stop() _log.info('Done') - elif t == gst.MESSAGE_ELEMENT: + elif message.type == gst.MESSAGE_ELEMENT: if message.structure.get_name() == 'progress': data = dict(message.structure) @@ -587,7 +616,6 @@ class VideoTranscoder: self.dst_discoverer.discover() - def __dst_discovered(self, data, is_media): self.dst_data = data @@ -596,8 +624,9 @@ class VideoTranscoder: def __stop(self): _log.debug(self.loop) - # Stop executing the pipeline - self.pipeline.set_state(gst.STATE_NULL) + if hasattr(self, 'pipeline'): + # Stop executing the pipeline + self.pipeline.set_state(gst.STATE_NULL) # This kills the loop, mercifully gobject.idle_add(self.__stop_mainloop) @@ -615,14 +644,15 @@ class VideoTranscoder: if __name__ == '__main__': os.nice(19) + logging.basicConfig() from optparse import OptionParser parser = OptionParser( - usage='%prog [-v] -a [ video | thumbnail ] SRC DEST') + usage='%prog [-v] -a [ video | thumbnail | discover ] SRC [ DEST ]') parser.add_option('-a', '--action', dest='action', - help='One of "video" or "thumbnail"') + help='One of "video", "discover" or "thumbnail"') parser.add_option('-v', dest='verbose', @@ -646,13 +676,17 @@ if __name__ == '__main__': _log.debug(args) - if not len(args) == 2: + if not len(args) == 2 and not options.action == 'discover': parser.print_help() sys.exit() + transcoder = VideoTranscoder() + if options.action == 'thumbnail': VideoThumbnailer(*args) elif options.action == 'video': def cb(data): print('I\'m a callback!') - transcoder = VideoTranscoder(*args, progress_callback=cb) + transcoder.transcode(*args, progress_callback=cb) + elif options.action == 'discover': + print transcoder.discover(*args).__dict__ |