diff -r 000000000000 -r fd39db613f8b src/pyams_media/video.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_media/video.py Wed Sep 02 15:31:55 2015 +0200 @@ -0,0 +1,163 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import os.path +import subprocess + +from tempfile import NamedTemporaryFile + +# import interfaces +from pyams_file.interfaces import IVideo, IThumbnail +from zope.annotation.interfaces import IAnnotations +from zope.traversing.interfaces import ITraversable + +# import packages +import transaction + +from pyams_file.file import ImageFile, get_magic_content_type +from pyams_file.image import ThumbnailGeometrry +from pyams_media.ffbase import FFmpeg +from pyams_utils.adapter import adapter_config, ContextAdapter +from pyramid.threadlocal import get_current_registry +from zope.lifecycleevent import ObjectCreatedEvent, ObjectAddedEvent +from zope.location import locate + + +THUMBNAIL_ANNOTATION_KEY = 'pyams_media.video.thumbnail' + + +@adapter_config(context=IVideo, provides=IThumbnail) +class VideoThumbnailAdapter(object): + """Video thumbnail adapter""" + + def __init__(self, video): + self.video = video + annotations = IAnnotations(video) + self.thumbnail = annotations.get(THUMBNAIL_ANNOTATION_KEY) + + def get_image_size(self): + if self.thumbnail is not None: + return self.thumbnail.get_image_size() + else: + mpeg = FFmpeg('ffprobe') + streams = mpeg.info(self.video) + if streams: + for stream in streams: + if stream.get('codec_type') != 'video': + continue + return stream.get('width'), stream.get('height') + + def get_thumbnail_size(self, thumbnail_name, forced=False): + if self.thumbnail is not None: + return IThumbnail(self.thumbnail).get_thumbnail_size(thumbnail_name, forced) + else: + return self.get_image_size() + + def get_thumbnail_geometry(self, thumbnail_name): + if self.thumbnail is not None: + return IThumbnail(self.thumbnail).get_thumbnail_geometry(thumbnail_name) + else: + size = self.get_image_size() + if size: + geometry = ThumbnailGeometrry() + geometry.x1 = 0 + geometry.y1 = 0 + geometry.x2 = size[0] + geometry.y2 = size[1] + return geometry + + def set_thumbnail_geometry(self, thumbnail_name, geometry): + if self.thumbnail is not None: + IThumbnail(self.thumbnail).set_thumbnail_geometry(thumbnail_name, geometry) + + def clear_geometries(self): + if self.thumbnail is not None: + IThumbnail(self.thumbnail).clear_geometries() + + def get_thumbnail_name(self, thumbnail_name, with_size=False): + if self.thumbnail is not None: + return IThumbnail(self.thumbnail).get_thumbnail_name(thumbnail_name, with_size) + else: + size = self.get_image_size() + if size is not None: + if with_size: + return '{0}x{1}'.format(*size), size + else: + return '{0}x{1}'.format(*size) + else: + return None, None + + def get_thumbnail(self, thumbnail_name, format=None, time=5): + if self.thumbnail is None: + pipe = subprocess.Popen(('ffmpeg', '-i', '-', '-ss', str(time), '-f', 'image2', '-vframes', '1', '-'), + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if pipe: + stdout, stderr = pipe.communicate(self.video.data) + # Some videos formats can't be converted via pipes + # If so, we must provide a temporay file... + if not stdout: + output = NamedTemporaryFile(prefix='video_', suffix='.thumb') + output.write(self.video.data) + output.file.flush() + pipe = subprocess.Popen(('ffmpeg', '-i', output.name, '-ss', str(time), '-f', 'image2', + '-vframes', '1', '-'), + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if pipe: + stdout, stderr = pipe.communicate() + # Create final image + registry = get_current_registry() + annotations = IAnnotations(self.video) + image = ImageFile(stdout) + image.content_type = get_magic_content_type(image.data) + registry.notify(ObjectCreatedEvent(image)) + self.thumbnail = annotations[THUMBNAIL_ANNOTATION_KEY] = image + locate(self.thumbnail, self.video) + registry.notify(ObjectAddedEvent(image, self.video)) + if self.thumbnail is not None: + size_name = '{0[0]}x{0[1]}'.format(self.get_image_size()) + if thumbnail_name != size_name: + watermark = os.path.abspath(os.path.join(__file__, '..', + 'skin', 'resources', 'img', 'video-play-mask.png')) + return IThumbnail(self.thumbnail).get_thumbnail(thumbnail_name, format, watermark) + else: + return IThumbnail(self.thumbnail).get_thumbnail(thumbnail_name, format) + + def delete_thumbnail(self, thumbnail_name): + annotations = IAnnotations(self.video) + if THUMBNAIL_ANNOTATION_KEY in annotations: + del annotations[THUMBNAIL_ANNOTATION_KEY] + + def clear_thumbnails(self): + annotations = IAnnotations(self.video) + if THUMBNAIL_ANNOTATION_KEY in annotations: + del annotations[THUMBNAIL_ANNOTATION_KEY] + self.thumbnail = None + + +@adapter_config(name='thumb', context=IVideo, provides=ITraversable) +class ThumbnailTraverser(ContextAdapter): + """++thumb++ video namespace traverser""" + + def traverse(self, name, furtherpath=None): + if '.' in name: + thumbnail_name, format = name.rsplit('.', 1) + else: + thumbnail_name = name + format = None + thumbnails = IThumbnail(self.context) + result = thumbnails.get_thumbnail(thumbnail_name, format) + transaction.commit() + return result