Video transcoding is now gstreamer directly instead of through arista
This commit is contained in:
parent
a7ca2a7211
commit
e9c1b9381d
@ -14,10 +14,10 @@
|
|||||||
# 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 Image
|
|
||||||
import tempfile
|
import tempfile
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
from celery.task import Task
|
from celery.task import Task
|
||||||
from celery import registry
|
from celery import registry
|
||||||
@ -29,21 +29,9 @@ from mediagoblin.process_media.errors import BaseProcessingFail, BadMediaFail
|
|||||||
from mediagoblin.process_media import mark_entry_failed
|
from mediagoblin.process_media import mark_entry_failed
|
||||||
from . import transcoders
|
from . import transcoders
|
||||||
|
|
||||||
import gobject
|
|
||||||
gobject.threads_init()
|
|
||||||
|
|
||||||
import gst
|
|
||||||
import arista
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from arista.transcoder import TranscoderOptions
|
|
||||||
|
|
||||||
THUMB_SIZE = 180, 180
|
THUMB_SIZE = 180, 180
|
||||||
MEDIUM_SIZE = 640, 640
|
MEDIUM_SIZE = 640, 640
|
||||||
|
|
||||||
ARISTA_DEVICE = 'devices/web-advanced.json'
|
|
||||||
ARISTA_PRESET = None
|
|
||||||
|
|
||||||
loop = None # Is this even used?
|
loop = None # Is this even used?
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -63,11 +51,6 @@ def process_video(entry):
|
|||||||
and attaches callbacks to that child process, hopefully, the
|
and attaches callbacks to that child process, hopefully, the
|
||||||
entry-complete callback will be called when the video is done.
|
entry-complete callback will be called when the video is done.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
''' Empty dict, will store data which will be passed to the callback
|
|
||||||
functions '''
|
|
||||||
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']
|
||||||
@ -75,56 +58,64 @@ def process_video(entry):
|
|||||||
mgg.queue_store, queued_filepath,
|
mgg.queue_store, queued_filepath,
|
||||||
'source')
|
'source')
|
||||||
|
|
||||||
''' Initialize arista '''
|
medium_filepath = create_pub_filepath(
|
||||||
arista.init()
|
entry, '640p.webm')
|
||||||
|
|
||||||
''' Loads a preset file which specifies the format of the output video'''
|
thumbnail_filepath = create_pub_filepath(
|
||||||
device = arista.presets.load(
|
|
||||||
pkg_resources.resource_filename(
|
|
||||||
__name__,
|
|
||||||
ARISTA_DEVICE))
|
|
||||||
|
|
||||||
# FIXME: Is this needed since we only transcode one video?
|
|
||||||
queue = arista.queue.TranscodeQueue()
|
|
||||||
|
|
||||||
info['tmp_file'] = tempfile.NamedTemporaryFile(delete=False)
|
|
||||||
|
|
||||||
info['medium_filepath'] = create_pub_filepath(
|
|
||||||
entry, 'video.webm')
|
|
||||||
|
|
||||||
info['thumb_filepath'] = create_pub_filepath(
|
|
||||||
entry, 'thumbnail.jpg')
|
entry, 'thumbnail.jpg')
|
||||||
|
|
||||||
# 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))
|
# Create a temporary file for the video destination
|
||||||
|
tmp_dst = tempfile.NamedTemporaryFile()
|
||||||
|
|
||||||
opts = TranscoderOptions(
|
with tmp_dst:
|
||||||
'file://' + queued_filename, # Arista did it this way, IIRC
|
# Transcode queued file to a VP8/vorbis file that fits in a 640x640 square
|
||||||
preset,
|
transcoder = transcoders.VideoTranscoder(queued_filename, tmp_dst.name)
|
||||||
info['tmp_file'].name)
|
|
||||||
|
|
||||||
queue.append(opts)
|
# Push transcoded video to public storage
|
||||||
|
mgg.public_store.get_file(medium_filepath, 'wb').write(
|
||||||
|
tmp_dst.read())
|
||||||
|
|
||||||
info['entry'] = entry
|
entry['media_files']['webm_640'] = medium_filepath
|
||||||
|
|
||||||
queue.connect("entry-start", _transcoding_start, info)
|
# Save the width and height of the transcoded video
|
||||||
queue.connect("entry-pass-setup", _transcoding_pass_setup, info)
|
entry['media_data']['video'] = {
|
||||||
queue.connect("entry-error", _transcoding_error, info)
|
u'width': transcoder.dst_data.videowidth,
|
||||||
queue.connect("entry-complete", _transcoding_complete, info)
|
u'height': transcoder.dst_data.videoheight}
|
||||||
|
|
||||||
# Add data to the info dict, making it available to the callbacks
|
# Create a temporary file for the video thumbnail
|
||||||
info['loop'] = gobject.MainLoop()
|
tmp_thumb = tempfile.NamedTemporaryFile()
|
||||||
info['queued_filename'] = queued_filename
|
|
||||||
info['queued_filepath'] = queued_filepath
|
|
||||||
info['workbench'] = workbench
|
|
||||||
info['preset'] = preset
|
|
||||||
|
|
||||||
info['loop'].run()
|
with tmp_thumb:
|
||||||
|
# Create a thumbnail.jpg that fits in a 180x180 square
|
||||||
|
transcoders.VideoThumbnailer(queued_filename, tmp_thumb.name)
|
||||||
|
|
||||||
logger.debug('info: {0}'.format(info))
|
# Push the thumbnail to public storage
|
||||||
|
mgg.public_store.get_file(thumbnail_filepath, 'wb').write(
|
||||||
|
tmp_thumb.read())
|
||||||
|
|
||||||
|
entry['media_files']['thumb'] = thumbnail_filepath
|
||||||
|
|
||||||
|
|
||||||
|
# Push original file to public storage
|
||||||
|
queued_file = file(queued_filename, 'rb')
|
||||||
|
|
||||||
|
with queued_file:
|
||||||
|
original_filepath = create_pub_filepath(
|
||||||
|
entry,
|
||||||
|
queued_filepath[-1])
|
||||||
|
|
||||||
|
with mgg.public_store.get_file(original_filepath, 'wb') as \
|
||||||
|
original_file:
|
||||||
|
original_file.write(queued_file.read())
|
||||||
|
|
||||||
|
entry['media_files']['original'] = original_filepath
|
||||||
|
|
||||||
|
mgg.queue_store.delete_file(queued_filepath)
|
||||||
|
|
||||||
|
|
||||||
|
# Save the MediaEntry
|
||||||
|
entry.save()
|
||||||
|
|
||||||
|
|
||||||
def __create_thumbnail(info):
|
def __create_thumbnail(info):
|
||||||
@ -139,6 +130,7 @@ def __create_thumbnail(info):
|
|||||||
mgg.public_store.get_file(info['thumb_filepath'], 'wb').write(
|
mgg.public_store.get_file(info['thumb_filepath'], 'wb').write(
|
||||||
thumbnail.read())
|
thumbnail.read())
|
||||||
|
|
||||||
|
|
||||||
info['entry']['media_files']['thumb'] = info['thumb_filepath']
|
info['entry']['media_files']['thumb'] = info['thumb_filepath']
|
||||||
info['entry'].save()
|
info['entry'].save()
|
||||||
|
|
||||||
@ -267,6 +259,9 @@ class ProcessMedia(Task):
|
|||||||
mark_entry_failed(entry[u'_id'], exc)
|
mark_entry_failed(entry[u'_id'], exc)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
entry['state'] = u'processed'
|
||||||
|
entry.save()
|
||||||
|
|
||||||
def on_failure(self, exc, task_id, args, kwargs, einfo):
|
def on_failure(self, exc, task_id, args, kwargs, einfo):
|
||||||
"""
|
"""
|
||||||
If the processing failed we should mark that in the database.
|
If the processing failed we should mark that in the database.
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
from __future__ import division
|
from __future__ import division
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
|
import pdb
|
||||||
|
|
||||||
_log = logging.getLogger(__name__)
|
_log = logging.getLogger(__name__)
|
||||||
logging.basicConfig()
|
logging.basicConfig()
|
||||||
@ -28,14 +28,17 @@ try:
|
|||||||
gobject.threads_init()
|
gobject.threads_init()
|
||||||
except:
|
except:
|
||||||
_log.error('Could not import gobject')
|
_log.error('Could not import gobject')
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pygst
|
import pygst
|
||||||
pygst.require('0.10')
|
pygst.require('0.10')
|
||||||
import gst
|
import gst
|
||||||
|
from gst import pbutils
|
||||||
from gst.extend import discoverer
|
from gst.extend import discoverer
|
||||||
except:
|
except:
|
||||||
_log.error('pygst could not be imported')
|
_log.error('pygst could not be imported')
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
|
||||||
class VideoThumbnailer:
|
class VideoThumbnailer:
|
||||||
@ -201,12 +204,14 @@ class VideoThumbnailer:
|
|||||||
gobject.idle_add(self.loop.quit)
|
gobject.idle_add(self.loop.quit)
|
||||||
|
|
||||||
|
|
||||||
class VideoTranscoder():
|
class VideoTranscoder:
|
||||||
'''
|
'''
|
||||||
Video transcoder
|
Video transcoder
|
||||||
|
|
||||||
|
Transcodes the SRC video file to a VP8 WebM video file at DST
|
||||||
|
|
||||||
TODO:
|
TODO:
|
||||||
- Currently not working
|
- Audio pipeline
|
||||||
'''
|
'''
|
||||||
def __init__(self, src, dst, **kwargs):
|
def __init__(self, src, dst, **kwargs):
|
||||||
_log.info('Initializing VideoTranscoder...')
|
_log.info('Initializing VideoTranscoder...')
|
||||||
@ -215,7 +220,7 @@ class VideoTranscoder():
|
|||||||
self.source_path = src
|
self.source_path = src
|
||||||
self.destination_path = dst
|
self.destination_path = dst
|
||||||
|
|
||||||
self.destination_dimensions = kwargs.get('dimensions') or (180, 180)
|
self.destination_dimensions = kwargs.get('dimensions') or (640, 640)
|
||||||
|
|
||||||
if not type(self.destination_dimensions) == tuple:
|
if not type(self.destination_dimensions) == tuple:
|
||||||
raise Exception('dimensions must be tuple: (width, height)')
|
raise Exception('dimensions must be tuple: (width, height)')
|
||||||
@ -253,12 +258,14 @@ class VideoTranscoder():
|
|||||||
|
|
||||||
self.data = data
|
self.data = data
|
||||||
|
|
||||||
|
self._on_discovered()
|
||||||
|
|
||||||
# Tell the transcoding pipeline to start running
|
# Tell the transcoding pipeline to start running
|
||||||
self.pipeline.set_state(gst.STATE_PLAYING)
|
self.pipeline.set_state(gst.STATE_PLAYING)
|
||||||
_log.info('Transcoding...')
|
_log.info('Transcoding...')
|
||||||
|
|
||||||
def _on_discovered(self):
|
def _on_discovered(self):
|
||||||
self.__setup_capsfilter()
|
self.__setup_videoscale_capsfilter()
|
||||||
|
|
||||||
def _setup_pass(self):
|
def _setup_pass(self):
|
||||||
self.pipeline = gst.Pipeline('VideoTranscoderPipeline')
|
self.pipeline = gst.Pipeline('VideoTranscoderPipeline')
|
||||||
@ -276,7 +283,8 @@ class VideoTranscoder():
|
|||||||
self.pipeline.add(self.ffmpegcolorspace)
|
self.pipeline.add(self.ffmpegcolorspace)
|
||||||
|
|
||||||
self.videoscale = gst.element_factory_make('videoscale', 'videoscale')
|
self.videoscale = gst.element_factory_make('videoscale', 'videoscale')
|
||||||
self.videoscale.set_property('method', 'bilinear')
|
self.videoscale.set_property('method', 2) # I'm not sure this works
|
||||||
|
self.videoscale.set_property('add-borders', 0)
|
||||||
self.pipeline.add(self.videoscale)
|
self.pipeline.add(self.videoscale)
|
||||||
|
|
||||||
self.capsfilter = gst.element_factory_make('capsfilter', 'capsfilter')
|
self.capsfilter = gst.element_factory_make('capsfilter', 'capsfilter')
|
||||||
@ -286,16 +294,36 @@ class VideoTranscoder():
|
|||||||
self.vp8enc.set_property('quality', 6)
|
self.vp8enc.set_property('quality', 6)
|
||||||
self.vp8enc.set_property('threads', 2)
|
self.vp8enc.set_property('threads', 2)
|
||||||
self.vp8enc.set_property('speed', 2)
|
self.vp8enc.set_property('speed', 2)
|
||||||
|
self.pipeline.add(self.vp8enc)
|
||||||
|
|
||||||
|
|
||||||
|
# Audio
|
||||||
|
self.audioconvert = gst.element_factory_make('audioconvert', 'audioconvert')
|
||||||
|
self.pipeline.add(self.audioconvert)
|
||||||
|
|
||||||
|
self.vorbisenc = gst.element_factory_make('vorbisenc', 'vorbisenc')
|
||||||
|
self.vorbisenc.set_property('quality', 0.7)
|
||||||
|
self.pipeline.add(self.vorbisenc)
|
||||||
|
|
||||||
|
|
||||||
self.webmmux = gst.element_factory_make('webmmux', 'webmmux')
|
self.webmmux = gst.element_factory_make('webmmux', 'webmmux')
|
||||||
self.pipeline.add(self.webmmux)
|
self.pipeline.add(self.webmmux)
|
||||||
|
|
||||||
self.filesink = gst.element_factory_make('filesink', 'filesink')
|
self.filesink = gst.element_factory_make('filesink', 'filesink')
|
||||||
|
self.filesink.set_property('location', self.destination_path)
|
||||||
|
self.pipeline.add(self.filesink)
|
||||||
|
|
||||||
self.filesrc.link(self.decoder)
|
self.filesrc.link(self.decoder)
|
||||||
self.ffmpegcolorspace.link(self.videoscale)
|
self.ffmpegcolorspace.link(self.videoscale)
|
||||||
self.videoscale.link(self.capsfilter)
|
self.videoscale.link(self.capsfilter)
|
||||||
self.vp8enc.link(self.filesink)
|
self.capsfilter.link(self.vp8enc)
|
||||||
|
self.vp8enc.link(self.webmmux)
|
||||||
|
|
||||||
|
# Audio
|
||||||
|
self.audioconvert.link(self.vorbisenc)
|
||||||
|
self.vorbisenc.link(self.webmmux)
|
||||||
|
|
||||||
|
self.webmmux.link(self.filesink)
|
||||||
|
|
||||||
self._setup_bus()
|
self._setup_bus()
|
||||||
|
|
||||||
@ -303,39 +331,43 @@ class VideoTranscoder():
|
|||||||
'''
|
'''
|
||||||
Callback called when ``decodebin2`` has a pad that we can connect to
|
Callback called when ``decodebin2`` has a pad that we can connect to
|
||||||
'''
|
'''
|
||||||
pad.link(
|
_log.debug('Linked {0}'.format(pad))
|
||||||
self.ffmpegcolorspace.get_pad('sink'))
|
|
||||||
|
#pdb.set_trace()
|
||||||
|
|
||||||
|
if self.ffmpegcolorspace.get_pad_template('sink')\
|
||||||
|
.get_caps().intersect(pad.get_caps()).is_empty():
|
||||||
|
pad.link(
|
||||||
|
self.audioconvert.get_pad('sink'))
|
||||||
|
else:
|
||||||
|
pad.link(
|
||||||
|
self.ffmpegcolorspace.get_pad('sink'))
|
||||||
|
|
||||||
def _setup_bus(self):
|
def _setup_bus(self):
|
||||||
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_message)
|
self.bus.connect('message', self._on_message)
|
||||||
|
|
||||||
def __setup_capsfilter(self):
|
def __setup_videoscale_capsfilter(self):
|
||||||
thumbsizes = self.calculate_resize() # Returns tuple with (width, height)
|
caps = ['video/x-raw-yuv', 'pixel-aspect-ratio=1/1']
|
||||||
|
|
||||||
|
if self.data.videoheight > self.data.videowidth:
|
||||||
|
# Whoa! We have ourselves a portrait video!
|
||||||
|
caps.append('height={0}'.format(
|
||||||
|
self.destination_dimensions[1]))
|
||||||
|
else:
|
||||||
|
# It's a landscape, phew, how normal.
|
||||||
|
caps.append('width={0}'.format(
|
||||||
|
self.destination_dimensions[0]))
|
||||||
|
|
||||||
self.capsfilter.set_property(
|
self.capsfilter.set_property(
|
||||||
'caps',
|
'caps',
|
||||||
gst.caps_from_string('video/x-raw-rgb, width={width}, height={height}'.format(
|
gst.caps_from_string(
|
||||||
width=thumbsizes[0],
|
', '.join(caps)))
|
||||||
height=thumbsizes[1]
|
gst.DEBUG_BIN_TO_DOT_FILE (
|
||||||
)))
|
self.pipeline,
|
||||||
|
gst.DEBUG_GRAPH_SHOW_ALL,
|
||||||
def calculate_resize(self):
|
'supersimple-debug-graph')
|
||||||
x_ratio = self.destination_dimensions[0] / self.data.videowidth
|
|
||||||
y_ratio = self.destination_dimensions[1] / self.data.videoheight
|
|
||||||
|
|
||||||
if self.data.videoheight > self.data.videowidth:
|
|
||||||
# We're dealing with a portrait!
|
|
||||||
dimensions = (
|
|
||||||
int(self.data.videowidth * y_ratio),
|
|
||||||
180)
|
|
||||||
else:
|
|
||||||
dimensions = (
|
|
||||||
180,
|
|
||||||
int(self.data.videoheight * x_ratio))
|
|
||||||
|
|
||||||
return dimensions
|
|
||||||
|
|
||||||
def _on_message(self, bus, message):
|
def _on_message(self, bus, message):
|
||||||
_log.debug((bus, message))
|
_log.debug((bus, message))
|
||||||
@ -343,12 +375,25 @@ class VideoTranscoder():
|
|||||||
t = message.type
|
t = message.type
|
||||||
|
|
||||||
if t == gst.MESSAGE_EOS:
|
if t == gst.MESSAGE_EOS:
|
||||||
self.__stop()
|
self._discover_dst_and_stop()
|
||||||
_log.info('Done')
|
_log.info('Done')
|
||||||
elif t == gst.MESSAGE_ERROR:
|
elif t == gst.MESSAGE_ERROR:
|
||||||
_log.error((bus, message))
|
_log.error((bus, message))
|
||||||
self.__stop()
|
self.__stop()
|
||||||
|
|
||||||
|
def _discover_dst_and_stop(self):
|
||||||
|
self.dst_discoverer = discoverer.Discoverer(self.destination_path)
|
||||||
|
|
||||||
|
self.dst_discoverer.connect('discovered', self.__dst_discovered)
|
||||||
|
|
||||||
|
self.dst_discoverer.discover()
|
||||||
|
|
||||||
|
|
||||||
|
def __dst_discovered(self, data, is_media):
|
||||||
|
self.dst_data = data
|
||||||
|
|
||||||
|
self.__stop()
|
||||||
|
|
||||||
def __stop(self):
|
def __stop(self):
|
||||||
_log.debug(self.loop)
|
_log.debug(self.loop)
|
||||||
|
|
||||||
@ -358,6 +403,9 @@ class VideoTranscoder():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
import os
|
||||||
|
os.environ["GST_DEBUG_DUMP_DOT_DIR"] = "/tmp"
|
||||||
|
os.putenv('GST_DEBUG_DUMP_DOT_DIR', '/tmp')
|
||||||
from optparse import OptionParser
|
from optparse import OptionParser
|
||||||
|
|
||||||
parser = OptionParser(
|
parser = OptionParser(
|
||||||
@ -396,4 +444,5 @@ if __name__ == '__main__':
|
|||||||
if options.action == 'thumbnail':
|
if options.action == 'thumbnail':
|
||||||
VideoThumbnailer(*args)
|
VideoThumbnailer(*args)
|
||||||
elif options.action == 'video':
|
elif options.action == 'video':
|
||||||
VideoTranscoder(*args)
|
transcoder = VideoTranscoder(*args)
|
||||||
|
pdb.set_trace()
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
{% extends 'mediagoblin/user_pages/media.html' %}
|
{% extends 'mediagoblin/user_pages/media.html' %}
|
||||||
{% block mediagoblin_media %}
|
{% block mediagoblin_media %}
|
||||||
<video width="640" height="" controls>
|
<video width="{{ media.media_data.video.width }}"
|
||||||
|
height="{{ media.media_data.video.height }}" controls="controls">
|
||||||
<source src="{{ request.app.public_store.file_url(
|
<source src="{{ request.app.public_store.file_url(
|
||||||
media['media_files']['medium']) }}"
|
media['media_files']['webm_640']) }}"
|
||||||
type='video/webm; codecs="vp8, vorbis"' />
|
type='video/webm; codecs="vp8, vorbis"' />
|
||||||
</video>
|
</video>
|
||||||
{% if 'original' in media.media_files %}
|
{% if 'original' in media.media_files %}
|
||||||
|
<p>
|
||||||
<a href="{{ request.app.public_store.file_url(
|
<a href="{{ request.app.public_store.file_url(
|
||||||
media['media_files']['original']) }}">
|
media['media_files']['original']) }}">
|
||||||
{%- trans -%}
|
{%- trans -%}
|
||||||
Original
|
Original
|
||||||
{%- endtrans -%}
|
{%- endtrans -%}
|
||||||
</a>
|
</a>
|
||||||
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user