a | b/trunk/tubetutor/tubetutor.py | ||
---|---|---|---|
1 | import os |
||
2 | import tkinter |
||
3 | import ffmpeg |
||
4 | import atexit |
||
5 | import keyboard |
||
6 | import shutil |
||
7 | import glob |
||
8 | |||
9 | path="videos" |
||
10 | #resolution="3840:2160" |
||
11 | resolution="1920:1080" |
||
12 | |||
13 | def id(filename): |
||
14 | id = os.path.basename(filename).split(".")[0] |
||
15 | return id |
||
16 | |||
17 | def text_changed(event): |
||
18 | global path |
||
19 | filename=str(event.widget).split(".")[-1] + ".txt" |
||
20 | content=event.widget.get("1.0", tkinter.END) |
||
21 | f = open(os.path.join(path, "working", filename), "w") |
||
22 | if f: |
||
23 | f.write(content) |
||
24 | f.close() |
||
25 | |||
26 | class TubeTutorWin: |
||
27 | files=[] |
||
28 | frames=[] |
||
29 | buttons={} |
||
30 | labels={} |
||
31 | images={} |
||
32 | canvases={} |
||
33 | textfields={} |
||
34 | lasttextfield="" |
||
35 | master=None |
||
36 | frame=None |
||
37 | lastid=0 |
||
38 | keyboardid=None |
||
39 | hotkey_stop = False |
||
40 | |||
41 | def __init__(self, master, path): |
||
42 | self.master = master |
||
43 | self.path = path |
||
44 | master.title("TubeTutor - Create easy video tutorials") |
||
45 | self.refresh_files() |
||
46 | self.refresh_display() |
||
47 | self.textfields[self.lasttextfield].focus_set() |
||
48 | self.master.after(1000, self.poll) |
||
49 | # keyboard.add_hotkey('ctrl+<', self.hotkey) |
||
50 | keyboard.add_hotkey('ctrl+alt+r', self.hotkey) |
||
51 | |||
52 | def poll(self): |
||
53 | # change color of record button, if ffmpeg is running |
||
54 | try: |
||
55 | if not self.orig_buttoncolor: |
||
56 | self.orig_buttoncolor = self.buttons["Record"].cget("background") |
||
57 | except: |
||
58 | pass |
||
59 | |||
60 | if ffmpeg.is_recording(): |
||
61 | try: |
||
62 | self.buttons["Record"].configure(bg="red") |
||
63 | except: |
||
64 | pass |
||
65 | else: |
||
66 | try: |
||
67 | self.buttons["Record"].configure(bg=self.orig_buttoncolor) |
||
68 | except: |
||
69 | pass |
||
70 | # process record hotkey |
||
71 | if self.hotkey_stop: |
||
72 | print("hotkey polled") |
||
73 | self.hotkey_stop = False |
||
74 | self.refresh_files() |
||
75 | self.refresh_display() |
||
76 | self.textfields[self.lasttextfield].focus_force() |
||
77 | self.master.after(1000, self.poll) |
||
78 | |||
79 | def hotkey(self): |
||
80 | global resolution |
||
81 | print("Hotkey pressed") |
||
82 | if ffmpeg.is_recording(): |
||
83 | ffmpeg.stop() |
||
84 | self.hotkey_stop = True |
||
85 | else: |
||
86 | ffmpeg.record(self.get_filename()) |
||
87 | |||
88 | def get_filename(self, extension="mkv"): |
||
89 | global path |
||
90 | self.lastid += 10 |
||
91 | basename = "%04d.%s" % (self.lastid, extension) |
||
92 | return os.path.join(path, "working", basename) |
||
93 | |||
94 | def set_filename(self, filename): |
||
95 | base = filename.split(".")[0] |
||
96 | self.lastid = int(base) |
||
97 | |||
98 | def get_title(self): |
||
99 | title=self.textfields["title"].get("1.0", tkinter.END) |
||
100 | return title |
||
101 | |||
102 | |||
103 | def refresh_files(self): |
||
104 | self.files = [] |
||
105 | for root, dirs, files in os.walk(os.path.join(self.path, "working")): |
||
106 | for file in files: |
||
107 | if file.endswith(".mkv") or file.endswith(".txt"): |
||
108 | self.files.append(os.path.join(root, file)) |
||
109 | |||
110 | # Output the main window |
||
111 | def refresh_display(self): |
||
112 | # remove all widgets |
||
113 | for w in self.buttons: |
||
114 | self.buttons[w].pack_forget() |
||
115 | self.buttons = {} |
||
116 | |||
117 | for w in self.frames: |
||
118 | w.pack_forget() |
||
119 | self.frames = [] |
||
120 | |||
121 | for w in self.labels: |
||
122 | self.labels[w].pack_forget() |
||
123 | self.labels = {} |
||
124 | |||
125 | for w in self.textfields: |
||
126 | self.textfields[w].pack_forget() |
||
127 | self.textfields = {} |
||
128 | |||
129 | for w in self.canvases: |
||
130 | self.canvases[w].pack_forget() |
||
131 | self.images = {} |
||
132 | self.canvases = {} |
||
133 | |||
134 | self.lastid=0 |
||
135 | |||
136 | |||
137 | # re-add all widgets |
||
138 | self.add_label("description", text="Press <Ctrl+Alt+R> to start or end recording.") |
||
139 | |||
140 | # Title of the video |
||
141 | self.start_frame() |
||
142 | self.add_label("title", "Title:", side=tkinter.LEFT) |
||
143 | self.add_text("title", "Video Tutorial", width=30, height=1, autosave=False, side=tkinter.LEFT) |
||
144 | self.add_button("Render", self.render, side=tkinter.LEFT) |
||
145 | self.add_button("Stash", self.stash, side=tkinter.LEFT) |
||
146 | self.end_frame() |
||
147 | |||
148 | # Recording control panel |
||
149 | self.start_frame() |
||
150 | self.add_button("Record", self.record, side=tkinter.LEFT) |
||
151 | self.add_button("Stop", self.stop, side=tkinter.LEFT) |
||
152 | self.end_frame() |
||
153 | |||
154 | file = self.get_filename("txt") |
||
155 | content="" |
||
156 | try: |
||
157 | f = open(file, "r") |
||
158 | if f: |
||
159 | content = f.read() |
||
160 | f.close() |
||
161 | except: |
||
162 | pass |
||
163 | self.add_text(id(file), content); |
||
164 | for file in self.files: |
||
165 | if os.path.basename(file).startswith("tmp_"): |
||
166 | continue |
||
167 | if file.endswith(".mkv"): |
||
168 | self.start_frame() |
||
169 | moviefile=file |
||
170 | self.add_label(id(file) + "_image", image=os.path.join("media", "movie.png"), side=tkinter.LEFT) |
||
171 | self.add_label(id(file), text=file, side=tkinter.LEFT) |
||
172 | self.add_button("del", lambda: self.delete(moviefile), side=tkinter.LEFT) |
||
173 | self.end_frame() |
||
174 | self.set_filename(os.path.basename(file)) |
||
175 | |||
176 | file = self.get_filename("txt") |
||
177 | content="" |
||
178 | try: |
||
179 | f = open(file, "r") |
||
180 | if f: |
||
181 | content = f.read() |
||
182 | f.close() |
||
183 | except: |
||
184 | pass |
||
185 | self.add_text(id(file), content); |
||
186 | |||
187 | |||
188 | def add_image(self, name, filename, width=32, height=32, side=tkinter.TOP, fill=tkinter.NONE, expand=False): |
||
189 | self.images[name] = tkinter.PhotoImage(master=self.root(), file=filename) |
||
190 | |||
191 | self.canvases[name] = tkinter.Canvas(self.root(), width=width, height=height) |
||
192 | self.canvases[name].create_image(0, 0, image=self.images[name]) |
||
193 | self.canvases[name].pack(side=side, fill=fill, expand=expand) |
||
194 | |||
195 | def add_label(self, name, text="", image="", width=120, side=tkinter.TOP, fill=tkinter.NONE, expand=False): |
||
196 | if image != "": |
||
197 | self.images[name] = tkinter.PhotoImage(master=self.root(), file=image) |
||
198 | self.labels[name] = tkinter.Label(self.root(), width=width, text=text, image=self.images[name]) |
||
199 | else: |
||
200 | self.labels[name] = tkinter.Label(self.root(), text=text) |
||
201 | self.labels[name].pack(side=side, fill=fill, expand=expand) |
||
202 | |||
203 | def add_text(self, name, content="", autosave=True, width=50, height=2, side=tkinter.TOP, fill=tkinter.NONE, expand=False): |
||
204 | self.textfields[name] = tkinter.Text(self.root(), name=name, width=width, height=height) |
||
205 | if content != "": |
||
206 | self.textfields[name].insert(tkinter.INSERT, content) |
||
207 | self.textfields[name].pack(side=side, fill=fill, expand=expand) |
||
208 | if autosave: |
||
209 | self.textfields[name].bind("<KeyRelease>", text_changed) |
||
210 | self.lasttextfield = name |
||
211 | |||
212 | def add_button(self, name, fn, side=tkinter.TOP, fill=tkinter.NONE, expand=False): |
||
213 | self.buttons[name] = tkinter.Button(self.root(), text=name, command=fn) |
||
214 | self.buttons[name].pack(side=side, fill=fill, expand=expand) |
||
215 | |||
216 | def start_frame(self, side=tkinter.TOP, fill=tkinter.NONE, expand=False): |
||
217 | self.frame = tkinter.Frame(self.master) |
||
218 | self.frame.pack(side=side, fill=fill, expand=expand) |
||
219 | self.frames.append(self.frame) |
||
220 | |||
221 | def end_frame(self): |
||
222 | self.frame = None |
||
223 | |||
224 | def root(self): |
||
225 | if self.frame: |
||
226 | return self.frame |
||
227 | else: |
||
228 | return self.master |
||
229 | |||
230 | def record(self): |
||
231 | ffmpeg.record(self.get_filename()) |
||
232 | |||
233 | def stop(self): |
||
234 | ffmpeg.stop() |
||
235 | self.refresh_files() |
||
236 | self.refresh_display() |
||
237 | |||
238 | def render(self): |
||
239 | global resolution |
||
240 | self.refresh_files() |
||
241 | videos=[] |
||
242 | description="" |
||
243 | |||
244 | # intro |
||
245 | r = ffmpeg.renderer() |
||
246 | r.resolution(resolution) |
||
247 | r.filename(os.path.join("media", "intro.mkv")) |
||
248 | params = " -scale %s!" % resolution.replace(":", "x") |
||
249 | params += " -font %s -weight 500 -pointsize 100" % ffmpeg.font |
||
250 | params += " -draw \"gravity northwest fill white text 100,775 '%s'\"" % self.get_title() |
||
251 | params += " -scale %s!" % resolution.replace(":", "x") |
||
252 | ffmpeg.convert_image(os.path.join("media", "intro.jpg"), os.path.join(path, "tmp", "intro.jpg"), params) |
||
253 | r.add_image(os.path.join(path, "tmp", "intro.jpg"), fadein=False) |
||
254 | r.process(os.path.join(path, "tmp", "intro-scale.mkv"), "-vf scale=" + resolution) |
||
255 | r.concat(os.path.join(path, "tmp", "intro-concat.mkv")) |
||
256 | |||
257 | # mux intro |
||
258 | r = ffmpeg.renderer() |
||
259 | r.resolution(resolution) |
||
260 | r.filename(os.path.join(path, "tmp", "intro-concat.mkv")) |
||
261 | r.mux(os.path.join(path, "tmp", "intro.mkv"), os.path.join("media", "intro.mp3")) |
||
262 | videos.append(r.lastfilename) |
||
263 | |||
264 | # main |
||
265 | r = ffmpeg.renderer() |
||
266 | r.resolution(resolution) |
||
267 | for file in self.files: |
||
268 | if os.path.basename(file).startswith("tmp_"): |
||
269 | continue |
||
270 | if file.endswith(".txt"): |
||
271 | description += "- %s" % r.add_text(file) |
||
272 | if file.endswith(".mkv"): |
||
273 | r.add_video(file) |
||
274 | r.concat(os.path.join(path, "tmp", "concat.mkv")) |
||
275 | r.speedup(os.path.join(path, "tmp", "speedup.mkv"), 0.7) |
||
276 | r.mux(os.path.join(path, "tmp", "main.mkv"), os.path.join("media", "back.mp3")) |
||
277 | videos.append(r.lastfilename) |
||
278 | |||
279 | # outro |
||
280 | r = ffmpeg.renderer() |
||
281 | r.resolution(resolution) |
||
282 | r.filename(os.path.join("media", "outro.mkv")) |
||
283 | r.process(os.path.join(path, "tmp", "outro.mkv"), "-vf scale=" + resolution) |
||
284 | videos.append(r.lastfilename) |
||
285 | |||
286 | # mastering |
||
287 | outputfilename=self.get_title().replace(" ", "-").replace(".", "_").replace("\n", "") + ".mkv" |
||
288 | r = ffmpeg.renderer() |
||
289 | r.resolution(resolution) |
||
290 | for file in videos: |
||
291 | r.add_video(file) |
||
292 | r.concat(os.path.join(path, outputfilename), audio=True) |
||
293 | |||
294 | # write description |
||
295 | outputfilename=self.get_title().replace(" ", "-").replace(".", "_").replace("\n", "") + ".txt" |
||
296 | f = open(os.path.join("media", "legal.txt"), "r") |
||
297 | if f: |
||
298 | description += f.read() |
||
299 | f.close() |
||
300 | |||
301 | f = open(os.path.join(path, outputfilename), "w") |
||
302 | if f: |
||
303 | f.write(description) |
||
304 | f.close() |
||
305 | |||
306 | |||
307 | def delete(self, filename): |
||
308 | os.remove(filename) |
||
309 | self.refresh_files() |
||
310 | self.refresh_display() |
||
311 | |||
312 | def stash(self): |
||
313 | dirname=self.get_title().replace(" ", "-").replace(".", "_").replace("\n", "") |
||
314 | if not os.path.exists(os.path.join(path, dirname)): |
||
315 | os.makedirs(os.path.join(path, dirname)) |
||
316 | for filename in glob.glob(os.path.join(path, "working", "*")): |
||
317 | shutil.move(filename, os.path.join(path, dirname)) |
||
318 | self.refresh_files() |
||
319 | self.refresh_display() |
||
320 | |||
321 | def quit(self, event): |
||
322 | ffmpeg.stop() |
||
323 | |||
324 | |||
325 | root = tkinter.Tk() |
||
326 | |||
327 | win = TubeTutorWin(root, path) |
||
328 | root.bind("<Destroy>", win.quit) |
||
329 | root.mainloop() |