|
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 # import standard library |
|
14 |
|
15 # import interfaces |
|
16 |
|
17 # import packages |
|
18 from pyams_media.ffbase import FFVideoEffect, FFAudioEffect, FFmpeg |
|
19 from pyams_media.interfaces import IMediaInfo |
|
20 |
|
21 |
|
22 class FFDocument(FFVideoEffect, FFAudioEffect): |
|
23 """ |
|
24 audio/video document. A FFDocument describe a higer level action set |
|
25 combining several FF[Audio|Video]Effect methods. |
|
26 """ |
|
27 |
|
28 def __init__(self, file, metadata=None, effects={}): |
|
29 """ |
|
30 x.__init__(...) initializes x; see x.__class__.__doc__ for signature |
|
31 """ |
|
32 FFAudioEffect.__init__(self, file) |
|
33 FFVideoEffect.__init__(self, file, **effects) |
|
34 if not metadata: |
|
35 info = IMediaInfo(file, None) |
|
36 if info is not None: |
|
37 self.__metadata__ = info |
|
38 else: |
|
39 info = FFmpeg('ffprobe').info(file) |
|
40 if info: |
|
41 self.__metadata__ = info |
|
42 else: |
|
43 self.__metadata__ = [] |
|
44 else: |
|
45 self.__metadata__ = metadata |
|
46 |
|
47 def get_stream_info(self, codec_type=None): |
|
48 """Get metadata info for given stream""" |
|
49 for stream in self.__metadata__: |
|
50 if (not codec_type) or (stream.get('codec_type') == codec_type): |
|
51 return stream |
|
52 |
|
53 def __tlen__(self): |
|
54 """ |
|
55 return time length |
|
56 """ |
|
57 stream = self.get_stream_info() |
|
58 if stream is not None: |
|
59 t = self.__timeparse__(float(stream["duration"])) |
|
60 if self.seek(): |
|
61 t = t - self.seek() |
|
62 if self.duration(): |
|
63 t = t - (t - self.duration()) |
|
64 return t |
|
65 |
|
66 def __timereference__(self, reference, time): |
|
67 if isinstance(time, str): |
|
68 if '%' in time: |
|
69 parsed = (reference / 100.0) * int(time.split("%")[0]) |
|
70 else: |
|
71 elts = time.split(':') |
|
72 if len(elts) == 3: |
|
73 hhn, mmn, ssn = [float(i) for i in elts] |
|
74 parsed = hhn * 3600 + mmn * 60 + ssn |
|
75 elif len(elts) == 2: |
|
76 hhn, mmn = [float(i) for i in elts] |
|
77 ssn = 0 |
|
78 parsed = hhn * 3600 + mmn * 60 + ssn |
|
79 else: |
|
80 parsed = 0 |
|
81 else: |
|
82 parsed = time |
|
83 return parsed |
|
84 |
|
85 def __timeparse__(self, time): |
|
86 if isinstance(time, str): |
|
87 if ':' in time: |
|
88 hh, mm, ss = [float(i) for i in time.split(":")] |
|
89 return hh * 3600 + mm * 60 + ss |
|
90 elif isinstance(time, float): |
|
91 return time |
|
92 |
|
93 def __clone__(self): |
|
94 return FFDocument(self.__file__, self.__metadata__.copy(), self.__effects__.copy()) |
|
95 |
|
96 def resample(self, width=0, height=0, vstream=0): |
|
97 """Adjust video dimensions. If one dimension is specified, the re-sampling is proportional |
|
98 """ |
|
99 stream = self.get_stream_info('video') |
|
100 if stream is not None: |
|
101 w, h = stream['width'], stream['height'] |
|
102 if not width: |
|
103 width = int(w * (float(height) / h)) |
|
104 elif not height: |
|
105 height = int(h * (float(width) / w)) |
|
106 elif not width and height: |
|
107 return |
|
108 |
|
109 new = self.__clone__() |
|
110 if width < w: |
|
111 cropsize = (w - width) / 2 |
|
112 new.crop(0, 0, cropsize, cropsize) |
|
113 elif width > w: |
|
114 padsize = (width - w) / 2 |
|
115 new.pad(0, 0, padsize, padsize) |
|
116 if height < h: |
|
117 cropsize = (h - height) / 2 |
|
118 new.crop(cropsize, cropsize, 0, 0) |
|
119 elif height > h: |
|
120 padsize = (height - h) / 2 |
|
121 new.pad(padsize, padsize, 0, 0) |
|
122 return new |
|
123 |
|
124 def resize(self, width=0, height=0, vstream=0): |
|
125 """Resize video dimensions. If one dimension is specified, the re-sampling is proportional |
|
126 |
|
127 Width and height can be pixel or % (not mixable) |
|
128 """ |
|
129 stream = self.get_stream_info('video') |
|
130 if stream is not None: |
|
131 w, h = stream['width'], stream['height'] |
|
132 if type(width) == str or type(height) == str: |
|
133 if not width: |
|
134 width = height = int(height.split("%")[0]) |
|
135 elif not height: |
|
136 height = width = int(width.split("%")[0]) |
|
137 elif not width and height: |
|
138 return |
|
139 elif width and height: |
|
140 width = int(width.split("%")[0]) |
|
141 height = int(height.split("%")[0]) |
|
142 size = "%sx%s" % (int(w / 100.0 * width), int(h / 100.0 * height)) |
|
143 else: |
|
144 if not width: |
|
145 width = int(w * (float(height) / h)) |
|
146 elif not height: |
|
147 height = int(h * (float(width) / w)) |
|
148 elif not width and height: |
|
149 return |
|
150 size = "%sx%s" % (width, height) |
|
151 new = self.__clone__() |
|
152 new.size(size) |
|
153 return new |
|
154 |
|
155 def split(self, time): |
|
156 """Return a tuple of FFDocument splitted at a specified time. |
|
157 |
|
158 Allowed formats: %, sec, hh:mm:ss.mmm |
|
159 """ |
|
160 stream = self.get_stream_info() |
|
161 if stream is not None: |
|
162 sectime = self.__timeparse__(stream["duration"]) |
|
163 if self.duration(): |
|
164 sectime = sectime - (sectime - self.duration()) |
|
165 if self.seek(): |
|
166 sectime = sectime - self.seek() |
|
167 cut = self.__timereference__(sectime, time) |
|
168 |
|
169 first = self.__clone__() |
|
170 second = self.__clone__() |
|
171 first.duration(cut) |
|
172 second.seek(cut + 0.001) |
|
173 return first, second |
|
174 |
|
175 def ltrim(self, time): |
|
176 """Trim leftmost side (from start) of the clip""" |
|
177 stream = self.get_stream_info() |
|
178 if stream is not None: |
|
179 sectime = self.__timeparse__(stream["duration"]) |
|
180 if self.duration(): |
|
181 sectime = sectime - (sectime - self.duration()) |
|
182 if self.seek(): |
|
183 sectime = sectime - self.seek() |
|
184 trim = self.__timereference__(sectime, time) |
|
185 new = self.__clone__() |
|
186 if self.seek(): |
|
187 new.seek(self.seek() + trim) |
|
188 else: |
|
189 new.seek(trim) |
|
190 return new |
|
191 |
|
192 def rtrim(self, time): |
|
193 """Trim rightmost side (from end) of the clip""" |
|
194 stream = self.get_stream_info() |
|
195 if stream is not None: |
|
196 sectime = self.__timeparse__(self.__metadata__["duration"]) |
|
197 if self.duration(): |
|
198 sectime = sectime - (sectime - self.duration()) |
|
199 if self.seek(): |
|
200 sectime = sectime - self.seek() |
|
201 trim = self.__timereference__(sectime, time) |
|
202 new = self.__clone__() |
|
203 new.duration(trim) |
|
204 return new |
|
205 |
|
206 def trim(self, left, right): |
|
207 """Left and right trim (actually calls ltrim and rtrim)""" |
|
208 return self.__clone__().ltrim(left).rtrim(right) |
|
209 |
|
210 def chainto(self, ffdoc): |
|
211 """Prepare to append at the end of another movie clip""" |
|
212 offset = 0 |
|
213 if ffdoc.seek(): |
|
214 offset = ffdoc.seek() |
|
215 if ffdoc.duration(): |
|
216 offset = offset + ffdoc.seek() |
|
217 if ffdoc.offset(): |
|
218 offset = offset + ffdoc.offset() |
|
219 |
|
220 new = self.__clone__() |
|
221 new.offset(offset) |
|
222 return new |
|
223 |
|
224 #TODO: more and more effects!!! |