Merge branch 'master' into 419_cherrypick_large_uploads

This commit is contained in:
Christopher Allan Webber 2013-03-04 15:47:05 -06:00
commit f415c35b4e
16 changed files with 276 additions and 57 deletions

View File

@ -103,6 +103,13 @@ vorbis_quality = float(default=0.3)
# Autoplay the video when page is loaded? # Autoplay the video when page is loaded?
auto_play = boolean(default=True) auto_play = boolean(default=True)
[[skip_transcode]]
mime_types = string_list(default=list("video/webm"))
container_formats = string_list(default=list("Matroska"))
video_codecs = string_list(default=list("VP8 video"))
audio_codecs = string_list(default=list("Vorbis"))
dimensions_match = boolean(default=True)
[media_type:mediagoblin.media_types.audio] [media_type:mediagoblin.media_types.audio]
keep_original = boolean(default=True) keep_original = boolean(default=True)

View File

@ -17,6 +17,9 @@
from mediagoblin.tools.common import simple_printer from mediagoblin.tools.common import simple_printer
from sqlalchemy import Table from sqlalchemy import Table
class TableAlreadyExists(Exception):
pass
class MigrationManager(object): class MigrationManager(object):
""" """
@ -128,7 +131,10 @@ class MigrationManager(object):
# sanity check before we proceed, none of these should be created # sanity check before we proceed, none of these should be created
for model in self.models: for model in self.models:
# Maybe in the future just print out a "Yikes!" or something? # Maybe in the future just print out a "Yikes!" or something?
assert not model.__table__.exists(self.session.bind) if model.__table__.exists(self.session.bind):
raise TableAlreadyExists(
u"Intended to create table '%s' but it already exists" %
model.__table__.name)
self.migration_model.metadata.create_all( self.migration_model.metadata.create_all(
self.session.bind, self.session.bind,

View File

@ -126,24 +126,28 @@ class MediaEntryMixin(object):
""" """
return cleaned_markdown_conversion(self.description) return cleaned_markdown_conversion(self.description)
def get_display_media(self, media_map, def get_display_media(self):
fetch_order=common.DISPLAY_IMAGE_FETCHING_ORDER): """Find the best media for display.
"""
Find the best media for display.
Args: We try checking self.media_manager.fetching_order if it exists to
- media_map: a dict like pull down the order.
{u'image_size': [u'dir1', u'dir2', u'image.jpg']}
- fetch_order: the order we should try fetching images in
Returns: Returns:
(media_size, media_path) (media_size, media_path)
""" or, if not found, None.
media_sizes = media_map.keys()
for media_size in common.DISPLAY_IMAGE_FETCHING_ORDER: """
fetch_order = self.media_manager.get("media_fetch_order")
# No fetching order found? well, give up!
if not fetch_order:
return None
media_sizes = self.media_files.keys()
for media_size in fetch_order:
if media_size in media_sizes: if media_size in media_sizes:
return media_map[media_size] return media_size, self.media_files[media_size]
def main_mediafile(self): def main_mediafile(self):
pass pass

View File

@ -14,7 +14,6 @@
# 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 pdb
import logging import logging
import Image import Image
@ -233,5 +232,3 @@ if __name__ == '__main__':
thumbnailer = AudioThumbnailer() thumbnailer = AudioThumbnailer()
thumbnailer.spectrogram(*sys.argv[1:], width=640) thumbnailer.spectrogram(*sys.argv[1:], width=640)
pdb.set_trace()

View File

@ -25,4 +25,8 @@ MEDIA_MANAGER = {
"sniff_handler": sniff_handler, "sniff_handler": sniff_handler,
"display_template": "mediagoblin/media_displays/image.html", "display_template": "mediagoblin/media_displays/image.html",
"default_thumb": "images/media_thumbs/image.png", "default_thumb": "images/media_thumbs/image.png",
"accepted_extensions": ["jpg", "jpeg", "png", "gif", "tiff"]} "accepted_extensions": ["jpg", "jpeg", "png", "gif", "tiff"],
# Used by the media_entry.get_display_media method
"media_fetch_order": [u'medium', u'original', u'thumb'],
}

View File

@ -26,4 +26,9 @@ MEDIA_MANAGER = {
"display_template": "mediagoblin/media_displays/video.html", "display_template": "mediagoblin/media_displays/video.html",
"default_thumb": "images/media_thumbs/video.jpg", "default_thumb": "images/media_thumbs/video.jpg",
"accepted_extensions": [ "accepted_extensions": [
"mp4", "mov", "webm", "avi", "3gp", "3gpp", "mkv", "ogv", "m4v"]} "mp4", "mov", "webm", "avi", "3gp", "3gpp", "mkv", "ogv", "m4v"],
# Used by the media_entry.get_display_media method
"media_fetch_order": [u'webm_640', u'original'],
"default_webm_type": 'video/webm; codecs="vp8, vorbis"',
}

