summaryrefslogtreecommitdiff
path: root/editty/program.py
diff options
context:
space:
mode:
Diffstat (limited to 'editty/program.py')
-rw-r--r--editty/program.py388
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
17import logging
18import tempfile
19import struct
20import uuid
21
22from editty.segment import *
23
24class FrameInfo:
25 def __init__(self, **kw):
26 self.__dict__.update(kw)
27
28class 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