Skip to content

Commit 87952d1

Browse files
authored
Various updates (#68)
1 parent 53db338 commit 87952d1

29 files changed

+1482
-140
lines changed

src/composeController.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
import os
3+
import json
4+
5+
class ComposeController:
6+
7+
def __init__(self,ui,videoManager,ffmpegService,filterController,globalOptions={}):
8+
self.ui=ui
9+
self.globalOptions=globalOptions
10+
self.videoManager=videoManager
11+
self.ffmpegService=ffmpegService
12+
self.filterController=filterController
13+
14+
self.ui.setController(self)
15+
16+
if __name__ == '__main__':
17+
import webmGenerator

src/composeUi.py

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import tkinter as tk
2+
import tkinter.ttk as ttk
3+
from pygubu.widgets.scrolledframe import ScrolledFrame
4+
import os
5+
import string
6+
import mpv
7+
from tkinter.filedialog import askopenfilename
8+
import random
9+
import time
10+
from collections import deque
11+
import logging
12+
import json
13+
import threading
14+
15+
16+
17+
class ComposeUi(ttk.Frame):
18+
19+
def __init__(self, master=None,defaultProfile='None', *args, **kwargs):
20+
ttk.Frame.__init__(self, master)
21+
22+
self.master=master
23+
self.controller=None
24+
self.defaultProfile=defaultProfile
25+
26+
def setController(self,controller):
27+
self.controller=controller
28+
29+
def tabSwitched(self,tabName):
30+
pass
31+
32+
33+
if __name__ == '__main__':
34+
import webmGenerator

src/cutselectionController.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,13 @@ def __init__(self,ui,initialFiles,videoManager,ffmpegService,ytdlService,voiceAc
4343
"""
4444

4545
self.ui.setinitialFocus()
46+
self.initialFiles = initialFiles
4647

47-
self.loadFiles(initialFiles)
48+
self.ui.after(50, self.loadInitialFiles)
4849

50+
def loadInitialFiles(self):
51+
if self.initialFiles is not None and len(self.initialFiles)>0:
52+
self.loadFiles(self.initialFiles)
4953

5054
def fitoScreen(self):
5155
self.fit = not self.fit
@@ -288,8 +292,8 @@ def playVideoFile(self,filename,startTimestamp):
288292
self.currentlyPlayingFileName=filename
289293
self.ui.restartForNewFile(self.currentlyPlayingFileName)
290294

291-
def setVideoRect(self,x,y,w,h):
292-
self.player.command('script-message','screenspacetools_rect',x,y,w,h,'2f344bdd','69dbdbff',1,'inner')
295+
def setVideoRect(self,x,y,w,h,desc=''):
296+
self.player.command('script-message','screenspacetools_rect',x,y,w,h,desc,'2f344bdd','69dbdbff',1,'inner')
293297

294298
def clearVideoRect(self):
295299
self.player.command('script-message','screenspacetools_clear')

src/cutselectionUi.py

+43-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import logging
1313
import time
1414
import subprocess as sp
15-
from .modalWindows import PerfectLoopScanModal,YoutubeDLModal,TimestampModal,VoiceActivityDetectorModal
15+
from .modalWindows import PerfectLoopScanModal,YoutubeDLModal,TimestampModal,VoiceActivityDetectorModal,Tooltip
1616
from .timeLineSelectionFrameUI import TimeLineSelectionFrameUI
1717

1818
def format_timedelta(value, time_format="{days} days, {hours2}:{minutes2}:{seconds2}"):
@@ -190,6 +190,9 @@ def __init__(self, master=None, controller=None,globalOptions={},*args, **kwargs
190190
to=float("inf"),
191191
increment=0.1,
192192
)
193+
194+
Tooltip(self.entrySiceLength,text='The default initial length of newly added subclips.')
195+
193196
self.entrySiceLength.pack(anchor="e", side="right")
194197
self.frameSliceLength.config(height="200", width="200")
195198
self.frameSliceLength.pack(fill="x", pady="2", side="top")
@@ -206,6 +209,8 @@ def __init__(self, master=None, controller=None,globalOptions={},*args, **kwargs
206209
increment=0.1,
207210
)
208211

212+
Tooltip(self.entryTargetLength,text='The target length of the final clip, useful if you want to hit a certain duration.')
213+
209214
self.entryTargetLength.pack(side="right")
210215
self.frameTargetLength.config(height="200", width="200")
211216
self.frameTargetLength.pack(fill="x", pady="2", side="top")
@@ -222,6 +227,8 @@ def __init__(self, master=None, controller=None,globalOptions={},*args, **kwargs
222227
increment=0.1,
223228
)
224229

230+
Tooltip(self.entryTargetTrim,text='The expected overlap between clips, only useful if you plan to use join clips into a sequence and add fade effects between scenes.')
231+
225232
self.entryTargetTrim.pack(side="right")
226233
self.frameTargetTrim.config(height="200", width="200")
227234
self.frameTargetTrim.pack(fill="x", pady="2", side="top")
@@ -238,6 +245,9 @@ def __init__(self, master=None, controller=None,globalOptions={},*args, **kwargs
238245
increment=0.01,
239246
)
240247

248+
Tooltip(self.entryPreviewPos,text='The number of seconds the preview is offset from the start or end of the clip when dragging, useful fo aligning events, hold ctrl to switch between offsetting from start or end.')
249+
250+
241251
self.entryPreviewPos.pack(side="right")
242252
self.framePreviewPos.config(height="200", width="200")
243253
self.framePreviewPos.pack(fill="x", pady="2", side="top")
@@ -256,6 +266,10 @@ def __init__(self, master=None, controller=None,globalOptions={},*args, **kwargs
256266
self.loopModeVar.get(),
257267
*self.loopOptions
258268
)
269+
270+
Tooltip(self.entryLoopMode,text='How to loop between subclips, either just loop the current clip, or jump between clips for a full preview of all subclips.')
271+
272+
259273
self.entryLoopMode.config(style="small.TMenubutton")
260274
self.entryLoopMode.pack(side="right")
261275
self.frameLoopMode.config(height="200", width="200")
@@ -280,6 +294,9 @@ def __init__(self, master=None, controller=None,globalOptions={},*args, **kwargs
280294
self.labelCurrentSize.config(text="0.00s 0.00% (-0.00s)")
281295
self.labelCurrentSize.pack(side="top")
282296

297+
Tooltip(self.labelCurrentSize,text='Current size counter displaying: Total Length, Percentage of Target size, (Seconds deducted by target trim overlap)')
298+
299+
283300
self.progressToSize = ttk.Progressbar(self.frameCurrentSize)
284301
self.progressToSize.config(mode="determinate", orient="horizontal")
285302
self.progressToSize.pack(expand="true", fill="x", side="left")
@@ -304,24 +321,39 @@ def __init__(self, master=None, controller=None,globalOptions={},*args, **kwargs
304321
self.buttonLoadVideos.config(command=self.loadVideoFiles)
305322
self.buttonLoadVideos.pack(expand='true', fill="x", side="left")
306323

324+
Tooltip(self.buttonLoadVideos,text='Load a video file from your system')
325+
307326
self.buttonLoadYTdl = ttk.Button(self.labelframeButtons)
308327
self.buttonLoadYTdl.config(text="Load URL")
309328
self.buttonLoadYTdl.config(style="small.TButton")
310329
self.buttonLoadYTdl.config(command=self.loadVideoYTdl)
311330
self.buttonLoadYTdl.pack(expand='true', fill="x", side="left")
312331

332+
Tooltip(self.buttonLoadYTdl,text='Download a video from a URL using yt-dlp, many popualr video and streaming sites automatically supported.')
333+
313334
self.buttonClearSubclips = ttk.Button(self.labelframeButtons)
314335
self.buttonClearSubclips.config(text="Clear SubClips")
315336
self.buttonClearSubclips.config(style="small.TButton")
316337
self.buttonClearSubclips.config(command=self.clearSubclips)
317338
self.buttonClearSubclips.pack(expand='true', fill="x", side="left")
318339

340+
Tooltip(self.buttonClearSubclips,text='Remove all subclips to start a fresh with the same videos loaded.')
341+
342+
319343
self.labelframeButtons.pack(expand='false', fill="x", side="top")
320344

321345

346+
347+
348+
349+
350+
322351
self.scrolledframeVideoPreviewContainer = ScrolledFrame(
323352
self.labelframeSourceVideos, scrolltype="vertical"
324353
)
354+
355+
356+
325357
self.videoPreviewContainer = self.scrolledframeVideoPreviewContainer.innerframe
326358
self.previews = []
327359
self.scrolledframeVideoPreviewContainer.configure(usemousewheel=True)
@@ -334,6 +366,10 @@ def __init__(self, master=None, controller=None,globalOptions={},*args, **kwargs
334366
)
335367
self.labelframeSourceVideos.pack(expand="true", fill="both", side="top")
336368

369+
370+
371+
372+
337373
self.progresspreviewLabel = ttk.Label(self.frameSliceSettings)
338374
self.progresspreviewLabel.config(text="")
339375
self.progresspreviewData = "P5\n1 1\n255\n" + ("127" * 1 * 1)
@@ -584,7 +620,11 @@ def videomousePress(self,e):
584620
logging.debug('video mouse press drag')
585621
self.screenMouseRect[2]=e.x
586622
self.screenMouseRect[3]=e.y
587-
self.controller.setVideoRect(self.screenMouseRect[0],self.screenMouseRect[1],self.screenMouseRect[2],self.screenMouseRect[3])
623+
624+
vx1,vy1 = self.controller.screenSpaceToVideoSpace(self.screenMouseRect[0],self.screenMouseRect[1])
625+
vx2,vy2 = self.controller.screenSpaceToVideoSpace(self.screenMouseRect[2],self.screenMouseRect[3])
626+
627+
self.controller.setVideoRect(self.screenMouseRect[0],self.screenMouseRect[1],self.screenMouseRect[2],self.screenMouseRect[3],desc='{}x{}'.format(int(abs(vx1-vx2)),int(abs(vy1-vy2))))
588628
if e.type == tk.EventType.ButtonRelease:
589629
logging.debug('video mouse press release')
590630
self.mouseRectDragging=False
@@ -593,7 +633,7 @@ def videomousePress(self,e):
593633
vx2,vy2 = self.controller.screenSpaceToVideoSpace(self.screenMouseRect[2],self.screenMouseRect[3])
594634

595635
self.videoMouseRect=[vx1,vy1,vx2,vy2]
596-
self.controller.setVideoRect(self.screenMouseRect[0],self.screenMouseRect[1],self.screenMouseRect[2],self.screenMouseRect[3])
636+
self.controller.setVideoRect(self.screenMouseRect[0],self.screenMouseRect[1],self.screenMouseRect[2],self.screenMouseRect[3],desc='{}x{}'.format(int(abs(vx1-vx2)),int(abs(vy1-vy2))))
597637

598638
if self.screenMouseRect[0] is not None and not self.mouseRectDragging and self.screenMouseRect[0]==self.screenMouseRect[2] and self.screenMouseRect[1]==self.screenMouseRect[3]:
599639
logging.debug('video mouse rect clear')

src/encoders/apngEncoder.py

+2
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,10 @@ def encoderFunction(width,passNumber,passReason,passPhase=0,requestId=None,width
6767
initialDependentValue=initialWidth,
6868
sizeLimitMin=sizeLimitMin,
6969
sizeLimitMax=sizeLimitMax,
70+
allowEarlyExitWhenUndersize=globalOptions.get('allowEarlyExitIfUndersized',True),
7071
maxAttempts=globalOptions.get('maxEncodeAttemptsGif',10),
7172
dependentValueName='Width',
73+
dependentValueMaximum=options.get('maximumWidth',0),
7274
requestId=requestId,
7375
optimiserName=options.get('optimizer'))
7476

src/encoders/gifEncoder.py

+2
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,10 @@ def encoderFunction(width,passNumber,passReason,passPhase=0,requestId=None,width
6565
initialDependentValue=initialWidth,
6666
sizeLimitMin=sizeLimitMin,
6767
sizeLimitMax=sizeLimitMax,
68+
allowEarlyExitWhenUndersize=globalOptions.get('allowEarlyExitIfUndersized',True),
6869
maxAttempts=globalOptions.get('maxEncodeAttemptsGif',10),
6970
dependentValueName='Width',
71+
dependentValueMaximum=options.get('maximumWidth',0),
7072
requestId=requestId,
7173
optimiserName=options.get('optimizer'))
7274

src/encoders/mp4AV1Encoder.py

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
2+
import os
3+
import logging
4+
import subprocess as sp
5+
6+
from ..encodingUtils import getFreeNameForFileAndLog
7+
from ..encodingUtils import logffmpegEncodeProgress
8+
from ..encodingUtils import isRquestCancelled
9+
10+
from ..optimisers.nelderMead import encodeTargetingSize as encodeTargetingSize_nelder_mead
11+
from ..optimisers.linear import encodeTargetingSize as encodeTargetingSize_linear
12+
13+
def encoder(inputsList, outputPathName,filenamePrefix, filtercommand, options, totalEncodedSeconds, totalExpectedEncodedSeconds, statusCallback,requestId=None,encodeStageFilter='null',globalOptions={},packageglobalStatusCallback=print):
14+
15+
audoBitrate = 8
16+
for abr in ['48','64','96','128','192']:
17+
if abr in options.get('audioChannels',''):
18+
audoBitrate = int(abr)*1024
19+
20+
audio_mp = 8
21+
video_mp = 1024*1024
22+
initialBr = globalOptions.get('initialBr',16777216)
23+
dur = totalExpectedEncodedSeconds-totalEncodedSeconds
24+
25+
if options.get('maximumSize') == 0.0:
26+
sizeLimitMax = float('inf')
27+
sizeLimitMin = float('-inf')
28+
initialBr = globalOptions.get('initialBr',16777216)
29+
else:
30+
sizeLimitMax = options.get('maximumSize')*1024*1024
31+
sizeLimitMin = sizeLimitMax*(1.0-globalOptions.get('allowableTargetSizeUnderrun',0.25))
32+
targetSize_guide = (sizeLimitMin+sizeLimitMax)/2
33+
initialBr = ( ((targetSize_guide)/dur) - ((audoBitrate / 1024 / audio_mp)/dur) )*8
34+
35+
videoFileName,logFilePath,tempVideoFilePath,videoFilePath = getFreeNameForFileAndLog(filenamePrefix, 'mp4', requestId)
36+
37+
def encoderStatusCallback(text,percentage,**kwargs):
38+
statusCallback(text,percentage,**kwargs)
39+
packageglobalStatusCallback(text,percentage)
40+
41+
def encoderFunction(br,passNumber,passReason,passPhase=0, requestId=None,widthReduction=0.0,bufsize=None):
42+
43+
ffmpegcommand=[]
44+
ffmpegcommand+=['ffmpeg' ,'-y']
45+
ffmpegcommand+=inputsList
46+
47+
48+
49+
if widthReduction>0.0:
50+
encodefiltercommand = filtercommand+',[outv]scale=iw*(1-{widthReduction}):ih*(1-{widthReduction}):flags=bicubic[outvfinal]'.format(widthReduction=widthReduction)
51+
else:
52+
encodefiltercommand = filtercommand+',[outv]null[outvfinal]'.format(widthReduction=widthReduction)
53+
54+
if options.get('audioChannels') == 'No audio':
55+
ffmpegcommand+=['-filter_complex',encodefiltercommand+',[outa]anullsink']
56+
ffmpegcommand+=['-map','[outvfinal]']
57+
elif 'Copy' in options.get('audioChannels',''):
58+
ffmpegcommand+=['-filter_complex',encodefiltercommand]
59+
ffmpegcommand+=['-map','[outvfinal]','-map','a:0']
60+
else:
61+
ffmpegcommand+=['-filter_complex',encodefiltercommand]
62+
ffmpegcommand+=['-map','[outvfinal]','-map','[outa]']
63+
64+
65+
if passPhase==1:
66+
ffmpegcommand+=['-pass', '1', '-passlogfile', logFilePath ]
67+
elif passPhase==2:
68+
ffmpegcommand+=['-pass', '2', '-passlogfile', logFilePath ]
69+
70+
if bufsize is None:
71+
bufsize = 3000000
72+
if sizeLimitMax != 0.0:
73+
bufsize = str(min(2000000000.0,br*2))
74+
75+
threadCount = globalOptions.get('encoderStageThreads',4)
76+
metadataSuffix = globalOptions.get('titleMetadataSuffix',' WmG')
77+
78+
79+
audioCodec = ["-c:a","libopus"]
80+
if 'Copy' in options.get('audioChannels',''):
81+
audioCodec = []
82+
83+
ffmpegcommand+=["-shortest", "-slices", "8", "-copyts"
84+
,"-start_at_zero", "-c:v","libaom-av1"] + audioCodec + [
85+
"-stats","-pix_fmt","yuv420p","-bufsize", str(bufsize)
86+
,"-threads", str(threadCount),"-crf" ,'25','-g', '300'
87+
,'-psnr','-cpu-used','0','-row-mt', '1', '-preset', '8'
88+
,"-metadata", 'title={}'.format(filenamePrefix.replace('-',' -') + metadataSuffix) ]
89+
90+
if sizeLimitMax == 0.0:
91+
ffmpegcommand+=["-b:v","0","-qmin","0","-qmax","10"]
92+
else:
93+
ffmpegcommand+=["-b:v",str(br)]
94+
95+
if 'No audio' in options.get('audioChannels','') or passPhase==1:
96+
ffmpegcommand+=["-an"]
97+
elif 'Stereo' in options.get('audioChannels',''):
98+
ffmpegcommand+=["-ac","2"]
99+
ffmpegcommand+=["-ar",'48k']
100+
ffmpegcommand+=["-b:a",str(audoBitrate)]
101+
elif 'Mono' in options.get('audioChannels',''):
102+
ffmpegcommand+=["-ac","1"]
103+
ffmpegcommand+=["-ar",'48k']
104+
ffmpegcommand+=["-b:a",str(audoBitrate)]
105+
elif 'Copy' in options.get('audioChannels',''):
106+
ffmpegcommand+=["-c:a","copy"]
107+
else:
108+
ffmpegcommand+=["-an"]
109+
110+
ffmpegcommand+=["-sn"]
111+
112+
if passPhase==1:
113+
ffmpegcommand += ['-f', 'null', os.devnull]
114+
else:
115+
ffmpegcommand += [tempVideoFilePath]
116+
117+
logging.debug("Ffmpeg command: {}".format(' '.join(ffmpegcommand)))
118+
proc = sp.Popen(ffmpegcommand,stderr=sp.PIPE,stdin=sp.DEVNULL,stdout=sp.DEVNULL)
119+
encoderStatusCallback(None,None, lastEncodedBR=br, lastEncodedSize=None, lastBuff=bufsize, lastWR=widthReduction)
120+
psnr = logffmpegEncodeProgress(proc,'Pass {} {} {}'.format(passNumber,passReason,tempVideoFilePath),totalEncodedSeconds,totalExpectedEncodedSeconds,encoderStatusCallback,passNumber=passPhase,requestId=requestId)
121+
if isRquestCancelled(requestId):
122+
return 0, psnr
123+
if passPhase==1:
124+
return 0, psnr
125+
else:
126+
finalSize = os.stat(tempVideoFilePath).st_size
127+
encoderStatusCallback(None,None,lastEncodedSize=finalSize)
128+
return finalSize, psnr
129+
130+
encoderStatusCallback('Encoding final '+videoFileName,(totalEncodedSeconds)/totalExpectedEncodedSeconds)
131+
132+
optimiser = encodeTargetingSize_linear
133+
if 'Nelder-Mead' in options.get('optimizer'):
134+
optimiser = encodeTargetingSize_nelder_mead
135+
136+
finalFilenameConfirmed = optimiser(encoderFunction=encoderFunction,
137+
tempFilename=tempVideoFilePath,
138+
outputFilename=videoFilePath,
139+
initialDependentValue=initialBr,
140+
twoPassMode=True,
141+
allowEarlyExitWhenUndersize=globalOptions.get('allowEarlyExitIfUndersized',True),
142+
sizeLimitMin=sizeLimitMin,
143+
sizeLimitMax=sizeLimitMax,
144+
maxAttempts=globalOptions.get('maxEncodeAttempts',6),
145+
dependentValueMaximum=options.get('maximumBitrate',0),
146+
requestId=requestId,
147+
optimiserName=options.get('optimizer'))
148+
149+
encoderStatusCallback('Encoding final '+videoFileName,(totalEncodedSeconds)/totalExpectedEncodedSeconds )
150+
151+
encoderStatusCallback('Encoding complete '+videoFilePath,1,finalFilename=finalFilenameConfirmed)

0 commit comments

Comments
 (0)