diff options
Diffstat (limited to 'editty/program.py')
-rw-r--r-- | editty/program.py | 388 |
1 files changed, 388 insertions, 0 deletions
diff --git a/editty/program.py b/editty/program.py new file mode 100644 index 0000000..f73e263 --- /dev/null +++ b/editty/program.py | |||
@@ -0,0 +1,388 @@ | |||
1 | # -*- coding: utf-8 -*- | ||
2 | # Copyright (C) 2019 James E. Blair <corvus@gnu.org> | ||
3 | # | ||
4 | # This program is free software: you can redistribute it and/or modify | ||
5 | # it under the terms of the GNU 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. | ||
8 | # | ||
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 General Public License for more details. | ||
13 | # | ||
14 | # You should have received a copy of the GNU General Public License | ||
15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
16 | |||
17 | import logging | ||
18 | import tempfile | ||
19 | import struct | ||
20 | import uuid | ||
21 | |||
22 | from editty.segment import * | ||
23 | |||
24 | class FrameInfo: | ||
25 | def __init__(self, **kw): | ||
26 | self.__dict__.update(kw) | ||
27 | |||
28 | class Program: | ||
29 | def __init__(self, title='Untitled'): | ||
30 | self.log = logging.getLogger('program') | ||
31 | self.title = title | ||
32 | self.segments = [] | ||
33 | self.length = 0.0 | ||
34 | self.uuid = str(uuid.uuid4()) | ||
35 | |||
36 | def toJSON(self): | ||
37 | return dict(uuid=self.uuid, | ||
38 | title=self.title, | ||
39 | segments=[x.toJSON() for x in self.segments]) | ||
40 | |||
41 | @classmethod | ||
42 | def fromJSON(cls, data, sources): | ||
43 | p = Program(data['title']) | ||
44 | p.uuid = data['uuid'] | ||
45 | for segment in data['segments']: | ||
46 | log = logging.getLogger('program') | ||
47 | log.debug(segment) | ||
48 | p.append(Segment.fromJSON(segment, sources)) | ||
49 | return p | ||
50 | |||
51 | def copy(self): | ||
52 | p = Program() | ||
53 | p.title = self.title | ||
54 | p.segments = [s.copy() for s in self.segments] | ||
55 | p.length = self.length | ||
56 | return p | ||
57 | |||
58 | def cut(self, start, end): | ||
59 | self.log.debug("cut %s %s", start, end) | ||
60 | elapsed = 0.0 | ||
61 | cut_program = Program("Cut of %s" % self.title) | ||
62 | for segment in self.segments[:]: | ||
63 | segment_start = elapsed | ||
64 | segment_end = segment_start + segment.duration | ||
65 | segment_duration = segment.duration | ||
66 | self.log.debug("consider segment %s %s %s", segment, segment_start, segment_end) | ||
67 | # [xxx] | ||
68 | #[===========] | ||
69 | if segment_start < start and segment_end > end: | ||
70 | # The segment should be split and the middle removed | ||
71 | self.log.debug("split and remove segment %s" % segment) | ||
72 | # Save it for the clipboard | ||
73 | clipboard_copy = segment.copy() | ||
74 | clipboard_copy.start += start - segment_start | ||
75 | clipboard_copy.end -= segment_end - end | ||
76 | cut_program.append(clipboard_copy) | ||
77 | # Make the cut | ||
78 | segment_index = self.segments.index(segment) | ||
79 | new_segment = segment.copy() | ||
80 | self.segments.insert(segment_index+1, new_segment) | ||
81 | segment.end -= segment_end - start | ||
82 | new_segment.start += end - segment_start | ||
83 | # No more segments apply | ||
84 | break | ||
85 | # [xxxxxxxxxxx] | ||
86 | #[-----][=====][-----] | ||
87 | # [xxxxx] | ||
88 | #[-----][=====][-----] | ||
89 | elif segment_start >= start and segment_end <= end: | ||
90 | # The entire segment should be removed | ||
91 | self.log.debug("remove segment %s" % segment) | ||
92 | # Save it for the clipboard | ||
93 | clipboard_copy = segment.copy() | ||
94 | cut_program.append(clipboard_copy) | ||
95 | # Make the cut | ||
96 | self.segments.remove(segment) | ||
97 | # [xxxx] | ||
98 | #[-----][=====][-----] | ||
99 | # [xx] | ||
100 | #[-----][=====][-----] | ||
101 | elif segment_start < start and segment_end >= start: | ||
102 | # TODO: if segment_end == start, we may not need this segment in some cases | ||
103 | # Move back the end of this segment | ||
104 | delta = segment_end - start | ||
105 | self.log.debug("move end of segment %s %s", segment, delta) | ||
106 | # Save it for the clipboard | ||
107 | clipboard_copy = segment.copy() | ||
108 | clipboard_copy.start += start - segment_start | ||
109 | cut_program.append(clipboard_copy) | ||
110 | # Make the cut | ||
111 | segment.end -= delta | ||
112 | # [xxxx] | ||
113 | #[-----][=====][-----] | ||
114 | # [xx] | ||
115 | #[-----][=====][-----] | ||
116 | elif segment_end > end and segment_start <= end: | ||
117 | # TODO: if segment_start == end, we may not need this segment in some cases | ||
118 | # Move up the start of this segment | ||
119 | delta = end - segment_start | ||
120 | self.log.debug("move start of segment %s %s", segment, delta) | ||
121 | # Save it for the clipboard | ||
122 | clipboard_copy = segment.copy() | ||
123 | clipboard_copy.end -= segment_end - end | ||
124 | cut_program.append(clipboard_copy) | ||
125 | # Make the cut | ||
126 | segment.start += delta | ||
127 | elapsed += segment_duration | ||
128 | for segment in self.segments: | ||
129 | self.log.debug("segment %s %s %s", segment, segment.start, segment.end) | ||
130 | self.updateLength() | ||
131 | return cut_program | ||
132 | |||
133 | def insert(self, timecode, program): | ||
134 | self.log.debug("insert %s %s", timecode, program) | ||
135 | elapsed = 0.0 | ||
136 | for segment in self.segments[:]: | ||
137 | segment_start = elapsed | ||
138 | segment_end = segment_start + segment.duration | ||
139 | self.log.debug("consider segment %s %s %s", segment, segment_start, segment_end) | ||
140 | # [xx] | ||
141 | #[======] | ||
142 | if timecode > segment_end: | ||
143 | pass | ||
144 | elif timecode < segment_start: | ||
145 | pass | ||
146 | # [xxx] | ||
147 | #[===========] | ||
148 | elif segment_start < timecode < segment_end: | ||
149 | # The segment should be split and the program inserted in the middle | ||
150 | self.log.debug("split and insert program %s" % segment) | ||
151 | segment_index = self.segments.index(segment) | ||
152 | new_segment = segment.copy() | ||
153 | segment.end -= segment_end - timecode | ||
154 | new_segment.start += timecode - segment_start | ||
155 | self.segments = (self.segments[:segment_index+1] + | ||
156 | program.segments + | ||
157 | [new_segment] + | ||
158 | self.segments[segment_index+1:]) | ||
159 | # No more segments apply | ||
160 | break | ||
161 | #[xx] | ||
162 | #[=====] | ||
163 | elif segment_start == timecode: | ||
164 | # The program should be inserted before the current segment | ||
165 | self.log.debug("prepend segment %s" % segment) | ||
166 | segment_index = self.segments.index(segment) | ||
167 | self.segments = (self.segments[:segment_index] + | ||
168 | program.segments + | ||
169 | self.segments[segment_index:]) | ||
170 | # No more segments apply | ||
171 | break | ||
172 | # [xx] | ||
173 | #[=====] | ||
174 | elif segment_end == timecode: | ||
175 | # The program should be appended after the current segment | ||
176 | self.log.debug("append segment %s", segment) | ||
177 | segment_index = self.segments.index(segment) | ||
178 | self.segments = (self.segments[:segment_index+1] + | ||
179 | program.segments + | ||
180 | self.segments[segment_index+1:]) | ||
181 | # No more segments apply | ||
182 | break | ||
183 | elapsed += segment.duration | ||
184 | for segment in self.segments: | ||
185 | self.log.debug("segment %s %s %s", segment, segment.start, segment.end) | ||
186 | self.updateLength() | ||
187 | |||
188 | def append(self, obj): | ||
189 | if isinstance(obj, Segment): | ||
190 | self.segments.append(obj) | ||
191 | elif isinstance(obj, Program): | ||
192 | self.segments.extend(obj.segments) | ||
193 | else: | ||
194 | raise Exception("Can not add %s to Program" % repr(obj)) | ||
195 | self.updateLength() | ||
196 | |||
197 | def dissolve(self, start, end): | ||
198 | self.cut(start, end) | ||
199 | (prev_fi, cur_fi, next_fi) = self.getFramesAtTimecode(start) | ||
200 | if (not isinstance(prev_fi.segment, Clip) or | ||
201 | not isinstance(cur_fi.segment, Clip)): | ||
202 | raise Exception("Dissolve is only supported between two clips") | ||
203 | dis = Dissolve(prev_fi.segment.source, | ||
204 | prev_fi.segment.end, | ||
205 | cur_fi.segment.source, | ||
206 | cur_fi.segment.start, | ||
207 | end-start) | ||
208 | prog = Program() | ||
209 | prog.append(dis) | ||
210 | self.insert(start, prog) | ||
211 | |||
212 | def updateLength(self): | ||
213 | self.length = 0.0 | ||
214 | for segment in self.segments: | ||
215 | self.length += segment.duration | ||
216 | |||
217 | def getFramesAtTimecode(self, timecode): | ||
218 | # Return the frame before, at, and after the timecode | ||
219 | prev_frame = None | ||
220 | cur_frame = None | ||
221 | next_frame = None | ||
222 | for fi in self: | ||
223 | if fi.timecode > timecode: | ||
224 | next_frame = fi | ||
225 | return (prev_frame, cur_frame, next_frame) | ||
226 | prev_frame = cur_frame | ||
227 | cur_frame = fi | ||
228 | return (prev_frame, cur_frame, next_frame) | ||
229 | |||
230 | def __iter__(self): | ||
231 | previous_segment_duration = 0.0 | ||
232 | for si, segment in enumerate(self.segments): | ||
233 | for (fi, (timecode, frame)) in enumerate(segment): | ||
234 | yield FrameInfo(timecode=timecode + previous_segment_duration, | ||
235 | frame_index=fi, | ||
236 | segment_index=si, | ||
237 | segment=segment, | ||
238 | frame=frame) | ||
239 | previous_segment_duration += segment.duration | ||
240 | |||
241 | def getFrames(self, start, end): | ||
242 | for fi in self: | ||
243 | if end is not None and fi.timecode > end: | ||
244 | return | ||
245 | if start is not None and fi.timecode < start: | ||
246 | continue | ||
247 | start = None | ||
248 | yield fi | ||
249 | |||
250 | # TODO: this is currently unused | ||
251 | def render_script(self, size, stream_fn, timing_fn): | ||
252 | class LoadWidget(urwid.Widget): | ||
253 | term_modes = urwid.TermModes() | ||
254 | def beep(self): pass | ||
255 | def set_title(self, title): pass | ||
256 | class MyScreen(urwid.raw_display.Screen): | ||
257 | def signal_init(self): pass | ||
258 | def signal_restore(self): pass | ||
259 | canv = urwid.TermCanvas(size[0], size[1], LoadWidget()) | ||
260 | canv.modes.main_charset = urwid.vterm.CHARSET_UTF8 | ||
261 | with tempfile.TemporaryFile() as screen_in: | ||
262 | with open(stream_fn, 'w') as screen_out: | ||
263 | with open(timing_fn, 'w') as timing_out: | ||
264 | screen_out.write("Rendered by Editty\n") | ||
265 | screen = MyScreen(screen_in, screen_out) | ||
266 | screen.start() | ||
267 | elapsed = 0.0 | ||
268 | written = 0 | ||
269 | for fi in self: | ||
270 | canv.term = [line[:] for line in fi.frame.content] | ||
271 | canv.set_term_cursor(fi.frame.cursor[0], fi.frame.cursor[1]) | ||
272 | screen.draw_screen(size, canv) | ||
273 | screen._screen_buf_canvas=None | ||
274 | screen_out.flush() | ||
275 | current_pos = screen_out.tell() | ||
276 | delta_bytes = current_pos - written | ||
277 | delta_time = fi.timecode - elapsed | ||
278 | timing_out.write('%0.6f %i\n' % (delta_time, delta_bytes)) | ||
279 | written = current_pos | ||
280 | elapsed = fi.timecode | ||
281 | screen.stop() | ||
282 | screen_out.write("\n\nRendered by Editty\n") | ||
283 | |||
284 | # TODO: this is currently unused | ||
285 | def render_asciicast(self, size, cast_fn): | ||
286 | class LoadWidget(urwid.Widget): | ||
287 | term_modes = urwid.TermModes() | ||
288 | def beep(self): pass | ||
289 | def set_title(self, title): pass | ||
290 | class MyScreen(urwid.raw_display.Screen): | ||
291 | def signal_init(self): pass | ||
292 | def signal_restore(self): pass | ||
293 | canv = urwid.TermCanvas(size[0], size[1], LoadWidget()) | ||
294 | canv.modes.main_charset = urwid.vterm.CHARSET_UTF8 | ||
295 | show_cursor_escape = urwid.escape.SHOW_CURSOR | ||
296 | outdata = dict(version=1, | ||
297 | duration=self.length, | ||
298 | title="", | ||
299 | height=size[1], | ||
300 | width=size[0], | ||
301 | command=None, | ||
302 | stdout=[]) | ||
303 | stdout = outdata['stdout'] | ||
304 | with tempfile.TemporaryFile() as screen_in: | ||
305 | with tempfile.TemporaryFile('w+') as screen_out: | ||
306 | with open(cast_fn, 'w') as cast_out: | ||
307 | screen = MyScreen(screen_in, screen_out) | ||
308 | screen.start() | ||
309 | elapsed = 0.0 | ||
310 | for fi in self: | ||
311 | canv.term = [line[:] for line in fi.frame.content] | ||
312 | canv.set_term_cursor(fi.frame.cursor[0], fi.frame.cursor[1]) | ||
313 | if fi.segment.visible_cursor: | ||
314 | urwid.escape.SHOW_CURSOR = show_cursor_escape | ||
315 | else: | ||
316 | urwid.escape.SHOW_CURSOR = '' | ||
317 | screen.draw_screen(size, canv) | ||
318 | screen._screen_buf_canvas=None | ||
319 | screen_out.flush() | ||
320 | delta_bytes = screen_out.tell() | ||
321 | screen_out.seek(0) | ||
322 | data = screen_out.read() | ||
323 | self.log.debug("read %s chars %s bytes of %s" % (len(data), len(data.encode('utf8')), delta_bytes)) | ||
324 | screen_out.seek(0) | ||
325 | screen_out.truncate() | ||
326 | if len(data.encode('utf8')) != delta_bytes: | ||
327 | raise Exception("Short read") | ||
328 | delta_time = fi.timecode - elapsed | ||
329 | stdout.append([delta_time, data]) | ||
330 | self.log.debug("frame %s %s", delta_time, len(data)) | ||
331 | elapsed = fi.timecode | ||
332 | cast_out.write(json.dumps(outdata)) | ||
333 | screen.stop() | ||
334 | urwid.escape.SHOW_CURSOR = show_cursor_escape | ||
335 | |||
336 | def render_ttyrec(self, size, ttyrec_fn): | ||
337 | class LoadWidget(urwid.Widget): | ||
338 | term_modes = urwid.TermModes() | ||
339 | def beep(self): pass | ||
340 | def set_title(self, title): pass | ||
341 | class MyScreen(urwid.raw_display.Screen): | ||
342 | def signal_init(self): pass | ||
343 | def signal_restore(self): pass | ||
344 | canv = urwid.TermCanvas(size[0], size[1], LoadWidget()) | ||
345 | canv.modes.main_charset = urwid.vterm.CHARSET_UTF8 | ||
346 | show_cursor_escape = urwid.escape.SHOW_CURSOR | ||
347 | with tempfile.TemporaryFile() as screen_in: | ||
348 | with tempfile.TemporaryFile('w+') as screen_out: | ||
349 | with open(ttyrec_fn, 'wb') as ttyrec_out: | ||
350 | screen = MyScreen(screen_in, screen_out) | ||
351 | screen.start() | ||
352 | first = True | ||
353 | for fi in self: | ||
354 | canv.term = [line[:] for line in fi.frame.content] | ||
355 | canv.set_term_cursor(fi.frame.cursor[0], fi.frame.cursor[1]) | ||
356 | if fi.segment.visible_cursor: | ||
357 | urwid.escape.SHOW_CURSOR = show_cursor_escape | ||
358 | else: | ||
359 | urwid.escape.SHOW_CURSOR = '' | ||
360 | screen.draw_screen(size, canv) | ||
361 | screen._screen_buf_canvas=None | ||
362 | screen_out.flush() | ||
363 | delta_bytes = screen_out.tell() | ||
364 | screen_out.seek(0) | ||
365 | data = screen_out.read() | ||
366 | self.log.debug("read %s chars %s bytes of %s" % (len(data), len(data.encode('utf8')), delta_bytes)) | ||
367 | screen_out.seek(0) | ||
368 | screen_out.truncate() | ||
369 | if len(data.encode('utf8')) != delta_bytes: | ||
370 | raise Exception("Short read") | ||
371 | if first: | ||
372 | prefix = '\x1b%%G\x1b[8;%s;%st' % (size[1], size[0]) | ||
373 | data = prefix + data | ||
374 | first = False | ||
375 | tc_secs, tc_usecs = map(int, ('%0.6f' % fi.timecode).split('.')) | ||
376 | data = data.encode('utf8') | ||
377 | ttyrec_out.write(struct.pack('<III', | ||
378 | tc_secs, tc_usecs, len(data))) | ||
379 | ttyrec_out.write(data) | ||
380 | self.log.debug("frame %s %s %s", tc_secs, tc_usecs, len(data)) | ||
381 | elapsed = fi.timecode | ||
382 | if elapsed < self.length: | ||
383 | data = '\x1b[1;1H'.encode('utf8') | ||
384 | tc_secs, tc_usecs = map(int, ('%0.6f' % self.length).split('.')) | ||
385 | ttyrec_out.write(struct.pack('<III', | ||
386 | tc_secs, tc_usecs, 0)) | ||
387 | screen.stop() | ||
388 | urwid.escape.SHOW_CURSOR = show_cursor_escape | ||