|
1 # |
|
2 # Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net> |
|
3 # All Rights Reserved. |
|
4 # |
|
5 # This software is subject to the provisions of the Zope Public License, |
|
6 # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. |
|
7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED |
|
8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS |
|
10 # FOR A PARTICULAR PURPOSE. |
|
11 # |
|
12 |
|
13 __docformat__ = 'restructuredtext' |
|
14 |
|
15 |
|
16 # import standard library |
|
17 import os.path |
|
18 import subprocess |
|
19 |
|
20 from tempfile import NamedTemporaryFile |
|
21 |
|
22 # import interfaces |
|
23 from pyams_file.interfaces import IVideo, IThumbnail |
|
24 from zope.annotation.interfaces import IAnnotations |
|
25 from zope.traversing.interfaces import ITraversable |
|
26 |
|
27 # import packages |
|
28 import transaction |
|
29 |
|
30 from pyams_file.file import ImageFile, get_magic_content_type |
|
31 from pyams_file.image import ThumbnailGeometrry |
|
32 from pyams_media.ffbase import FFmpeg |
|
33 from pyams_utils.adapter import adapter_config, ContextAdapter |
|
34 from pyramid.threadlocal import get_current_registry |
|
35 from zope.lifecycleevent import ObjectCreatedEvent, ObjectAddedEvent |
|
36 from zope.location import locate |
|
37 |
|
38 |
|
39 THUMBNAIL_ANNOTATION_KEY = 'pyams_media.video.thumbnail' |
|
40 |
|
41 |
|
42 @adapter_config(context=IVideo, provides=IThumbnail) |
|
43 class VideoThumbnailAdapter(object): |
|
44 """Video thumbnail adapter""" |
|
45 |
|
46 def __init__(self, video): |
|
47 self.video = video |
|
48 annotations = IAnnotations(video) |
|
49 self.thumbnail = annotations.get(THUMBNAIL_ANNOTATION_KEY) |
|
50 |
|
51 def get_image_size(self): |
|
52 if self.thumbnail is not None: |
|
53 return self.thumbnail.get_image_size() |
|
54 else: |
|
55 mpeg = FFmpeg('ffprobe') |
|
56 streams = mpeg.info(self.video) |
|
57 if streams: |
|
58 for stream in streams: |
|
59 if stream.get('codec_type') != 'video': |
|
60 continue |
|
61 return stream.get('width'), stream.get('height') |
|
62 |
|
63 def get_thumbnail_size(self, thumbnail_name, forced=False): |
|
64 if self.thumbnail is not None: |
|
65 return IThumbnail(self.thumbnail).get_thumbnail_size(thumbnail_name, forced) |
|
66 else: |
|
67 return self.get_image_size() |
|
68 |
|
69 def get_thumbnail_geometry(self, thumbnail_name): |
|
70 if self.thumbnail is not None: |
|
71 return IThumbnail(self.thumbnail).get_thumbnail_geometry(thumbnail_name) |
|
72 else: |
|
73 size = self.get_image_size() |
|
74 if size: |
|
75 geometry = ThumbnailGeometrry() |
|
76 geometry.x1 = 0 |
|
77 geometry.y1 = 0 |
|
78 geometry.x2 = size[0] |
|
79 geometry.y2 = size[1] |
|
80 return geometry |
|
81 |
|
82 def set_thumbnail_geometry(self, thumbnail_name, geometry): |
|
83 if self.thumbnail is not None: |
|
84 IThumbnail(self.thumbnail).set_thumbnail_geometry(thumbnail_name, geometry) |
|
85 |
|
86 def clear_geometries(self): |
|
87 if self.thumbnail is not None: |
|
88 IThumbnail(self.thumbnail).clear_geometries() |
|
89 |
|
90 def get_thumbnail_name(self, thumbnail_name, with_size=False): |
|
91 if self.thumbnail is not None: |
|
92 return IThumbnail(self.thumbnail).get_thumbnail_name(thumbnail_name, with_size) |
|
93 else: |
|
94 size = self.get_image_size() |
|
95 if size is not None: |
|
96 if with_size: |
|
97 return '{0}x{1}'.format(*size), size |
|
98 else: |
|
99 return '{0}x{1}'.format(*size) |
|
100 else: |
|
101 return None, None |
|
102 |
|
103 def get_thumbnail(self, thumbnail_name, format=None, time=5): |
|
104 if self.thumbnail is None: |
|
105 pipe = subprocess.Popen(('ffmpeg', '-i', '-', '-ss', str(time), '-f', 'image2', '-vframes', '1', '-'), |
|
106 stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
|
107 if pipe: |
|
108 stdout, stderr = pipe.communicate(self.video.data) |
|
109 # Some videos formats can't be converted via pipes |
|
110 # If so, we must provide a temporay file... |
|
111 if not stdout: |
|
112 output = NamedTemporaryFile(prefix='video_', suffix='.thumb') |
|
113 output.write(self.video.data) |
|
114 output.file.flush() |
|
115 pipe = subprocess.Popen(('ffmpeg', '-i', output.name, '-ss', str(time), '-f', 'image2', |
|
116 '-vframes', '1', '-'), |
|
117 stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
|
118 if pipe: |
|
119 stdout, stderr = pipe.communicate() |
|
120 # Create final image |
|
121 registry = get_current_registry() |
|
122 annotations = IAnnotations(self.video) |
|
123 image = ImageFile(stdout) |
|
124 image.content_type = get_magic_content_type(image.data) |
|
125 registry.notify(ObjectCreatedEvent(image)) |
|
126 self.thumbnail = annotations[THUMBNAIL_ANNOTATION_KEY] = image |
|
127 locate(self.thumbnail, self.video) |
|
128 registry.notify(ObjectAddedEvent(image, self.video)) |
|
129 if self.thumbnail is not None: |
|
130 size_name = '{0[0]}x{0[1]}'.format(self.get_image_size()) |
|
131 if thumbnail_name != size_name: |
|
132 watermark = os.path.abspath(os.path.join(__file__, '..', |
|
133 'skin', 'resources', 'img', 'video-play-mask.png')) |
|
134 return IThumbnail(self.thumbnail).get_thumbnail(thumbnail_name, format, watermark) |
|
135 else: |
|
136 return IThumbnail(self.thumbnail).get_thumbnail(thumbnail_name, format) |
|
137 |
|
138 def delete_thumbnail(self, thumbnail_name): |
|
139 annotations = IAnnotations(self.video) |
|
140 if THUMBNAIL_ANNOTATION_KEY in annotations: |
|
141 del annotations[THUMBNAIL_ANNOTATION_KEY] |
|
142 |
|
143 def clear_thumbnails(self): |
|
144 annotations = IAnnotations(self.video) |
|
145 if THUMBNAIL_ANNOTATION_KEY in annotations: |
|
146 del annotations[THUMBNAIL_ANNOTATION_KEY] |
|
147 self.thumbnail = None |
|
148 |
|
149 |
|
150 @adapter_config(name='thumb', context=IVideo, provides=ITraversable) |
|
151 class ThumbnailTraverser(ContextAdapter): |
|
152 """++thumb++ video namespace traverser""" |
|
153 |
|
154 def traverse(self, name, furtherpath=None): |
|
155 if '.' in name: |
|
156 thumbnail_name, format = name.rsplit('.', 1) |
|
157 else: |
|
158 thumbnail_name = name |
|
159 format = None |
|
160 thumbnails = IThumbnail(self.context) |
|
161 result = thumbnails.get_thumbnail(thumbnail_name, format) |
|
162 transaction.commit() |
|
163 return result |