| 1 | # GNU MediaGoblin -- federated, autonomous media hosting |
| 2 | # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. |
| 3 | # |
| 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU Affero General Public License as published by |
| 6 | # the Free Software Foundation, either version 3 of the License, or |
| 7 | # (at your option) any later version. |
| 8 | # |
| 9 | # This program is distributed in the hope that it will be useful, |
| 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | # GNU Affero General Public License for more details. |
| 13 | # |
| 14 | # You should have received a copy of the GNU Affero General Public License |
| 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | |
| 17 | from tempfile import NamedTemporaryFile |
| 18 | import logging |
| 19 | |
| 20 | from mediagoblin import mg_globals as mgg |
| 21 | from mediagoblin.processing import \ |
| 22 | create_pub_filepath, FilenameBuilder, BaseProcessingFail, ProgressCallback |
| 23 | from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ |
| 24 | |
| 25 | from . import transcoders |
| 26 | from .util import skip_transcode |
| 27 | |
| 28 | |
| 29 | _log = logging.getLogger(__name__) |
| 30 | _log.setLevel(logging.DEBUG) |
| 31 | |
| 32 | |
| 33 | class VideoTranscodingFail(BaseProcessingFail): |
| 34 | ''' |
| 35 | Error raised if video transcoding fails |
| 36 | ''' |
| 37 | general_message = _(u'Video transcoding failed') |
| 38 | |
| 39 | |
| 40 | def sniff_handler(media_file, **kw): |
| 41 | transcoder = transcoders.VideoTranscoder() |
| 42 | data = transcoder.discover(media_file.name) |
| 43 | |
| 44 | _log.debug('Discovered: {0}'.format(data)) |
| 45 | |
| 46 | if not data: |
| 47 | _log.error('Could not discover {0}'.format( |
| 48 | kw.get('media'))) |
| 49 | return False |
| 50 | |
| 51 | if data['is_video'] == True: |
| 52 | return True |
| 53 | |
| 54 | return False |
| 55 | |
| 56 | |
| 57 | def process_video(proc_state): |
| 58 | """ |
| 59 | Process a video entry, transcode the queued media files (originals) and |
| 60 | create a thumbnail for the entry. |
| 61 | |
| 62 | A Workbench() represents a local tempory dir. It is automatically |
| 63 | cleaned up when this function exits. |
| 64 | """ |
| 65 | entry = proc_state.entry |
| 66 | workbench = proc_state.workbench |
| 67 | video_config = mgg.global_config['media_type:mediagoblin.media_types.video'] |
| 68 | |
| 69 | queued_filepath = entry.queued_media_file |
| 70 | queued_filename = proc_state.get_queued_filename() |
| 71 | name_builder = FilenameBuilder(queued_filename) |
| 72 | |
| 73 | medium_filepath = create_pub_filepath( |
| 74 | entry, name_builder.fill('{basename}-640p.webm')) |
| 75 | |
| 76 | thumbnail_filepath = create_pub_filepath( |
| 77 | entry, name_builder.fill('{basename}.thumbnail.jpg')) |
| 78 | |
| 79 | # Create a temporary file for the video destination (cleaned up with workbench) |
| 80 | tmp_dst = NamedTemporaryFile(dir=workbench.dir, delete=False) |
| 81 | with tmp_dst: |
| 82 | # Transcode queued file to a VP8/vorbis file that fits in a 640x640 square |
| 83 | progress_callback = ProgressCallback(entry) |
| 84 | |
| 85 | dimensions = ( |
| 86 | mgg.global_config['media:medium']['max_width'], |
| 87 | mgg.global_config['media:medium']['max_height']) |
| 88 | |
| 89 | # Extract metadata and keep a record of it |
| 90 | metadata = transcoders.VideoTranscoder().discover(queued_filename) |
| 91 | store_metadata(entry, metadata) |
| 92 | |
| 93 | # Figure out whether or not we need to transcode this video or |
| 94 | # if we can skip it |
| 95 | if skip_transcode(metadata): |
| 96 | _log.debug('Skipping transcoding') |
| 97 | # Just push the submitted file to the tmp_dst |
| 98 | open(tmp_dst.name, 'wb').write(open(queued_filename, 'rb').read()) |
| 99 | |
| 100 | dst_dimensions = metadata['videowidth'], metadata['videoheight'] |
| 101 | |
| 102 | # Push original file to public storage |
| 103 | _log.debug('Saving original...') |
| 104 | proc_state.copy_original(queued_filepath[-1]) |
| 105 | |
| 106 | did_transcode = False |
| 107 | else: |
| 108 | transcoder = transcoders.VideoTranscoder() |
| 109 | |
| 110 | transcoder.transcode(queued_filename, tmp_dst.name, |
| 111 | vp8_quality=video_config['vp8_quality'], |
| 112 | vp8_threads=video_config['vp8_threads'], |
| 113 | vorbis_quality=video_config['vorbis_quality'], |
| 114 | progress_callback=progress_callback, |
| 115 | dimensions=dimensions) |
| 116 | |
| 117 | dst_dimensions = transcoder.dst_data.videowidth,\ |
| 118 | transcoder.dst_data.videoheight |
| 119 | |
| 120 | # Push transcoded video to public storage |
| 121 | _log.debug('Saving medium...') |
| 122 | mgg.public_store.copy_local_to_storage(tmp_dst.name, medium_filepath) |
| 123 | _log.debug('Saved medium') |
| 124 | |
| 125 | entry.media_files['webm_640'] = medium_filepath |
| 126 | |
| 127 | did_transcode = True |
| 128 | |
| 129 | # Save the width and height of the transcoded video |
| 130 | entry.media_data_init( |
| 131 | width=dst_dimensions[0], |
| 132 | height=dst_dimensions[1]) |
| 133 | |
| 134 | # Temporary file for the video thumbnail (cleaned up with workbench) |
| 135 | tmp_thumb = NamedTemporaryFile(dir=workbench.dir, suffix='.jpg', delete=False) |
| 136 | |
| 137 | with tmp_thumb: |
| 138 | # Create a thumbnail.jpg that fits in a 180x180 square |
| 139 | transcoders.VideoThumbnailerMarkII( |
| 140 | queued_filename, |
| 141 | tmp_thumb.name, |
| 142 | 180) |
| 143 | |
| 144 | # Push the thumbnail to public storage |
| 145 | _log.debug('Saving thumbnail...') |
| 146 | mgg.public_store.copy_local_to_storage(tmp_thumb.name, thumbnail_filepath) |
| 147 | entry.media_files['thumb'] = thumbnail_filepath |
| 148 | |
| 149 | # save the original... but only if we did a transcoding |
| 150 | # (if we skipped transcoding and just kept the original anyway as the main |
| 151 | # media, then why would we save the original twice?) |
| 152 | if video_config['keep_original'] and did_transcode: |
| 153 | # Push original file to public storage |
| 154 | _log.debug('Saving original...') |
| 155 | proc_state.copy_original(queued_filepath[-1]) |
| 156 | |
| 157 | # Remove queued media file from storage and database |
| 158 | proc_state.delete_queue_file() |
| 159 | |
| 160 | |
| 161 | def store_metadata(media_entry, metadata): |
| 162 | """ |
| 163 | Store metadata from this video for this media entry. |
| 164 | """ |
| 165 | # Let's pull out the easy, not having to be converted ones first |
| 166 | stored_metadata = dict( |
| 167 | [(key, metadata[key]) |
| 168 | for key in [ |
| 169 | "videoheight", "videolength", "videowidth", |
| 170 | "audiorate", "audiolength", "audiochannels", "audiowidth", |
| 171 | "mimetype", "tags"] |
| 172 | if key in metadata]) |
| 173 | |
| 174 | # We have to convert videorate into a sequence because it's a |
| 175 | # special type normally.. |
| 176 | |
| 177 | if "videorate" in metadata: |
| 178 | videorate = metadata["videorate"] |
| 179 | stored_metadata["videorate"] = [videorate.num, videorate.denom] |
| 180 | |
| 181 | media_entry.media_data_init( |
| 182 | orig_metadata=stored_metadata) |