View File

@ -14,4 +14,19 @@
# 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/>.
from mediagoblin.db.migration_tools import RegisterMigration, inspect_table
from sqlalchemy import MetaData, Column, Unicode
MIGRATIONS = {} MIGRATIONS = {}
@RegisterMigration(1, MIGRATIONS)
def add_orig_metadata_column(db_conn):
metadata = MetaData(bind=db_conn.bind)
vid_data = inspect_table(metadata, "video__mediadata")
col = Column('orig_metadata', Unicode,
default=None, nullable=True)
col.create(vid_data)
db_conn.commit()

View File

@ -20,12 +20,30 @@ from mediagoblin.db.base import Base
from sqlalchemy import ( from sqlalchemy import (
Column, Integer, SmallInteger, ForeignKey) Column, Integer, SmallInteger, ForeignKey)
from sqlalchemy.orm import relationship, backref from sqlalchemy.orm import relationship, backref
from mediagoblin.db.extratypes import JSONEncoded
from mediagoblin.media_types import video
BACKREF_NAME = "video__media_data" BACKREF_NAME = "video__media_data"
class VideoData(Base): class VideoData(Base):
"""
Attributes:
- media_data: the originating media entry (of course)
- width: width of the transcoded video
- height: height of the transcoded video
- orig_metadata: A loose json structure containing metadata gstreamer
pulled from the original video.
This field is NOT GUARANTEED to exist!
Likely metadata extracted:
"videoheight", "videolength", "videowidth",
"audiorate", "audiolength", "audiochannels", "audiowidth",
"mimetype", "tags"
TODO: document the above better.
"""
__tablename__ = "video__mediadata" __tablename__ = "video__mediadata"
# The primary key *and* reference to the main media_entry # The primary key *and* reference to the main media_entry
@ -38,6 +56,35 @@ class VideoData(Base):
width = Column(SmallInteger) width = Column(SmallInteger)
height = Column(SmallInteger) height = Column(SmallInteger)
orig_metadata = Column(JSONEncoded)
def source_type(self):
"""
Construct a useful type=... that is to say, used like:
<video><source type="{{ entry.media_data.source_type() }}" /></video>
Try to construct it out of self.orig_metadata... if we fail we
just dope'ily fall back on DEFAULT_WEBM_TYPE
"""
orig_metadata = self.orig_metadata or {}
if "webm_640" not in self.get_media_entry.media_files \
and "mimetype" in orig_metadata \
and "tags" in orig_metadata \
and "audio-codec" in orig_metadata["tags"] \
and "video-codec" in orig_metadata["tags"]:
if orig_metadata['mimetype'] == 'application/ogg':
# stupid ambiguous .ogg extension
mimetype = "video/ogg"
else:
mimetype = orig_metadata['mimetype']
return '%s; codecs="%s, %s"' % (
mimetype,
orig_metadata["tags"]["video-codec"].lower(),
orig_metadata["tags"]["audio-codec"].lower())
else:
return video.MEDIA_MANAGER["default_webm_type"]
DATA_MODEL = VideoData DATA_MODEL = VideoData
MODELS = [VideoData] MODELS = [VideoData]

View File

