From 5c754fdaeeddb1bbeff165bbecb77e33b75b3c7d Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Tue, 22 Jan 2013 22:44:19 +0100 Subject: [PATCH 01/16] Added option to skip transcoding - If the video input matches the configurable rules, just copy it to the output without transcoding it. --- mediagoblin/config_spec.ini | 6 ++ mediagoblin/media_types/video/processing.py | 59 ++++++++++++------- mediagoblin/media_types/video/transcoders.py | 4 +- mediagoblin/media_types/video/util.py | 60 ++++++++++++++++++++ 4 files changed, 109 insertions(+), 20 deletions(-) create mode 100644 mediagoblin/media_types/video/util.py diff --git a/mediagoblin/config_spec.ini b/mediagoblin/config_spec.ini index bee67d46..712d087e 100644 --- a/mediagoblin/config_spec.ini +++ b/mediagoblin/config_spec.ini @@ -97,6 +97,12 @@ vp8_quality = integer(default=8) # Range: -0.1..1 vorbis_quality = float(default=0.3) +[[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] keep_original = boolean(default=True) diff --git a/mediagoblin/media_types/video/processing.py b/mediagoblin/media_types/video/processing.py index 4c9f0131..53fe1a73 100644 --- a/mediagoblin/media_types/video/processing.py +++ b/mediagoblin/media_types/video/processing.py @@ -24,6 +24,8 @@ from mediagoblin.processing import \ from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ from . import transcoders +from .util import skip_transcode + _log = logging.getLogger(__name__) _log.setLevel(logging.DEBUG) @@ -80,24 +82,43 @@ def process_video(entry, workbench=None): with tmp_dst: # Transcode queued file to a VP8/vorbis file that fits in a 640x640 square 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 - _log.debug('Saving medium...') - mgg.public_store.copy_local_to_storage(tmp_dst.name, medium_filepath) - _log.debug('Saved medium') + dimensions = ( + mgg.global_config['media:medium']['max_width'], + mgg.global_config['media:medium']['max_height']) - entry.media_files['webm_640'] = medium_filepath + metadata = transcoders.VideoTranscoder().discover(queued_filename) - # Save the width and height of the transcoded video - entry.media_data_init( - width=transcoder.dst_data.videowidth, - height=transcoder.dst_data.videoheight) + if skip_transcode(metadata): + _log.debug('Skipping transcoding') + # Just push the submitted file to the tmp_dst + open(tmp_dst.name, 'wb').write(open(queued_filename, 'rb').read()) + + dst_dimensions = metadata['videowidth'], metadata['videoheight'] + 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 + + # 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) tmp_thumb = NamedTemporaryFile(dir=workbench.dir, suffix='.jpg', delete=False) @@ -109,10 +130,10 @@ def process_video(entry, workbench=None): tmp_thumb.name, 180) - # Push the thumbnail to public storage - _log.debug('Saving thumbnail...') - mgg.public_store.copy_local_to_storage(tmp_thumb.name, thumbnail_filepath) - entry.media_files['thumb'] = thumbnail_filepath + # Push the thumbnail to public storage + _log.debug('Saving thumbnail...') + mgg.public_store.copy_local_to_storage(tmp_thumb.name, thumbnail_filepath) + entry.media_files['thumb'] = thumbnail_filepath if video_config['keep_original']: # Push original file to public storage diff --git a/mediagoblin/media_types/video/transcoders.py b/mediagoblin/media_types/video/transcoders.py index 152de288..8aa7121f 100644 --- a/mediagoblin/media_types/video/transcoders.py +++ b/mediagoblin/media_types/video/transcoders.py @@ -673,6 +673,7 @@ class VideoTranscoder: self._setup() self._run() + # XXX: This could be a static method. def discover(self, src): ''' Discover properties about a media file @@ -793,7 +794,8 @@ class VideoTranscoder: self.audioconvert = gst.element_factory_make('audioconvert', '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'] self.audiocapsfilter.set_property( 'caps', diff --git a/mediagoblin/media_types/video/util.py b/mediagoblin/media_types/video/util.py new file mode 100644 index 00000000..93f098f7 --- /dev/null +++ b/mediagoblin/media_types/video/util.py @@ -0,0 +1,60 @@ +# 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 . + +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 + From fd693e368bf7d344c29ff99b77acbb92f4524b73 Mon Sep 17 00:00:00 2001 From: Christopher Allan Webber Date: Fri, 1 Mar 2013 17:28:07 -0600 Subject: [PATCH 02/16] If we're not transcoding, copy this file directly over to ['original'] There's no reason to copy it over to 'webm_640' in such a case, clearly. Added logic so we don't do it twice either. Haven't tested this yet though ;) This commit sponsored by Algot Runeman. Thank you! --- mediagoblin/media_types/video/processing.py | 23 +++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/mediagoblin/media_types/video/processing.py b/mediagoblin/media_types/video/processing.py index 41929f3d..32313be7 100644 --- a/mediagoblin/media_types/video/processing.py +++ b/mediagoblin/media_types/video/processing.py @@ -94,6 +94,12 @@ def process_video(proc_state): open(tmp_dst.name, 'wb').write(open(queued_filename, 'rb').read()) 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() @@ -107,12 +113,14 @@ def process_video(proc_state): 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') + # 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 + entry.media_files['webm_640'] = medium_filepath + + did_transcode = True # Save the width and height of the transcoded video entry.media_data_init( @@ -134,7 +142,10 @@ def process_video(proc_state): mgg.public_store.copy_local_to_storage(tmp_thumb.name, 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 _log.debug('Saving original...') proc_state.copy_original(queued_filepath[-1]) From ddbf6af1e20222882a7ce559804ed48f4ad31a92 Mon Sep 17 00:00:00 2001 From: Christopher Allan Webber Date: Sat, 2 Mar 2013 19:06:31 -0600 Subject: [PATCH 03/16] Huge amount of work to (mostly) allow .ogg (and maybe other) formats to skip transcode - Update get_display_media in several ways: - now uses the media type's own declaration of the order of things - returns both the media_size and the media_path, as per the docstring - implicitly uses self.media_files as opposed to forcing you to pass it in - update videos to use get_display_media - update images to declare media_fetch_order in the media manager (videos also) - update stl to use media.media_files['original'] instead of weird use of get_display_media - update sidebar to only conditionally show webm_640 TODO still: identify video type information *during* processing, show that in the element. This commit sponsored by Nathan Yergler. Thanks, nyergler! --- mediagoblin/db/mixin.py | 24 ++++++++++++------- mediagoblin/media_types/image/__init__.py | 6 ++++- mediagoblin/media_types/video/__init__.py | 6 ++++- .../mediagoblin/media_displays/stl.html | 2 +- .../mediagoblin/media_displays/video.html | 8 ++++--- .../mediagoblin/user_pages/media.html | 2 +- mediagoblin/tools/common.py | 1 - 7 files changed, 32 insertions(+), 17 deletions(-) diff --git a/mediagoblin/db/mixin.py b/mediagoblin/db/mixin.py index 6789a970..c4bd806c 100644 --- a/mediagoblin/db/mixin.py +++ b/mediagoblin/db/mixin.py @@ -126,24 +126,30 @@ class MediaEntryMixin(object): """ return cleaned_markdown_conversion(self.description) - def get_display_media(self, media_map, - fetch_order=common.DISPLAY_IMAGE_FETCHING_ORDER): + def get_display_media(self, fetch_order=None): """ Find the best media for display. Args: - - media_map: a dict like - {u'image_size': [u'dir1', u'dir2', u'image.jpg']} - - fetch_order: the order we should try fetching images in + - fetch_order: the order we should try fetching images in. + If this isn't supplied, we try checking + self.media_data.fetching_order if it exists. Returns: - (media_size, media_path) + (media_size, media_path) + or, if not found, None. """ - media_sizes = media_map.keys() + fetch_order = self.media_manager.get("media_fetch_order") - for media_size in common.DISPLAY_IMAGE_FETCHING_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: - return media_map[media_size] + return media_size, self.media_files[media_size] def main_mediafile(self): pass diff --git a/mediagoblin/media_types/image/__init__.py b/mediagoblin/media_types/image/__init__.py index 36d7c201..3e167db1 100644 --- a/mediagoblin/media_types/image/__init__.py +++ b/mediagoblin/media_types/image/__init__.py @@ -25,4 +25,8 @@ MEDIA_MANAGER = { "sniff_handler": sniff_handler, "display_template": "mediagoblin/media_displays/image.html", "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'], +} diff --git a/mediagoblin/media_types/video/__init__.py b/mediagoblin/media_types/video/__init__.py index 3faa5b9f..fd364c02 100644 --- a/mediagoblin/media_types/video/__init__.py +++ b/mediagoblin/media_types/video/__init__.py @@ -26,4 +26,8 @@ MEDIA_MANAGER = { "display_template": "mediagoblin/media_displays/video.html", "default_thumb": "images/media_thumbs/video.jpg", "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'], +} diff --git a/mediagoblin/templates/mediagoblin/media_displays/stl.html b/mediagoblin/templates/mediagoblin/media_displays/stl.html index 043faac8..a89e0b4f 100644 --- a/mediagoblin/templates/mediagoblin/media_displays/stl.html +++ b/mediagoblin/templates/mediagoblin/media_displays/stl.html @@ -23,7 +23,7 @@ {% 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( media.media_files['perspective']) %} {% set top_view = request.app.public_store.file_url( diff --git a/mediagoblin/templates/mediagoblin/media_displays/video.html b/mediagoblin/templates/mediagoblin/media_displays/video.html index 9eeb7c85..2e33e1a3 100644 --- a/mediagoblin/templates/mediagoblin/media_displays/video.html +++ b/mediagoblin/templates/mediagoblin/media_displays/video.html @@ -33,7 +33,7 @@ data-setup='{"height": {{ media.media_data.height }}, "width": {{ media.media_data.width }} }'>
{%- trans -%}Sorry, this video will not work because @@ -53,7 +53,9 @@
  • {% trans %}Original file{% endtrans %} {% endif %} -
  • {% trans %}WebM file (640p; VP8/Vorbis){% endtrans %} + {% if 'webm_640' in media.media_files %} +
  • {% trans %}WebM file (640p; VP8/Vorbis){% endtrans %} + {% endif %} {% endblock %} diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html index f151c577..b77c12b9 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/media.html +++ b/mediagoblin/templates/mediagoblin/user_pages/media.html @@ -47,7 +47,7 @@
    {% block mediagoblin_media %} {% 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 # isn't the original... so link to the original! #} diff --git a/mediagoblin/tools/common.py b/mediagoblin/tools/common.py index c9f9d032..34586611 100644 --- a/mediagoblin/tools/common.py +++ b/mediagoblin/tools/common.py @@ -16,7 +16,6 @@ import sys -DISPLAY_IMAGE_FETCHING_ORDER = [u'medium', u'original', u'thumb'] global TESTS_ENABLED TESTS_ENABLED = False From 29adab4651cd2485441a08bb97ae0165e5be0017 Mon Sep 17 00:00:00 2001 From: Christopher Allan Webber Date: Sun, 3 Mar 2013 10:36:37 -0600 Subject: [PATCH 04/16] Now store metadata info from processing into the media type. This comes in several parts: - Store the metadata from gstreamer during processing - Add a new JSONEncoded field to the VideoData table - And, of course, add a migration for that field! This commit sponsored by Julius Tuomisto. Thank you, Julius! --- mediagoblin/media_types/video/migrations.py | 15 +++++++++++ mediagoblin/media_types/video/models.py | 19 ++++++++++++++ mediagoblin/media_types/video/processing.py | 28 +++++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/mediagoblin/media_types/video/migrations.py b/mediagoblin/media_types/video/migrations.py index f54c23ea..442bbd8d 100644 --- a/mediagoblin/media_types/video/migrations.py +++ b/mediagoblin/media_types/video/migrations.py @@ -14,4 +14,19 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from mediagoblin.db.migration_tools import RegisterMigration, inspect_table + +from sqlalchemy import MetaData, Column, Unicode + 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() diff --git a/mediagoblin/media_types/video/models.py b/mediagoblin/media_types/video/models.py index a771352c..e0043718 100644 --- a/mediagoblin/media_types/video/models.py +++ b/mediagoblin/media_types/video/models.py @@ -20,12 +20,29 @@ from mediagoblin.db.base import Base from sqlalchemy import ( Column, Integer, SmallInteger, ForeignKey) from sqlalchemy.orm import relationship, backref +from mediagoblin.db.extratypes import JSONEncoded BACKREF_NAME = "video__media_data" 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" # The primary key *and* reference to the main media_entry @@ -38,6 +55,8 @@ class VideoData(Base): width = Column(SmallInteger) height = Column(SmallInteger) + orig_metadata = Column(JSONEncoded) + DATA_MODEL = VideoData MODELS = [VideoData] diff --git a/mediagoblin/media_types/video/processing.py b/mediagoblin/media_types/video/processing.py index 32313be7..ec9ff225 100644 --- a/mediagoblin/media_types/video/processing.py +++ b/mediagoblin/media_types/video/processing.py @@ -86,8 +86,12 @@ def process_video(proc_state): mgg.global_config['media:medium']['max_width'], mgg.global_config['media:medium']['max_height']) + # Extract metadata and keep a record of it metadata = transcoders.VideoTranscoder().discover(queued_filename) + store_metadata(entry, metadata) + # Figure out whether or not we need to transcode this video or + # if we can skip it if skip_transcode(metadata): _log.debug('Skipping transcoding') # Just push the submitted file to the tmp_dst @@ -152,3 +156,27 @@ def process_video(proc_state): # Remove queued media file from storage and database 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] + + media_entry.media_data_init( + orig_metadata=stored_metadata) From 3ff006ef3b3b4dbd302ef8c30f5c3062e2954973 Mon Sep 17 00:00:00 2001 From: Christopher Allan Webber Date: Sun, 3 Mar 2013 10:38:06 -0600 Subject: [PATCH 05/16] pdb.set_trace() in mediagoblin code is Not Allowed(TM), removing from audio code Of course, the version that appears here is not really dangerous because it's for the "call the file individually" form of debugging, but it isn't allowed anyway. This commit sponsored by Michael Faryniarz. Thanks! --- mediagoblin/media_types/audio/transcoders.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mediagoblin/media_types/audio/transcoders.py b/mediagoblin/media_types/audio/transcoders.py index f3d49c30..3a9a2125 100644 --- a/mediagoblin/media_types/audio/transcoders.py +++ b/mediagoblin/media_types/audio/transcoders.py @@ -14,7 +14,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import pdb import logging import Image @@ -233,5 +232,3 @@ if __name__ == '__main__': thumbnailer = AudioThumbnailer() thumbnailer.spectrogram(*sys.argv[1:], width=640) - - pdb.set_trace() From 14814f217af471538774a5861469af157735d893 Mon Sep 17 00:00:00 2001 From: Christopher Allan Webber Date: Sun, 3 Mar 2013 11:03:30 -0600 Subject: [PATCH 06/16] {% set %} the display_type and display_path and use that elsewhere This will make some stuff a bit cleaner that's coming up... This commit sponsored by J B Nicholson-Owens. Thanks! --- mediagoblin/templates/mediagoblin/media_displays/video.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mediagoblin/templates/mediagoblin/media_displays/video.html b/mediagoblin/templates/mediagoblin/media_displays/video.html index 2e33e1a3..06f5f226 100644 --- a/mediagoblin/templates/mediagoblin/media_displays/video.html +++ b/mediagoblin/templates/mediagoblin/media_displays/video.html @@ -27,13 +27,14 @@ {%- endblock %} {% block mediagoblin_media %} + {% set display_type, display_path = media.get_display_media() %} +
  • elements. This commit sponsored by Jukka Hellen. Thanks! --- .../mediagoblin/media_displays/video.html | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/mediagoblin/templates/mediagoblin/media_displays/video.html b/mediagoblin/templates/mediagoblin/media_displays/video.html index 06f5f226..d30c6a2b 100644 --- a/mediagoblin/templates/mediagoblin/media_displays/video.html +++ b/mediagoblin/templates/mediagoblin/media_displays/video.html @@ -22,8 +22,8 @@ {{ super() }} - + {%- endblock %} {% block mediagoblin_media %} @@ -32,8 +32,8 @@