Multimedia refractoring, and added video thumbnail support
This commit is contained in:
parent
62be795e91
commit
26729e0277
@ -17,6 +17,9 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from mediagoblin.util import lazy_pass_to_ugettext as _
|
||||||
|
|
||||||
|
|
||||||
class FileTypeNotSupported(Exception):
|
class FileTypeNotSupported(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -17,16 +17,17 @@
|
|||||||
import Image
|
import Image
|
||||||
import tempfile
|
import tempfile
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
import os
|
||||||
|
|
||||||
from celery.task import Task
|
from celery.task import Task
|
||||||
from celery import registry
|
from celery import registry
|
||||||
|
|
||||||
from mediagoblin.db.util import ObjectId
|
from mediagoblin.db.util import ObjectId
|
||||||
from mediagoblin import mg_globals as mgg
|
from mediagoblin import mg_globals as mgg
|
||||||
|
|
||||||
from mediagoblin.util import lazy_pass_to_ugettext as _
|
from mediagoblin.util import lazy_pass_to_ugettext as _
|
||||||
|
from mediagoblin.process_media.errors import BaseProcessingFail, BadMediaFail
|
||||||
import mediagoblin.media_types.video
|
from mediagoblin.process_media import mark_entry_failed
|
||||||
|
from . import transcoders
|
||||||
|
|
||||||
import gobject
|
import gobject
|
||||||
gobject.threads_init()
|
gobject.threads_init()
|
||||||
@ -39,10 +40,12 @@ from arista.transcoder import TranscoderOptions
|
|||||||
|
|
||||||
THUMB_SIZE = 180, 180
|
THUMB_SIZE = 180, 180
|
||||||
MEDIUM_SIZE = 640, 640
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
logging.basicConfig()
|
logging.basicConfig()
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
@ -51,10 +54,20 @@ logger.setLevel(logging.DEBUG)
|
|||||||
def process_video(entry):
|
def process_video(entry):
|
||||||
"""
|
"""
|
||||||
Code to process a video
|
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 = {}
|
info = {}
|
||||||
|
|
||||||
workbench = mgg.workbench_manager.create_workbench()
|
workbench = mgg.workbench_manager.create_workbench()
|
||||||
|
|
||||||
queued_filepath = entry['queued_media_file']
|
queued_filepath = entry['queued_media_file']
|
||||||
@ -62,29 +75,36 @@ def process_video(entry):
|
|||||||
mgg.queue_store, queued_filepath,
|
mgg.queue_store, queued_filepath,
|
||||||
'source')
|
'source')
|
||||||
|
|
||||||
|
''' Initialize arista '''
|
||||||
arista.init()
|
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(
|
# FIXME: Is this needed since we only transcode one video?
|
||||||
__name__,
|
|
||||||
'presets/web-advanced.json')
|
|
||||||
device = arista.presets.load(web_advanced_preset)
|
|
||||||
|
|
||||||
queue = arista.queue.TranscodeQueue()
|
queue = arista.queue.TranscodeQueue()
|
||||||
|
|
||||||
info['tmp_file'] = tmp_file = tempfile.NamedTemporaryFile()
|
|
||||||
|
|
||||||
info['medium_filepath'] = medium_filepath = create_pub_filepath(entry, 'video.webm')
|
info['tmp_file'] = tempfile.NamedTemporaryFile(delete=False)
|
||||||
|
|
||||||
output = tmp_file.name
|
info['medium_filepath'] = create_pub_filepath(
|
||||||
|
entry, 'video.webm')
|
||||||
|
|
||||||
uri = 'file://' + queued_filename
|
info['thumb_filepath'] = create_pub_filepath(
|
||||||
|
entry, 'thumbnail.jpg')
|
||||||
|
|
||||||
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))
|
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)
|
queue.append(opts)
|
||||||
|
|
||||||
@ -95,68 +115,78 @@ def process_video(entry):
|
|||||||
queue.connect("entry-error", _transcoding_error, info)
|
queue.connect("entry-error", _transcoding_error, info)
|
||||||
queue.connect("entry-complete", _transcoding_complete, 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_filename'] = queued_filename
|
||||||
info['queued_filepath'] = queued_filepath
|
info['queued_filepath'] = queued_filepath
|
||||||
info['workbench'] = workbench
|
info['workbench'] = workbench
|
||||||
|
info['preset'] = preset
|
||||||
|
|
||||||
|
info['loop'].run()
|
||||||
|
|
||||||
logger.debug('info: {0}'.format(info))
|
logger.debug('info: {0}'.format(info))
|
||||||
|
|
||||||
loop.run()
|
|
||||||
|
|
||||||
'''
|
|
||||||
try:
|
|
||||||
#thumb = Image.open(mediagoblin.media_types.video.MEDIA_MANAGER['default_thumb'])
|
|
||||||
except IOError:
|
|
||||||
raise BadMediaFail()
|
|
||||||
|
|
||||||
thumb.thumbnail(THUMB_SIZE, Image.ANTIALIAS)
|
def __create_thumbnail(info):
|
||||||
# ensure color mode is compatible with jpg
|
thumbnail = tempfile.NamedTemporaryFile()
|
||||||
if thumb.mode != "RGB":
|
|
||||||
thumb = thumb.convert("RGB")
|
|
||||||
|
|
||||||
thumb_filepath = create_pub_filepath(entry, 'thumbnail.jpg')
|
logger.info('thumbnailing...')
|
||||||
thumb_file = mgg.public_store.get_file(thumb_filepath, 'w')
|
transcoders.VideoThumbnailer(info['tmp_file'].name, thumbnail.name)
|
||||||
|
logger.debug('Done thumbnailing')
|
||||||
|
|
||||||
with thumb_file:
|
os.remove(info['tmp_file'].name)
|
||||||
thumb.save(thumb_file, "JPEG", quality=90)
|
|
||||||
'''
|
|
||||||
|
|
||||||
def __close_processing(queue, qentry, info, error=False):
|
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):
|
||||||
'''
|
'''
|
||||||
Update MediaEntry, move files, handle errors
|
Updates MediaEntry, moves files, handles errors
|
||||||
'''
|
'''
|
||||||
if not error:
|
if not kwargs.get('error'):
|
||||||
|
logger.info('Transcoding successful')
|
||||||
|
|
||||||
qentry.transcoder.stop()
|
qentry.transcoder.stop()
|
||||||
gobject.idle_add(info['loop'].quit)
|
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:
|
with info['tmp_file'] as tmp_file:
|
||||||
mgg.public_store.get_file(info['medium_filepath'], 'wb').write(
|
mgg.public_store.get_file(info['medium_filepath'], 'wb').write(
|
||||||
tmp_file.read())
|
tmp_file.read())
|
||||||
info['entry']['media_files']['medium'] = info['medium_filepath']
|
info['entry']['media_files']['medium'] = info['medium_filepath']
|
||||||
|
|
||||||
print('\n=== DONE! ===\n')
|
|
||||||
|
|
||||||
# we have to re-read because unlike PIL, not everything reads
|
# we have to re-read because unlike PIL, not everything reads
|
||||||
# things in string representation :)
|
# things in string representation :)
|
||||||
queued_file = file(info['queued_filename'], 'rb')
|
queued_file = file(info['queued_filename'], 'rb')
|
||||||
|
|
||||||
with queued_file:
|
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())
|
original_file.write(queued_file.read())
|
||||||
|
|
||||||
mgg.queue_store.delete_file(info['queued_filepath'])
|
mgg.queue_store.delete_file(info['queued_filepath'])
|
||||||
|
|
||||||
|
|
||||||
|
logger.debug('...Done')
|
||||||
|
|
||||||
info['entry']['queued_media_file'] = []
|
info['entry']['queued_media_file'] = []
|
||||||
media_files_dict = info['entry'].setdefault('media_files', {})
|
media_files_dict = info['entry'].setdefault('media_files', {})
|
||||||
media_files_dict['original'] = original_filepath
|
media_files_dict['original'] = original_filepath
|
||||||
# media_files_dict['thumb'] = thumb_filepath
|
|
||||||
|
|
||||||
info['entry']['state'] = u'processed'
|
info['entry']['state'] = u'processed'
|
||||||
|
info['entry']['media_data'][u'preset'] = info['preset'].name
|
||||||
|
__create_thumbnail(info)
|
||||||
info['entry'].save()
|
info['entry'].save()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -174,17 +204,20 @@ def _transcoding_start(queue, qentry, info):
|
|||||||
logger.info('-> Starting transcoding')
|
logger.info('-> Starting transcoding')
|
||||||
logger.debug(queue, qentry, info)
|
logger.debug(queue, qentry, info)
|
||||||
|
|
||||||
|
|
||||||
def _transcoding_complete(*args):
|
def _transcoding_complete(*args):
|
||||||
__close_processing(*args)
|
__close_processing(*args)
|
||||||
print(args)
|
print(args)
|
||||||
|
|
||||||
def _transcoding_error(*args):
|
|
||||||
logger.info('-> Error')
|
def _transcoding_error(queue, qentry, info):
|
||||||
__close_processing(*args, error=True)
|
logger.info('Error')
|
||||||
logger.debug(*args)
|
__close_processing(queue, qentry, info, error=True)
|
||||||
|
logger.debug(queue, quentry, info)
|
||||||
|
|
||||||
|
|
||||||
def _transcoding_pass_setup(queue, qentry, options):
|
def _transcoding_pass_setup(queue, qentry, options):
|
||||||
logger.info('-> Pass setup')
|
logger.info('Pass setup')
|
||||||
logger.debug(queue, qentry, options)
|
logger.debug(queue, qentry, options)
|
||||||
|
|
||||||
|
|
||||||
@ -200,10 +233,10 @@ def check_interrupted():
|
|||||||
except:
|
except:
|
||||||
# Something pretty bad happened... just exit!
|
# Something pretty bad happened... just exit!
|
||||||
gobject.idle_add(loop.quit)
|
gobject.idle_add(loop.quit)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def create_pub_filepath(entry, filename):
|
def create_pub_filepath(entry, filename):
|
||||||
return mgg.public_store.get_unique_filepath(
|
return mgg.public_store.get_unique_filepath(
|
||||||
@ -212,34 +245,6 @@ def create_pub_filepath(entry, filename):
|
|||||||
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
|
# Media processing initial steps
|
||||||
################################
|
################################
|
||||||
@ -311,4 +316,3 @@ def mark_entry_failed(entry_id, exc):
|
|||||||
{'$set': {u'state': u'failed',
|
{'$set': {u'state': u'failed',
|
||||||
u'fail_error': None,
|
u'fail_error': None,
|
||||||
u'fail_metadata': {}}})
|
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
|
# 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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
from mediagoblin import mg_globals
|
from mediagoblin import mg_globals
|
||||||
from mediagoblin.util import render_to_response, Pagination
|
from mediagoblin.util import render_to_response, Pagination
|
||||||
from mediagoblin.db.util import DESCENDING
|
from mediagoblin.db.util import DESCENDING
|
||||||
from mediagoblin.decorators import uses_pagination
|
from mediagoblin.decorators import uses_pagination
|
||||||
|
from mediagoblin import media_types
|
||||||
|
|
||||||
|
|
||||||
@uses_pagination
|
@uses_pagination
|
||||||
def root_view(request, page):
|
def root_view(request, page):
|
||||||
@ -26,12 +30,12 @@ def root_view(request, page):
|
|||||||
|
|
||||||
pagination = Pagination(page, cursor)
|
pagination = Pagination(page, cursor)
|
||||||
media_entries = pagination()
|
media_entries = pagination()
|
||||||
|
|
||||||
return render_to_response(
|
return render_to_response(
|
||||||
request, 'mediagoblin/root.html',
|
request, 'mediagoblin/root.html',
|
||||||
{'media_entries': media_entries,
|
{'media_entries': media_entries,
|
||||||
'allow_registration': mg_globals.app_config["allow_registration"],
|
'allow_registration': mg_globals.app_config["allow_registration"],
|
||||||
'pagination': pagination})
|
'pagination': pagination,
|
||||||
|
'sys': sys})
|
||||||
|
|
||||||
|
|
||||||
def simple_template_render(request):
|
def simple_template_render(request):
|
||||||
|
@ -45,7 +45,10 @@ class Workbench(object):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.dir)
|
return str(self.dir)
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return repr(self.dir)
|
try:
|
||||||
|
return str(self)
|
||||||
|
except AttributeError:
|
||||||
|
return 'None'
|
||||||
|
|
||||||
def joinpath(self, *args):
|
def joinpath(self, *args):
|
||||||
return os.path.join(self.dir, *args)
|
return os.path.join(self.dir, *args)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user