Multimedia refractoring, and added video thumbnail support
This commit is contained in:
parent
62be795e91
commit
26729e0277
@ -17,6 +17,9 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from mediagoblin.util import lazy_pass_to_ugettext as _
|
||||
|
||||
|
||||
class FileTypeNotSupported(Exception):
|
||||
pass
|
||||
|
||||
|
@ -17,16 +17,17 @@
|
||||
import Image
|
||||
import tempfile
|
||||
import pkg_resources
|
||||
import os
|
||||
|
||||
from celery.task import Task
|
||||
from celery import registry
|
||||
|
||||
from mediagoblin.db.util import ObjectId
|
||||
from mediagoblin import mg_globals as mgg
|
||||
|
||||
from mediagoblin.util import lazy_pass_to_ugettext as _
|
||||
|
||||
import mediagoblin.media_types.video
|
||||
from mediagoblin.process_media.errors import BaseProcessingFail, BadMediaFail
|
||||
from mediagoblin.process_media import mark_entry_failed
|
||||
from . import transcoders
|
||||
|
||||
import gobject
|
||||
gobject.threads_init()
|
||||
@ -39,10 +40,12 @@ from arista.transcoder import TranscoderOptions
|
||||
|
||||
THUMB_SIZE = 180, 180
|
||||
MEDIUM_SIZE = 640, 640
|
||||
ARISTA_DEVICE_KEY = 'web'
|
||||
|
||||
ARISTA_DEVICE = 'devices/web-advanced.json'
|
||||
ARISTA_PRESET = None
|
||||
|
||||
loop = None # Is this even used?
|
||||
|
||||
loop = None
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig()
|
||||
logger.setLevel(logging.DEBUG)
|
||||
@ -51,10 +54,20 @@ logger.setLevel(logging.DEBUG)
|
||||
def process_video(entry):
|
||||
"""
|
||||
Code to process a video
|
||||
|
||||
Much of this code is derived from the arista-transcoder script in
|
||||
the arista PyPI package and changed to match the needs of
|
||||
MediaGoblin
|
||||
|
||||
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.
|
||||
"""
|
||||
global loop
|
||||
loop = None
|
||||
|
||||
''' Empty dict, will store data which will be passed to the callback
|
||||
functions '''
|
||||
info = {}
|
||||
|
||||
workbench = mgg.workbench_manager.create_workbench()
|
||||
|
||||
queued_filepath = entry['queued_media_file']
|
||||
@ -62,29 +75,36 @@ def process_video(entry):
|
||||
mgg.queue_store, queued_filepath,
|
||||
'source')
|
||||
|
||||
''' Initialize arista '''
|
||||
arista.init()
|
||||
|
||||
''' Loads a preset file which specifies the format of the output video'''
|
||||
device = arista.presets.load(
|
||||
pkg_resources.resource_filename(
|
||||
__name__,
|
||||
ARISTA_DEVICE))
|
||||
|
||||
web_advanced_preset = pkg_resources.resource_filename(
|
||||
__name__,
|
||||
'presets/web-advanced.json')
|
||||
device = arista.presets.load(web_advanced_preset)
|
||||
|
||||
# FIXME: Is this needed since we only transcode one video?
|
||||
queue = arista.queue.TranscodeQueue()
|
||||
|
||||
info['tmp_file'] = tmp_file = tempfile.NamedTemporaryFile()
|
||||
info['tmp_file'] = tempfile.NamedTemporaryFile(delete=False)
|
||||
|
||||
info['medium_filepath'] = medium_filepath = create_pub_filepath(entry, 'video.webm')
|
||||
info['medium_filepath'] = create_pub_filepath(
|
||||
entry, 'video.webm')
|
||||
|
||||
output = tmp_file.name
|
||||
info['thumb_filepath'] = create_pub_filepath(
|
||||
entry, 'thumbnail.jpg')
|
||||
|
||||
uri = 'file://' + queued_filename
|
||||
|
||||
preset = device.presets[device.default]
|
||||
# With the web-advanced.json device preset, this will select
|
||||
# 480p WebM w/ OGG Vorbis
|
||||
preset = device.presets[ARISTA_PRESET or device.default]
|
||||
|
||||
logger.debug('preset: {0}'.format(preset))
|
||||
|
||||
opts = TranscoderOptions(uri, preset, output)
|
||||
opts = TranscoderOptions(
|
||||
'file://' + queued_filename, # Arista did it this way, IIRC
|
||||
preset,
|
||||
info['tmp_file'].name)
|
||||
|
||||
queue.append(opts)
|
||||
|
||||
@ -95,68 +115,78 @@ def process_video(entry):
|
||||
queue.connect("entry-error", _transcoding_error, info)
|
||||
queue.connect("entry-complete", _transcoding_complete, info)
|
||||
|
||||
info['loop'] = loop = gobject.MainLoop()
|
||||
# Add data to the info dict, making it available to the callbacks
|
||||
info['loop'] = gobject.MainLoop()
|
||||
info['queued_filename'] = queued_filename
|
||||
info['queued_filepath'] = queued_filepath
|
||||
info['workbench'] = workbench
|
||||
info['preset'] = preset
|
||||
|
||||
info['loop'].run()
|
||||
|
||||
logger.debug('info: {0}'.format(info))
|
||||
|
||||
loop.run()
|
||||
|
||||
def __create_thumbnail(info):
|
||||
thumbnail = tempfile.NamedTemporaryFile()
|
||||
|
||||
logger.info('thumbnailing...')
|
||||
transcoders.VideoThumbnailer(info['tmp_file'].name, thumbnail.name)
|
||||
logger.debug('Done thumbnailing')
|
||||
|
||||
os.remove(info['tmp_file'].name)
|
||||
|
||||
mgg.public_store.get_file(info['thumb_filepath'], 'wb').write(
|
||||
thumbnail.read())
|
||||
|
||||
info['entry']['media_files']['thumb'] = info['thumb_filepath']
|
||||
info['entry'].save()
|
||||
|
||||
|
||||
def __close_processing(queue, qentry, info, **kwargs):
|
||||
'''
|
||||
try:
|
||||
#thumb = Image.open(mediagoblin.media_types.video.MEDIA_MANAGER['default_thumb'])
|
||||
except IOError:
|
||||
raise BadMediaFail()
|
||||
|
||||
thumb.thumbnail(THUMB_SIZE, Image.ANTIALIAS)
|
||||
# ensure color mode is compatible with jpg
|
||||
if thumb.mode != "RGB":
|
||||
thumb = thumb.convert("RGB")
|
||||
|
||||
thumb_filepath = create_pub_filepath(entry, 'thumbnail.jpg')
|
||||
thumb_file = mgg.public_store.get_file(thumb_filepath, 'w')
|
||||
|
||||
with thumb_file:
|
||||
thumb.save(thumb_file, "JPEG", quality=90)
|
||||
Updates MediaEntry, moves files, handles errors
|
||||
'''
|
||||
if not kwargs.get('error'):
|
||||
logger.info('Transcoding successful')
|
||||
|
||||
def __close_processing(queue, qentry, info, error=False):
|
||||
'''
|
||||
Update MediaEntry, move files, handle errors
|
||||
'''
|
||||
if not error:
|
||||
qentry.transcoder.stop()
|
||||
gobject.idle_add(info['loop'].quit)
|
||||
info['loop'].quit()
|
||||
info['loop'].quit() # Do I have to do this again?
|
||||
|
||||
print('\n-> Saving video...\n')
|
||||
logger.info('Saving files...')
|
||||
|
||||
# Write the transcoded media to the storage system
|
||||
with info['tmp_file'] as tmp_file:
|
||||
mgg.public_store.get_file(info['medium_filepath'], 'wb').write(
|
||||
tmp_file.read())
|
||||
info['entry']['media_files']['medium'] = info['medium_filepath']
|
||||
|
||||
print('\n=== DONE! ===\n')
|
||||
|
||||
# we have to re-read because unlike PIL, not everything reads
|
||||
# things in string representation :)
|
||||
queued_file = file(info['queued_filename'], 'rb')
|
||||
|
||||
with queued_file:
|
||||
original_filepath = create_pub_filepath(info['entry'], info['queued_filepath'][-1])
|
||||
original_filepath = create_pub_filepath(
|
||||
info['entry'],
|
||||
info['queued_filepath'][-1])
|
||||
|
||||
with mgg.public_store.get_file(original_filepath, 'wb') as original_file:
|
||||
with mgg.public_store.get_file(original_filepath, 'wb') as \
|
||||
original_file:
|
||||
original_file.write(queued_file.read())
|
||||
|
||||
mgg.queue_store.delete_file(info['queued_filepath'])
|
||||
|
||||
|
||||
logger.debug('...Done')
|
||||
|
||||
info['entry']['queued_media_file'] = []
|
||||
media_files_dict = info['entry'].setdefault('media_files', {})
|
||||
media_files_dict['original'] = original_filepath
|
||||
# media_files_dict['thumb'] = thumb_filepath
|
||||
|
||||
info['entry']['state'] = u'processed'
|
||||
info['entry']['media_data'][u'preset'] = info['preset'].name
|
||||
__create_thumbnail(info)
|
||||
info['entry'].save()
|
||||
|
||||
else:
|
||||
@ -174,17 +204,20 @@ def _transcoding_start(queue, qentry, info):
|
||||
logger.info('-> Starting transcoding')
|
||||
logger.debug(queue, qentry, info)
|
||||
|
||||
|
||||
def _transcoding_complete(*args):
|
||||
__close_processing(*args)
|
||||
print(args)
|
||||
|
||||
def _transcoding_error(*args):
|
||||
logger.info('-> Error')
|
||||
__close_processing(*args, error=True)
|
||||
logger.debug(*args)
|
||||
|
||||
def _transcoding_error(queue, qentry, info):
|
||||
logger.info('Error')
|
||||
__close_processing(queue, qentry, info, error=True)
|
||||
logger.debug(queue, quentry, info)
|
||||
|
||||
|
||||
def _transcoding_pass_setup(queue, qentry, options):
|
||||
logger.info('-> Pass setup')
|
||||
logger.info('Pass setup')
|
||||
logger.debug(queue, qentry, options)
|
||||
|
||||
|
||||
@ -212,34 +245,6 @@ def create_pub_filepath(entry, filename):
|
||||
filename])
|
||||
|
||||
|
||||
class BaseProcessingFail(Exception):
|
||||
"""
|
||||
Base exception that all other processing failure messages should
|
||||
subclass from.
|
||||
|
||||
You shouldn't call this itself; instead you should subclass it
|
||||
and provid the exception_path and general_message applicable to
|
||||
this error.
|
||||
"""
|
||||
general_message = u''
|
||||
|
||||
@property
|
||||
def exception_path(self):
|
||||
return u"%s:%s" % (
|
||||
self.__class__.__module__, self.__class__.__name__)
|
||||
|
||||
def __init__(self, **metadata):
|
||||
self.metadata = metadata or {}
|
||||
|
||||
|
||||
class BadMediaFail(BaseProcessingFail):
|
||||
"""
|
||||
Error that should be raised when an inappropriate file was given
|
||||
for the media type specified.
|
||||
"""
|
||||
general_message = _(u'Invalid file given for media type.')
|
||||
|
||||
|
||||
################################
|
||||
# Media processing initial steps
|
||||
################################
|
||||
@ -311,4 +316,3 @@ def mark_entry_failed(entry_id, exc):
|
||||
{'$set': {u'state': u'failed',
|
||||
u'fail_error': None,
|
||||
u'fail_metadata': {}}})
|
||||
|
||||
|
113
mediagoblin/media_types/video/transcoders.py
Normal file
113
mediagoblin/media_types/video/transcoders.py
Normal file
@ -0,0 +1,113 @@
|
||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||
# Copyright (C) 2011 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 sys
|
||||
import logging
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
logging.basicConfig()
|
||||
_log.setLevel(logging.INFO)
|
||||
|
||||
try:
|
||||
import gobject
|
||||
except:
|
||||
_log.error('Could not import gobject')
|
||||
|
||||
try:
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
except:
|
||||
_log.error('pygst could not be imported')
|
||||
|
||||
class VideoThumbnailer:
|
||||
def __init__(self, src, dst):
|
||||
self._set_up_pass(src, dst)
|
||||
|
||||
self.loop = gobject.MainLoop()
|
||||
self.loop.run()
|
||||
|
||||
def _set_up_pass(self, src, dst):
|
||||
self.pipeline = gst.Pipeline('TranscodingPipeline')
|
||||
|
||||
_log.debug('Pipeline: {0}'.format(self.pipeline))
|
||||
|
||||
self.filesrc = gst.element_factory_make('filesrc', 'filesrc')
|
||||
self.filesrc.set_property('location', src)
|
||||
self.pipeline.add(self.filesrc)
|
||||
|
||||
self.decoder = gst.element_factory_make('decodebin2', 'decoder')
|
||||
|
||||
self.decoder.connect('new-decoded-pad', self._on_dynamic_pad)
|
||||
self.pipeline.add(self.decoder)
|
||||
|
||||
self.ffmpegcolorspace = gst.element_factory_make('ffmpegcolorspace', 'ffmpegcolorspace')
|
||||
self.pipeline.add(self.ffmpegcolorspace)
|
||||
|
||||
self.videoscale = gst.element_factory_make('videoscale', 'videoscale')
|
||||
self.pipeline.add(self.videoscale)
|
||||
|
||||
self.capsfilter = gst.element_factory_make('capsfilter', 'capsfilter')
|
||||
self.capsfilter.set_property('caps', gst.caps_from_string('video/x-raw-rgb, width=180, height=100'))
|
||||
self.pipeline.add(self.capsfilter)
|
||||
|
||||
self.jpegenc = gst.element_factory_make('jpegenc', 'jpegenc')
|
||||
self.pipeline.add(self.jpegenc)
|
||||
|
||||
self.filesink = gst.element_factory_make('filesink', 'filesink')
|
||||
self.filesink.set_property('location', dst)
|
||||
self.pipeline.add(self.filesink)
|
||||
|
||||
# Link all the elements together
|
||||
self.filesrc.link(self.decoder)
|
||||
self.ffmpegcolorspace.link(self.videoscale)
|
||||
self.videoscale.link(self.capsfilter)
|
||||
self.capsfilter.link(self.jpegenc)
|
||||
self.jpegenc.link(self.filesink)
|
||||
|
||||
bus = self.pipeline.get_bus()
|
||||
bus.add_signal_watch()
|
||||
bus.connect('message', self._on_message)
|
||||
|
||||
self.pipeline.set_state(gst.STATE_PLAYING)
|
||||
|
||||
|
||||
def _on_message(self, bus, message):
|
||||
_log.info((bus, message))
|
||||
|
||||
t = message.type
|
||||
|
||||
if t == gst.MESSAGE_EOS:
|
||||
self.__shutdown()
|
||||
|
||||
def _on_dynamic_pad(self, dbin, pad, islast):
|
||||
'''
|
||||
Callback called when ``decodebin2`` has a pad that we can connect to
|
||||
'''
|
||||
pad.link(
|
||||
self.ffmpegcolorspace.get_pad('sink'))
|
||||
|
||||
def __shutdown(self):
|
||||
_log.debug(self.loop)
|
||||
|
||||
self.pipeline.set_state(gst.STATE_NULL)
|
||||
|
||||
gobject.idle_add(self.loop.quit)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
VideoThumbnailer('/home/joar/Dropbox/Public/blender/fluid-box.mp4', '/tmp/dest.jpg')
|
||||
VideoThumbnailer('/home/joar/Dropbox/iPhone/Video 2011-10-05 21 58 03.mov', '/tmp/dest2.jpg')
|
@ -14,10 +14,14 @@
|
||||
# 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 sys
|
||||
|
||||
from mediagoblin import mg_globals
|
||||
from mediagoblin.util import render_to_response, Pagination
|
||||
from mediagoblin.db.util import DESCENDING
|
||||
from mediagoblin.decorators import uses_pagination
|
||||
from mediagoblin import media_types
|
||||
|
||||
|
||||
@uses_pagination
|
||||
def root_view(request, page):
|
||||
@ -26,12 +30,12 @@ def root_view(request, page):
|
||||
|
||||
pagination = Pagination(page, cursor)
|
||||
media_entries = pagination()
|
||||
|
||||
return render_to_response(
|
||||
request, 'mediagoblin/root.html',
|
||||
{'media_entries': media_entries,
|
||||
'allow_registration': mg_globals.app_config["allow_registration"],
|
||||
'pagination': pagination})
|
||||
'pagination': pagination,
|
||||
'sys': sys})
|
||||
|
||||
|
||||
def simple_template_render(request):
|
||||
|
@ -45,7 +45,10 @@ class Workbench(object):
|
||||
def __str__(self):
|
||||
return str(self.dir)
|
||||
def __repr__(self):
|
||||
return repr(self.dir)
|
||||
try:
|
||||
return str(self)
|
||||
except AttributeError:
|
||||
return 'None'
|
||||
|
||||
def joinpath(self, *args):
|
||||
return os.path.join(self.dir, *args)
|
||||
|
Loading…
x
Reference in New Issue
Block a user