@ -23,6 +23,7 @@ from mediagoblin.processing import \
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
from . import transcoders from . import transcoders
from .util import skip_transcode
_log = logging.getLogger(__name__) _log = logging.getLogger(__name__)
_log.setLevel(logging.DEBUG) _log.setLevel(logging.DEBUG)
@ -79,24 +80,53 @@ def process_video(proc_state):
with tmp_dst: with tmp_dst:
# Transcode queued file to a VP8/vorbis file that fits in a 640x640 square # Transcode queued file to a VP8/vorbis file that fits in a 640x640 square
progress_callback = ProgressCallback(entry) progress_callback = ProgressCallback(entry)
transcoder = transcoders.VideoTranscoder()
transcoder.transcode(queued_filename, tmp_dst.name,
vp8_quality=video_config['vp8_quality'],
vp8_threads=video_config['vp8_threads'],
vorbis_quality=video_config['vorbis_quality'],
progress_callback=progress_callback)
# Push transcoded video to public storage dimensions = (
_log.debug('Saving medium...') mgg.global_config['media:medium']['max_width'],
mgg.public_store.copy_local_to_storage(tmp_dst.name, medium_filepath) mgg.global_config['media:medium']['max_height'])
_log.debug('Saved medium')
entry.media_files['webm_640'] = medium_filepath # Extract metadata and keep a record of it
metadata = transcoders.VideoTranscoder().discover(queued_filename)
store_metadata(entry, metadata)
# Save the width and height of the transcoded video # Figure out whether or not we need to transcode this video or
entry.media_data_init( # if we can skip it
width=transcoder.dst_data.videowidth, if skip_transcode(metadata):
height=transcoder.dst_data.videoheight) _log.debug('Skipping transcoding')
dst_dimensions = metadata['videowidth'], metadata['videoheight']
# Push original file to public storage
_log.debug('Saving original...')
proc_state.copy_original(queued_filepath[-1])
did_transcode = False
else:
transcoder = transcoders.VideoTranscoder()
transcoder.transcode(queued_filename, tmp_dst.name,
vp8_quality=video_config['vp8_quality'],
vp8_threads=video_config['vp8_threads'],
vorbis_quality=video_config['vorbis_quality'],
progress_callback=progress_callback,
dimensions=dimensions)
dst_dimensions = transcoder.dst_data.videowidth,\
transcoder.dst_data.videoheight
# Push transcoded video to public storage
_log.debug('Saving medium...')
mgg.public_store.copy_local_to_storage(tmp_dst.name, medium_filepath)
_log.debug('Saved medium')
entry.media_files['webm_640'] = medium_filepath
did_transcode = True
# Save the width and height of the transcoded video
entry.media_data_init(
width=dst_dimensions[0],
height=dst_dimensions[1])
# Temporary file for the video thumbnail (cleaned up with workbench) # Temporary file for the video thumbnail (cleaned up with workbench)
tmp_thumb = NamedTemporaryFile(dir=workbench.dir, suffix='.jpg', delete=False) tmp_thumb = NamedTemporaryFile(dir=workbench.dir, suffix='.jpg', delete=False)
@ -108,15 +138,44 @@ def process_video(proc_state):
tmp_thumb.name, tmp_thumb.name,
180) 180)
# Push the thumbnail to public storage # Push the thumbnail to public storage
_log.debug('Saving thumbnail...') _log.debug('Saving thumbnail...')
mgg.public_store.copy_local_to_storage(tmp_thumb.name, thumbnail_filepath) mgg.public_store.copy_local_to_storage(tmp_thumb.name, thumbnail_filepath)
entry.media_files['thumb'] = thumbnail_filepath entry.media_files['thumb'] = thumbnail_filepath
if video_config['keep_original']: # save the original... but only if we did a transcoding
# (if we skipped transcoding and just kept the original anyway as the main
# media, then why would we save the original twice?)
if video_config['keep_original'] and did_transcode:
# Push original file to public storage # Push original file to public storage
_log.debug('Saving original...') _log.debug('Saving original...')
proc_state.copy_original(queued_filepath[-1]) proc_state.copy_original(queued_filepath[-1])
# Remove queued media file from storage and database # Remove queued media file from storage and database
proc_state.delete_queue_file() proc_state.delete_queue_file()
def store_metadata(media_entry, metadata):
"""
Store metadata from this video for this media entry.
"""
# Let's pull out the easy, not having to be converted ones first
stored_metadata = dict(
[(key, metadata[key])
for key in [
"videoheight", "videolength", "videowidth",
"audiorate", "audiolength", "audiochannels", "audiowidth",
"mimetype", "tags"]
if key in metadata])
# We have to convert videorate into a sequence because it's a
# special type normally..
if "videorate" in metadata:
videorate = metadata["videorate"]
stored_metadata["videorate"] = [videorate.num, videorate.denom]
# Only save this field if there's something to save
if len(stored_metadata):
media_entry.media_data_init(
orig_metadata=stored_metadata)

