1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
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.
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.
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/>.
22 from mediagoblin
import mg_globals
as mgg
23 from mediagoblin
.processing
import (
24 FilenameBuilder
, BaseProcessingFail
,
25 ProgressCallback
, MediaProcessor
,
26 ProcessingManager
, request_from_args
,
27 get_process_filename
, store_public
,
29 from mediagoblin
.tools
.translate
import lazy_pass_to_ugettext
as _
31 from . import transcoders
32 from .util
import skip_transcode
34 _log
= logging
.getLogger(__name__
)
35 _log
.setLevel(logging
.DEBUG
)
37 MEDIA_TYPE
= 'mediagoblin.media_types.video'
40 class VideoTranscodingFail(BaseProcessingFail
):
42 Error raised if video transcoding fails
44 general_message
= _(u
'Video transcoding failed')
47 def sniff_handler(media_file
, filename
):
48 transcoder
= transcoders
.VideoTranscoder()
49 data
= transcoder
.discover(media_file
.name
)
51 _log
.info('Sniffing {0}'.format(MEDIA_TYPE
))
52 _log
.debug('Discovered: {0}'.format(data
))
55 _log
.error('Could not discover {0}'.format(filename
))
58 if data
['is_video'] is True:
64 def store_metadata(media_entry
, metadata
):
66 Store metadata from this video for this media entry.
68 # Let's pull out the easy, not having to be converted ones first
69 stored_metadata
= dict(
72 "videoheight", "videolength", "videowidth",
73 "audiorate", "audiolength", "audiochannels", "audiowidth",
77 # We have to convert videorate into a sequence because it's a
78 # special type normally..
80 if "videorate" in metadata
:
81 videorate
= metadata
["videorate"]
82 stored_metadata
["videorate"] = [videorate
.num
, videorate
.denom
]
84 # Also make a whitelist conversion of the tags.
85 if "tags" in metadata
:
86 tags_metadata
= metadata
['tags']
88 # we don't use *all* of these, but we know these ones are
91 [(key
, tags_metadata
[key
])
93 "application-name", "artist", "audio-codec", "bitrate",
94 "container-format", "copyright", "encoder",
95 "encoder-version", "license", "nominal-bitrate", "title",
97 if key
in tags_metadata
])
98 if 'date' in tags_metadata
:
99 date
= tags_metadata
['date']
100 tags
['date'] = "%s-%s-%s" % (
101 date
.year
, date
.month
, date
.day
)
103 # TODO: handle timezone info; gst.get_time_zone_offset +
104 # python's tzinfo should help
105 if 'datetime' in tags_metadata
:
106 dt
= tags_metadata
['datetime']
107 tags
['datetime'] = datetime
.datetime(
108 dt
.get_year(), dt
.get_month(), dt
.get_day(), dt
.get_hour(),
109 dt
.get_minute(), dt
.get_second(),
110 dt
.get_microsecond()).isoformat()
112 metadata
['tags'] = tags
114 # Only save this field if there's something to save
115 if len(stored_metadata
):
116 media_entry
.media_data_init(
117 orig_metadata
=stored_metadata
)
120 class CommonVideoProcessor(MediaProcessor
):
122 Provides a base for various video processing steps
124 acceptable_files
= ['original', 'best_quality', 'webm_video']
126 def common_setup(self
):
127 self
.video_config
= mgg \
128 .global_config
['plugins'][MEDIA_TYPE
]
130 # Pull down and set up the processing file
131 self
.process_filename
= get_process_filename(
132 self
.entry
, self
.workbench
, self
.acceptable_files
)
133 self
.name_builder
= FilenameBuilder(self
.process_filename
)
135 self
.transcoder
= transcoders
.VideoTranscoder()
136 self
.did_transcode
= False
138 def copy_original(self
):
139 # If we didn't transcode, then we need to keep the original
140 if not self
.did_transcode
or \
141 (self
.video_config
['keep_original'] and self
.did_transcode
):
143 self
.entry
, self
.process_filename
,
144 self
.name_builder
.fill('{basename}{ext}'))
146 def _keep_best(self
):
148 If there is no original, keep the best file that we have
150 if not self
.entry
.media_files
.get('best_quality'):
151 # Save the best quality file if no original?
152 if not self
.entry
.media_files
.get('original') and \
153 self
.entry
.media_files
.get('webm_video'):
154 self
.entry
.media_files
['best_quality'] = self
.entry \
155 .media_files
['webm_video']
157 def _skip_processing(self
, keyname
, **kwargs
):
158 file_metadata
= self
.entry
.get_file_metadata(keyname
)
160 if not file_metadata
:
164 if keyname
== 'webm_video':
165 if kwargs
.get('medium_size') != file_metadata
.get('medium_size'):
167 elif kwargs
.get('vp8_quality') != file_metadata
.get('vp8_quality'):
169 elif kwargs
.get('vp8_threads') != file_metadata
.get('vp8_threads'):
171 elif kwargs
.get('vorbis_quality') != \
172 file_metadata
.get('vorbis_quality'):
174 elif keyname
== 'thumb':
175 if kwargs
.get('thumb_size') != file_metadata
.get('thumb_size'):
181 def transcode(self
, medium_size
=None, vp8_quality
=None, vp8_threads
=None,
182 vorbis_quality
=None):
183 progress_callback
= ProgressCallback(self
.entry
)
184 tmp_dst
= os
.path
.join(self
.workbench
.dir,
185 self
.name_builder
.fill('{basename}.medium.webm'))
189 mgg
.global_config
['media:medium']['max_width'],
190 mgg
.global_config
['media:medium']['max_height'])
192 vp8_quality
= self
.video_config
['vp8_quality']
194 vp8_threads
= self
.video_config
['vp8_threads']
195 if not vorbis_quality
:
196 vorbis_quality
= self
.video_config
['vorbis_quality']
198 file_metadata
= {'medium_size': medium_size
,
199 'vp8_threads': vp8_threads
,
200 'vp8_quality': vp8_quality
,
201 'vorbis_quality': vorbis_quality
}
203 if self
._skip
_processing
('webm_video', **file_metadata
):
206 # Extract metadata and keep a record of it
207 metadata
= self
.transcoder
.discover(self
.process_filename
)
208 store_metadata(self
.entry
, metadata
)
210 # Figure out whether or not we need to transcode this video or
212 if skip_transcode(metadata
, medium_size
):
213 _log
.debug('Skipping transcoding')
215 dst_dimensions
= metadata
['videowidth'], metadata
['videoheight']
217 # If there is an original and transcoded, delete the transcoded
218 # since it must be of lower quality then the original
219 if self
.entry
.media_files
.get('original') and \
220 self
.entry
.media_files
.get('webm_video'):
221 self
.entry
.media_files
['webm_video'].delete()
224 self
.transcoder
.transcode(self
.process_filename
, tmp_dst
,
225 vp8_quality
=vp8_quality
,
226 vp8_threads
=vp8_threads
,
227 vorbis_quality
=vorbis_quality
,
228 progress_callback
=progress_callback
,
229 dimensions
=tuple(medium_size
))
231 dst_dimensions
= self
.transcoder
.dst_data
.videowidth
,\
232 self
.transcoder
.dst_data
.videoheight
236 # Push transcoded video to public storage
237 _log
.debug('Saving medium...')
238 store_public(self
.entry
, 'webm_video', tmp_dst
,
239 self
.name_builder
.fill('{basename}.medium.webm'))
240 _log
.debug('Saved medium')
242 self
.entry
.set_file_metadata('webm_video', **file_metadata
)
244 self
.did_transcode
= True
246 # Save the width and height of the transcoded video
247 self
.entry
.media_data_init(
248 width
=dst_dimensions
[0],
249 height
=dst_dimensions
[1])
251 def generate_thumb(self
, thumb_size
=None):
252 # Temporary file for the video thumbnail (cleaned up with workbench)
253 tmp_thumb
= os
.path
.join(self
.workbench
.dir,
254 self
.name_builder
.fill(
255 '{basename}.thumbnail.jpg'))
258 thumb_size
= (mgg
.global_config
['media:thumb']['max_width'],)
260 if self
._skip
_processing
('thumb', thumb_size
=thumb_size
):
263 # We will only use the width so that the correct scale is kept
264 transcoders
.VideoThumbnailerMarkII(
265 self
.process_filename
,
269 # Push the thumbnail to public storage
270 _log
.debug('Saving thumbnail...')
271 store_public(self
.entry
, 'thumb', tmp_thumb
,
272 self
.name_builder
.fill('{basename}.thumbnail.jpg'))
274 self
.entry
.set_file_metadata('thumb', thumb_size
=thumb_size
)
276 class InitialProcessor(CommonVideoProcessor
):
278 Initial processing steps for new video
281 description
= "Initial processing"
284 def media_is_eligible(cls
, entry
=None, state
=None):
288 "unprocessed", "failed")
291 def generate_parser(cls
):
292 parser
= argparse
.ArgumentParser(
293 description
=cls
.description
,
299 metavar
=('max_width', 'max_height'),
310 help='0 means number_of_CPUs - 1')
315 help='Range -0.1..1')
320 metavar
=('max_width', 'max_height'),
326 def args_to_request(cls
, args
):
327 return request_from_args(
328 args
, ['medium_size', 'vp8_quality', 'vp8_threads',
329 'vorbis_quality', 'thumb_size'])
331 def process(self
, medium_size
=None, vp8_threads
=None, vp8_quality
=None,
332 vorbis_quality
=None, thumb_size
=None):
335 self
.transcode(medium_size
=medium_size
, vp8_quality
=vp8_quality
,
336 vp8_threads
=vp8_threads
, vorbis_quality
=vorbis_quality
)
339 self
.generate_thumb(thumb_size
=thumb_size
)
340 self
.delete_queue_file()
343 class Resizer(CommonVideoProcessor
):
345 Video thumbnail resizing process steps for processed media
348 description
= 'Resize thumbnail'
349 thumb_size
= 'thumb_size'
352 def media_is_eligible(cls
, entry
=None, state
=None):
355 return state
in 'processed'
358 def generate_parser(cls
):
359 parser
= argparse
.ArgumentParser(
360 description
=cls
.description
,
366 metavar
=('max_width', 'max_height'),
369 # Needed for gmg reprocess thumbs to work
379 def args_to_request(cls
, args
):
380 return request_from_args(
381 args
, ['thumb_size', 'file'])
383 def process(self
, thumb_size
=None, file=None):
385 self
.generate_thumb(thumb_size
=thumb_size
)
388 class Transcoder(CommonVideoProcessor
):
390 Transcoding processing steps for processed video
393 description
= 'Re-transcode video'
396 def media_is_eligible(cls
, entry
=None, state
=None):
399 return state
in 'processed'
402 def generate_parser(cls
):
403 parser
= argparse
.ArgumentParser(
404 description
=cls
.description
,
410 metavar
=('max_width', 'max_height'),
421 help='0 means number_of_CPUs - 1')
426 help='Range -0.1..1')
431 def args_to_request(cls
, args
):
432 return request_from_args(
433 args
, ['medium_size', 'vp8_threads', 'vp8_quality',
436 def process(self
, medium_size
=None, vp8_quality
=None, vp8_threads
=None,
437 vorbis_quality
=None):
439 self
.transcode(medium_size
=medium_size
, vp8_threads
=vp8_threads
,
440 vp8_quality
=vp8_quality
, vorbis_quality
=vorbis_quality
)
443 class VideoProcessingManager(ProcessingManager
):
445 super(self
.__class
__, self
).__init
__()
446 self
.add_processor(InitialProcessor
)
447 self
.add_processor(Resizer
)
448 self
.add_processor(Transcoder
)