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
, **kw
):
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(
59 if data
['is_video'] is True:
65 def store_metadata(media_entry
, metadata
):
67 Store metadata from this video for this media entry.
69 # Let's pull out the easy, not having to be converted ones first
70 stored_metadata
= dict(
73 "videoheight", "videolength", "videowidth",
74 "audiorate", "audiolength", "audiochannels", "audiowidth",
78 # We have to convert videorate into a sequence because it's a
79 # special type normally..
81 if "videorate" in metadata
:
82 videorate
= metadata
["videorate"]
83 stored_metadata
["videorate"] = [videorate
.num
, videorate
.denom
]
85 # Also make a whitelist conversion of the tags.
86 if "tags" in metadata
:
87 tags_metadata
= metadata
['tags']
89 # we don't use *all* of these, but we know these ones are
92 [(key
, tags_metadata
[key
])
94 "application-name", "artist", "audio-codec", "bitrate",
95 "container-format", "copyright", "encoder",
96 "encoder-version", "license", "nominal-bitrate", "title",
98 if key
in tags_metadata
])
99 if 'date' in tags_metadata
:
100 date
= tags_metadata
['date']
101 tags
['date'] = "%s-%s-%s" % (
102 date
.year
, date
.month
, date
.day
)
104 # TODO: handle timezone info; gst.get_time_zone_offset +
105 # python's tzinfo should help
106 if 'datetime' in tags_metadata
:
107 dt
= tags_metadata
['datetime']
108 tags
['datetime'] = datetime
.datetime(
109 dt
.get_year(), dt
.get_month(), dt
.get_day(), dt
.get_hour(),
110 dt
.get_minute(), dt
.get_second(),
111 dt
.get_microsecond()).isoformat()
113 metadata
['tags'] = tags
115 # Only save this field if there's something to save
116 if len(stored_metadata
):
117 media_entry
.media_data_init(
118 orig_metadata
=stored_metadata
)
121 class CommonVideoProcessor(MediaProcessor
):
123 Provides a base for various video processing steps
125 acceptable_files
= ['original', 'best_quality', 'webm_video']
127 def common_setup(self
):
128 self
.video_config
= mgg \
129 .global_config
['media_type:mediagoblin.media_types.video']
131 # Pull down and set up the processing file
132 self
.process_filename
= get_process_filename(
133 self
.entry
, self
.workbench
, self
.acceptable_files
)
134 self
.name_builder
= FilenameBuilder(self
.process_filename
)
136 self
.transcoder
= transcoders
.VideoTranscoder()
137 self
.did_transcode
= False
139 def copy_original(self
):
140 # If we didn't transcode, then we need to keep the original
141 if not self
.did_transcode
or \
142 (self
.video_config
['keep_original'] and self
.did_transcode
):
144 self
.entry
, self
.process_filename
,
145 self
.name_builder
.fill('{basename}{ext}'))
147 def _keep_best(self
):
149 If there is no original, keep the best file that we have
151 if not self
.entry
.media_files
.get('best_quality'):
152 # Save the best quality file if no original?
153 if not self
.entry
.media_files
.get('original') and \
154 self
.entry
.media_files
.get('webm_video'):
155 self
.entry
.media_files
['best_quality'] = self
.entry \
156 .media_files
['webm_video']
159 def transcode(self
, medium_size
=None, vp8_quality
=None, vp8_threads
=None,
160 vorbis_quality
=None):
161 progress_callback
= ProgressCallback(self
.entry
)
162 tmp_dst
= os
.path
.join(self
.workbench
.dir,
163 self
.name_builder
.fill('{basename}.medium.webm'))
167 mgg
.global_config
['media:medium']['max_width'],
168 mgg
.global_config
['media:medium']['max_height'])
170 vp8_quality
= self
.video_config
['vp8_quality']
172 vp8_threads
= self
.video_config
['vp8_threads']
173 if not vorbis_quality
:
174 vorbis_quality
= self
.video_config
['vorbis_quality']
176 # Extract metadata and keep a record of it
177 metadata
= self
.transcoder
.discover(self
.process_filename
)
178 store_metadata(self
.entry
, metadata
)
180 # Figure out whether or not we need to transcode this video or
182 if skip_transcode(metadata
, medium_size
):
183 _log
.debug('Skipping transcoding')
185 dst_dimensions
= metadata
['videowidth'], metadata
['videoheight']
187 # If there is an original and transcoded, delete the transcoded
188 # since it must be of lower quality then the original
189 if self
.entry
.media_files
.get('original') and \
190 self
.entry
.media_files
.get('webm_video'):
191 self
.entry
.media_files
['webm_video'].delete()
194 self
.transcoder
.transcode(self
.process_filename
, tmp_dst
,
195 vp8_quality
=vp8_quality
,
196 vp8_threads
=vp8_threads
,
197 vorbis_quality
=vorbis_quality
,
198 progress_callback
=progress_callback
,
199 dimensions
=tuple(medium_size
))
201 dst_dimensions
= self
.transcoder
.dst_data
.videowidth
,\
202 self
.transcoder
.dst_data
.videoheight
206 # Push transcoded video to public storage
207 _log
.debug('Saving medium...')
208 store_public(self
.entry
, 'webm_video', tmp_dst
,
209 self
.name_builder
.fill('{basename}.medium.webm'))
210 _log
.debug('Saved medium')
212 self
.did_transcode
= True
214 # Save the width and height of the transcoded video
215 self
.entry
.media_data_init(
216 width
=dst_dimensions
[0],
217 height
=dst_dimensions
[1])
219 def generate_thumb(self
, thumb_size
=None):
220 # Temporary file for the video thumbnail (cleaned up with workbench)
221 tmp_thumb
= os
.path
.join(self
.workbench
.dir,
222 self
.name_builder
.fill(
223 '{basename}.thumbnail.jpg'))
226 thumb_size
= (mgg
.global_config
['media:thumb']['max_width'],
227 mgg
.global_config
['media:thumb']['max_height'])
229 transcoders
.VideoThumbnailerMarkII(
230 self
.process_filename
,
235 # Push the thumbnail to public storage
236 _log
.debug('Saving thumbnail...')
237 store_public(self
.entry
, 'thumb', tmp_thumb
,
238 self
.name_builder
.fill('{basename}.thumbnail.jpg'))
241 class InitialProcessor(CommonVideoProcessor
):
243 Initial processing steps for new video
246 description
= "Initial processing"
249 def media_is_eligible(cls
, entry
=None, state
=None):
253 "unprocessed", "failed")
256 def generate_parser(cls
):
257 parser
= argparse
.ArgumentParser(
258 description
=cls
.description
,
264 metavar
=('max_width', 'max_height'),
275 help='0 means number_of_CPUs - 1')
280 help='Range -0.1..1')
285 metavar
=('max_width', 'max_height'),
291 def args_to_request(cls
, args
):
292 return request_from_args(
293 args
, ['medium_size', 'vp8_quality', 'vp8_threads',
294 'vorbis_quality', 'thumb_size'])
296 def process(self
, medium_size
=None, vp8_threads
=None, vp8_quality
=None,
297 vorbis_quality
=None, thumb_size
=None):
300 self
.transcode(medium_size
=medium_size
, vp8_quality
=vp8_quality
,
301 vp8_threads
=vp8_threads
, vorbis_quality
=vorbis_quality
)
304 self
.generate_thumb(thumb_size
=thumb_size
)
305 self
.delete_queue_file()
308 class Resizer(CommonVideoProcessor
):
310 Video thumbnail resizing process steps for processed media
313 description
= 'Resize thumbnail'
314 thumb_size
= 'thumb_size'
317 def media_is_eligible(cls
, entry
=None, state
=None):
320 return state
in 'processed'
323 def generate_parser(cls
):
324 parser
= argparse
.ArgumentParser(
325 description
=cls
.description
,
331 metavar
=('max_width', 'max_height'),
334 # Needed for gmg reprocess thumbs to work
344 def args_to_request(cls
, args
):
345 return request_from_args(
346 args
, ['thumb_size', 'file'])
348 def process(self
, thumb_size
=None, file=None):
350 self
.generate_thumb(thumb_size
=thumb_size
)
353 class Transcoder(CommonVideoProcessor
):
355 Transcoding processing steps for processed video
358 description
= 'Re-transcode video'
361 def media_is_eligible(cls
, entry
=None, state
=None):
364 return state
in 'processed'
367 def generate_parser(cls
):
368 parser
= argparse
.ArgumentParser(
369 description
=cls
.description
,
375 metavar
=('max_width', 'max_height'),
386 help='0 means number_of_CPUs - 1')
391 help='Range -0.1..1')
396 def args_to_request(cls
, args
):
397 return request_from_args(
398 args
, ['medium_size', 'vp8_threads', 'vp8_quality',
401 def process(self
, medium_size
=None, vp8_quality
=None, vp8_threads
=None,
402 vorbis_quality
=None):
404 self
.transcode(medium_size
=medium_size
, vp8_threads
=vp8_threads
,
405 vp8_quality
=vp8_quality
, vorbis_quality
=vorbis_quality
)
408 class VideoProcessingManager(ProcessingManager
):
410 super(self
.__class
__, self
).__init
__()
411 self
.add_processor(InitialProcessor
)
412 self
.add_processor(Resizer
)
413 self
.add_processor(Transcoder
)