View File

@ -700,6 +700,7 @@ class VideoTranscoder:
self._setup() self._setup()
self._run() self._run()
# XXX: This could be a static method.
def discover(self, src): def discover(self, src):
''' '''
Discover properties about a media file Discover properties about a media file
@ -820,7 +821,8 @@ class VideoTranscoder:
self.audioconvert = gst.element_factory_make('audioconvert', 'audioconvert') self.audioconvert = gst.element_factory_make('audioconvert', 'audioconvert')
self.pipeline.add(self.audioconvert) self.pipeline.add(self.audioconvert)
self.audiocapsfilter = gst.element_factory_make('capsfilter', 'audiocapsfilter') self.audiocapsfilter = gst.element_factory_make('capsfilter',
'audiocapsfilter')
audiocaps = ['audio/x-raw-float'] audiocaps = ['audio/x-raw-float']
self.audiocapsfilter.set_property( self.audiocapsfilter.set_property(
'caps', 'caps',

View File

@ -0,0 +1,59 @@
# 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
from mediagoblin import mg_globals as mgg
_log = logging.getLogger(__name__)
def skip_transcode(metadata):
'''
Checks video metadata against configuration values for skip_transcode.
Returns True if the video matches the requirements in the configuration.
'''
config = mgg.global_config['media_type:mediagoblin.media_types.video']\
['skip_transcode']
medium_config = mgg.global_config['media:medium']
_log.debug('skip_transcode config: {0}'.format(config))
if config['mime_types'] and metadata.get('mimetype'):
if not metadata['mimetype'] in config['mime_types']:
return False
if config['container_formats'] and metadata['tags'].get('audio-codec'):
if not metadata['tags']['container-format'] in config['container_formats']:
return False
if config['video_codecs'] and metadata['tags'].get('audio-codec'):
if not metadata['tags']['video-codec'] in config['video_codecs']:
return False
if config['audio_codecs'] and metadata['tags'].get('audio-codec'):
if not metadata['tags']['audio-codec'] in config['audio_codecs']:
return False
if config['dimensions_match']:
if not metadata['videoheight'] <= medium_config['max_height']:
return False
if not metadata['videowidth'] <= medium_config['max_width']:
return False
return True

View File

@ -23,7 +23,7 @@
{% set model_download = request.app.public_store.file_url( {% set model_download = request.app.public_store.file_url(
media.get_display_media(media.media_files)) %} media.media_files['original']) %}
{% set perspective_view = request.app.public_store.file_url( {% set perspective_view = request.app.public_store.file_url(
media.media_files['perspective']) %} media.media_files['perspective']) %}
{% set top_view = request.app.public_store.file_url( {% set top_view = request.app.public_store.file_url(

View File

@ -22,19 +22,24 @@
{{ super() }} {{ super() }}
<script type="text/javascript" src="{{ <script type="text/javascript" src="{{
request.staticdirect('/extlib/video-js/video.min.js') }}"></script> request.staticdirect('/extlib/video-js/video.min.js') }}"></script>
<link href="{{ request.staticdirect('/css/vjs-mg-skin.css') <link href="{{ request.staticdirect('/css/vjs-mg-skin.css') }}"
}}" rel="stylesheet"> rel="stylesheet">
{%- endblock %} {%- endblock %}
{% block mediagoblin_media %} {% block mediagoblin_media %}
{% set display_type, display_path = media.get_display_media() %}
<video controls <video controls
{% if global_config['media_type:mediagoblin.media_types.video']['auto_play'] %}autoplay{% endif %} {% if global_config['media_type:mediagoblin.media_types.video']['auto_play'] %}autoplay{% endif %}
preload="auto" class="video-js vjs-mg-skin" preload="auto" class="video-js vjs-mg-skin"
data-setup='{"height": {{ media.media_data.height }}, data-setup='{"height": {{ media.media_data.height }},
"width": {{ media.media_data.width }} }'> "width": {{ media.media_data.width }} }'>
<source src="{{ request.app.public_store.file_url( <source src="{{ request.app.public_store.file_url(display_path) }}"
media.media_files['webm_640']) }}" {% if media.media_data %}
type="video/webm; codecs=&quot;vp8, vorbis&quot;" /> type="{{ media.media_data.source_type() }}"
{% else %}
type="{{ media.media_manager['default_webm_type'] }}"
{% endif %} />
<div class="no_html5"> <div class="no_html5">
{%- trans -%}Sorry, this video will not work because {%- trans -%}Sorry, this video will not work because
your web browser does not support HTML5 your web browser does not support HTML5
@ -50,10 +55,20 @@
<h3>{% trans %}Download{% endtrans %}</h3> <h3>{% trans %}Download{% endtrans %}</h3>
<ul> <ul>
{% if 'original' in media.media_files %} {% if 'original' in media.media_files %}
<li><a href="{{ request.app.public_store.file_url( <li>
media.media_files.original) }}">{% trans %}Original file{% endtrans %}</a> <a href="{{ request.app.public_store.file_url(
media.media_files.original) }}">
{%- trans %}Original file{% endtrans -%}
</a>
</li>
{% endif %}
{% if 'webm_640' in media.media_files %}
<li>
<a href="{{ request.app.public_store.file_url(
media.media_files.webm_640) }}">
{%- trans %}WebM file (640p; VP8/Vorbis){% endtrans -%}
</a>
</li>
{% endif %} {% endif %}
<li><a href="{{ request.app.public_store.file_url(
media.media_files.webm_640) }}">{% trans %}WebM file (640p; VP8/Vorbis){% endtrans %}</a>
</ul> </ul>
{% endblock %} {% endblock %}

View File

@ -47,7 +47,7 @@
<div class="media_image_container"> <div class="media_image_container">
{% block mediagoblin_media %} {% block mediagoblin_media %}
{% set display_media = request.app.public_store.file_url( {% set display_media = request.app.public_store.file_url(
media.get_display_media(media.media_files)) %} media.get_display_media()[1]) %}
{# if there's a medium file size, that means the medium size {# if there's a medium file size, that means the medium size
# isn't the original... so link to the original! # isn't the original... so link to the original!
#} #}

View File

@ -16,7 +16,6 @@
import sys import sys
DISPLAY_IMAGE_FETCHING_ORDER = [u'medium', u'original', u'thumb']
global TESTS_ENABLED global TESTS_ENABLED
TESTS_ENABLED = False TESTS_ENABLED = False

View File

@ -227,7 +227,8 @@ def media_collect(request, media):
# Otherwise, use the collection selected from the drop-down # Otherwise, use the collection selected from the drop-down
else: else:
collection = Collection.query.filter_by( collection = Collection.query.filter_by(
id=request.form.get('collection')).first() id=form.collection.data,
creator=request.user.id).first()
# Make sure the user actually selected a collection # Make sure the user actually selected a collection
if not collection: if not collection:
@ -236,7 +237,7 @@ def media_collect(request, media):
_('You have to select or add a collection')) _('You have to select or add a collection'))
return redirect(request, "mediagoblin.user_pages.media_collect", return redirect(request, "mediagoblin.user_pages.media_collect",
user=media.get_uploader.username, user=media.get_uploader.username,
media=media.id) media_id=media.id)
# Check whether media already exists in collection # Check whether media already exists in collection
@ -250,7 +251,6 @@ def media_collect(request, media):
collection_item = request.db.CollectionItem() collection_item = request.db.CollectionItem()
collection_item.collection = collection.id collection_item.collection = collection.id
collection_item.media_entry = media.id collection_item.media_entry = media.id
collection_item.author = request.user.id
collection_item.note = request.form['note'] collection_item.note = request.form['note']
collection_item.save() collection_item.save()