From 825b7af6e30deaf85646ce93507d8cb5a0b426ae Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 3 Jun 2017 20:39:32 -0400 Subject: [PATCH 1/9] start of background replacement components --- components/__base__.py | 6 + components/color.py | 82 +++++++++++ components/color.ui | 306 +++++++++++++++++++++++++++++++++++++++++ components/image.py | 42 ++++++ components/image.ui | 200 +++++++++++++++++++++++++++ components/text.py | 3 - components/video.py | 42 ++++++ components/video.ui | 227 ++++++++++++++++++++++++++++++ 8 files changed, 905 insertions(+), 3 deletions(-) create mode 100644 components/color.py create mode 100644 components/color.ui create mode 100644 components/image.py create mode 100644 components/image.ui create mode 100644 components/video.py create mode 100644 components/video.ui diff --git a/components/__base__.py b/components/__base__.py index 45512ce..45c148d 100644 --- a/components/__base__.py +++ b/components/__base__.py @@ -66,4 +66,10 @@ class Component: def savePreset(self): return {} + + def cancel(self): + self.canceled = True + + def reset(self): + self.canceled = False ''' diff --git a/components/color.py b/components/color.py new file mode 100644 index 0000000..d86470c --- /dev/null +++ b/components/color.py @@ -0,0 +1,82 @@ +from PIL import Image, ImageDraw +from PyQt4 import uic, QtGui, QtCore +from PyQt4.QtGui import QColor +import os +from . import __base__ + +class Component(__base__.Component): + '''Color''' + def widget(self, parent): + self.parent = parent + page = uic.loadUi(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'color.ui')) + + self.color1 = (0,0,0) + self.color2 = (133,133,133) + self.x = 0 + self.y = 0 + + page.lineEdit_color1.setText('%s,%s,%s' % self.color1) + page.lineEdit_color2.setText('%s,%s,%s' % self.color2) + page.pushButton_color1.clicked.connect(lambda: self.pickColor(1)) + btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*self.color1).name() + page.pushButton_color1.setStyleSheet(btnStyle) + page.pushButton_color2.clicked.connect(lambda: self.pickColor(2)) + btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*self.color2).name() + page.pushButton_color2.setStyleSheet(btnStyle) + # disable color #2 until non-default 'fill' option gets changed + page.lineEdit_color2.setDisabled(True) + page.pushButton_color2.setDisabled(True) + page.spinBox_x.setValue(self.x) + page.spinBox_x.setValue(self.y) + + page.lineEdit_color1.textChanged.connect(self.update) + page.lineEdit_color2.textChanged.connect(self.update) + page.spinBox_x.valueChanged.connect(self.update) + page.spinBox_y.valueChanged.connect(self.update) + self.page = page + return page + + def update(self): + self.color1 = self.RGBFromString(self.page.lineEdit_color1.text()) + self.color2 = self.RGBFromString(self.page.lineEdit_color2.text()) + self.x = self.page.spinBox_x.value() + self.y = self.page.spinBox_y.value() + self.parent.drawPreview() + + def previewRender(self, previewWorker): + width = int(previewWorker.core.settings.value('outputWidth')) + height = int(previewWorker.core.settings.value('outputHeight')) + return self.drawFrame(width, height) + + def frameRender(self, moduleNo, frameNo): + width = int(self.worker.core.settings.value('outputWidth')) + height = int(self.worker.core.settings.value('outputHeight')) + return self.drawFrame(width, height) + + def drawFrame(self, width, height): + r,g,b = self.color1 + return Image.new("RGBA", (width, height), (r, g, b, 255)) + + def loadPreset(self, presetDict): + # update widgets using a preset dict + pass + + def savePreset(self): + return {} + + def pickColor(self, num): + RGBstring, btnStyle = super().pickColor() + if not RGBstring: + return + if num == 1: + self.page.lineEdit_color1.setText(RGBstring) + self.page.pushButton_color1.setStyleSheet(btnStyle) + else: + self.page.lineEdit_color2.setText(RGBstring) + self.page.pushButton_color2.setStyleSheet(btnStyle) + + def cancel(self): + self.canceled = True + + def reset(self): + self.canceled = False diff --git a/components/color.ui b/components/color.ui new file mode 100644 index 0000000..fd427e6 --- /dev/null +++ b/components/color.ui @@ -0,0 +1,306 @@ + + + Form + + + + 0 + 0 + 586 + 197 + + + + Form + + + + + + 4 + + + + + + + + 0 + 0 + + + + + 31 + 0 + + + + Color #1 + + + + + + + + 32 + 32 + + + + + + + + 32 + 32 + + + + + + + + + 0 + 0 + + + + + 1 + 0 + + + + 12 + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + + 0 + 0 + + + + + 31 + 0 + + + + Color #2 + + + + + + + + 32 + 32 + + + + + + + + 32 + 32 + + + + + + + + + 0 + 0 + + + + + 1 + 0 + + + + 12 + + + + + + + + + 0 + + + + + + 0 + 0 + + + + Fill + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + + 0 + 0 + + + + X + + + + + + + + 0 + 0 + + + + + 80 + 16777215 + + + + + 0 + 0 + + + + 0 + + + 999999999 + + + 0 + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + + 0 + 0 + + + + Y + + + + + + + + 0 + 0 + + + + + 80 + 16777215 + + + + 999999999 + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/components/image.py b/components/image.py new file mode 100644 index 0000000..3176d7c --- /dev/null +++ b/components/image.py @@ -0,0 +1,42 @@ +from PIL import Image, ImageDraw +from PyQt4 import uic, QtGui, QtCore +import os +from . import __base__ + +class Component(__base__.Component): + '''Image''' + def widget(self, parent): + self.parent = parent + page = uic.loadUi(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'image.ui')) + self.page = page + return page + + def update(self): + # read widget values + self.parent.drawPreview() + + def previewRender(self, previewWorker): + width = int(previewWorker.core.settings.value('outputWidth')) + height = int(previewWorker.core.settings.value('outputHeight')) + return self.drawFrame(width, height) + + def frameRender(self, moduleNo, frameNo): + width = int(self.worker.core.settings.value('outputWidth')) + height = int(self.worker.core.settings.value('outputHeight')) + return self.drawFrame(width, height) + + def drawFrame(self, width, height): + return Image.new("RGBA", (width, height), (0,0,0,255)) + + def loadPreset(self, presetDict): + # update widgets using a preset dict + pass + + def savePreset(self): + return {} + + def cancel(self): + self.canceled = True + + def reset(self): + self.canceled = False diff --git a/components/image.ui b/components/image.ui new file mode 100644 index 0000000..6a24370 --- /dev/null +++ b/components/image.ui @@ -0,0 +1,200 @@ + + + Form + + + + 0 + 0 + 586 + 197 + + + + Form + + + + + + 4 + + + + + + + + 0 + 0 + + + + + 31 + 0 + + + + Image + + + + + + + + 1 + 0 + + + + 12 + + + + + + + + 0 + 0 + + + + + 1 + 0 + + + + + 32 + 32 + + + + ... + + + + 32 + 32 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + + 0 + 0 + + + + X + + + + + + + + 0 + 0 + + + + + 80 + 16777215 + + + + 999999999 + + + + + + + + 0 + 0 + + + + Y + + + + + + + + 0 + 0 + + + + + 80 + 16777215 + + + + + 0 + 0 + + + + 0 + + + 999999999 + + + 0 + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/components/text.py b/components/text.py index 7efde39..d2e009d 100644 --- a/components/text.py +++ b/components/text.py @@ -18,7 +18,6 @@ class Component(__base__.Component): self.parent = parent self.textColor = (255,255,255) self.title = 'Text' - self.titleFont = None self.alignment = 1 self.fontSize = height / 13.5 self.xPosition = width / 2 @@ -35,8 +34,6 @@ class Component(__base__.Component): page.pushButton_textColor.setStyleSheet(btnStyle) page.lineEdit_title.setText(self.title) - #if self.titleFont: - # page.fontComboBox_titleFont.setCurrentFont(QFont(self.titleFont)) page.comboBox_textAlign.setCurrentIndex(int(self.alignment)) page.spinBox_fontSize.setValue(int(self.fontSize)) page.spinBox_xTextAlign.setValue(int(self.xPosition)) diff --git a/components/video.py b/components/video.py new file mode 100644 index 0000000..1365f34 --- /dev/null +++ b/components/video.py @@ -0,0 +1,42 @@ +from PIL import Image, ImageDraw +from PyQt4 import uic, QtGui, QtCore +import os +from . import __base__ + +class Component(__base__.Component): + '''Video''' + def widget(self, parent): + self.parent = parent + page = uic.loadUi(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'video.ui')) + self.page = page + return page + + def update(self): + # read widget values + self.parent.drawPreview() + + def previewRender(self, previewWorker): + width = int(previewWorker.core.settings.value('outputWidth')) + height = int(previewWorker.core.settings.value('outputHeight')) + return self.drawFrame(width, height) + + def frameRender(self, moduleNo, frameNo): + width = int(self.worker.core.settings.value('outputWidth')) + height = int(self.worker.core.settings.value('outputHeight')) + return self.drawFrame(width, height) + + def drawFrame(self, width, height): + return Image.new("RGBA", (width, height), (0,0,0,255)) + + def loadPreset(self, presetDict): + # update widgets using a preset dict + pass + + def savePreset(self): + return {} + + def cancel(self): + self.canceled = True + + def reset(self): + self.canceled = False diff --git a/components/video.ui b/components/video.ui new file mode 100644 index 0000000..73697f3 --- /dev/null +++ b/components/video.ui @@ -0,0 +1,227 @@ + + + Form + + + + 0 + 0 + 586 + 197 + + + + Form + + + + + + 4 + + + + + + + + 0 + 0 + + + + + 31 + 0 + + + + Video + + + + + + + + 1 + 0 + + + + 12 + + + + + + + + 0 + 0 + + + + + 1 + 0 + + + + + 32 + 32 + + + + ... + + + + 32 + 32 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + + 0 + 0 + + + + X + + + + + + + + 0 + 0 + + + + + 80 + 16777215 + + + + 999999999 + + + + + + + + 0 + 0 + + + + Y + + + + + + + + 0 + 0 + + + + + 80 + 16777215 + + + + + 0 + 0 + + + + 0 + + + 999999999 + + + 0 + + + + + + + + + + + + + Loop + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + From cfb8e17b6362719ca736997a23a939bec4975e70 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 3 Jun 2017 22:58:40 -0400 Subject: [PATCH 2/9] basic image component --- components/color.py | 15 +++++++++++---- components/image.py | 36 ++++++++++++++++++++++++++++++------ components/image.ui | 11 ++++------- components/video.py | 38 ++++++++++++++++++++++++++++++++------ components/video.ui | 13 +++++-------- 5 files changed, 82 insertions(+), 31 deletions(-) diff --git a/components/color.py b/components/color.py index d86470c..ae818e2 100644 --- a/components/color.py +++ b/components/color.py @@ -57,12 +57,19 @@ class Component(__base__.Component): r,g,b = self.color1 return Image.new("RGBA", (width, height), (r, g, b, 255)) - def loadPreset(self, presetDict): - # update widgets using a preset dict - pass + def loadPreset(self, pr): + self.page.lineEdit_color1.setText('%s,%s,%s' % pr['color1']) + self.page.lineEdit_color2.setText('%s,%s,%s' % pr['color2']) + btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*pr['color1']).name() + self.page.pushButton_color1.setStyleSheet(btnStyle) + btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*pr['color2']).name() + self.page.pushButton_color2.setStyleSheet(btnStyle) def savePreset(self): - return {} + return { + 'color1' : self.color1, + 'color2' : self.color2, + } def pickColor(self, num): RGBstring, btnStyle = super().pickColor() diff --git a/components/image.py b/components/image.py index 3176d7c..021bb9e 100644 --- a/components/image.py +++ b/components/image.py @@ -7,12 +7,20 @@ class Component(__base__.Component): '''Image''' def widget(self, parent): self.parent = parent + self.settings = parent.settings page = uic.loadUi(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'image.ui')) + self.imagePath = '' + self.x = 0 + self.y = 0 + + page.lineEdit_image.textChanged.connect(self.update) + page.pushButton_image.clicked.connect(self.pickImage) + self.page = page return page def update(self): - # read widget values + self.imagePath = self.page.lineEdit_image.text() self.parent.drawPreview() def previewRender(self, previewWorker): @@ -26,17 +34,33 @@ class Component(__base__.Component): return self.drawFrame(width, height) def drawFrame(self, width, height): - return Image.new("RGBA", (width, height), (0,0,0,255)) + frame = Image.new("RGBA", (width, height), (0,0,0,0)) + if self.imagePath and os.path.exists(self.imagePath): + image = Image.open(self.imagePath) + if image.size != (width, height): + image = image.resize((width, height), Image.ANTIALIAS) + frame.paste(image) + return frame - def loadPreset(self, presetDict): - # update widgets using a preset dict - pass + def loadPreset(self, pr): + self.page.lineEdit_image.setText(pr['image']) def savePreset(self): - return {} + return { + 'image' : self.imagePath, + } def cancel(self): self.canceled = True def reset(self): self.canceled = False + + def pickImage(self): + imgDir = self.settings.value("backgroundDir", os.path.expanduser("~")) + filename = QtGui.QFileDialog.getOpenFileName(self.page, + "Choose Image", imgDir, "Image Files (*.jpg *.png)") + if filename: + self.settings.setValue("backgroundDir", os.path.dirname(filename)) + self.page.lineEdit_image.setText(filename) + self.update() diff --git a/components/image.ui b/components/image.ui index 6a24370..3cd5b1b 100644 --- a/components/image.ui +++ b/components/image.ui @@ -41,20 +41,17 @@ - + 1 0 - - 12 - - + 0 @@ -114,7 +111,7 @@ - + 0 @@ -146,7 +143,7 @@ - + 0 diff --git a/components/video.py b/components/video.py index 1365f34..561e40b 100644 --- a/components/video.py +++ b/components/video.py @@ -7,12 +7,20 @@ class Component(__base__.Component): '''Video''' def widget(self, parent): self.parent = parent + self.settings = parent.settings page = uic.loadUi(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'video.ui')) + self.videoPath = '' + self.x = 0 + self.y = 0 + + page.lineEdit_video.textChanged.connect(self.update) + page.pushButton_video.clicked.connect(self.pickVideo) + self.page = page return page def update(self): - # read widget values + self.videoPath = self.page.lineEdit_video.text() self.parent.drawPreview() def previewRender(self, previewWorker): @@ -26,17 +34,35 @@ class Component(__base__.Component): return self.drawFrame(width, height) def drawFrame(self, width, height): - return Image.new("RGBA", (width, height), (0,0,0,255)) + frame = Image.new("RGBA", (width, height), (0,0,0,0)) + ''' + if self.imagePath and os.path.exists(self.imagePath): + image = Image.open(self.imagePath) + if image.size != (width, height): + image = image.resize((width, height), Image.ANTIALIAS) + frame.paste(image) + ''' + return frame - def loadPreset(self, presetDict): - # update widgets using a preset dict - pass + def loadPreset(self, pr): + self.page.lineEdit_video.setText(pr['video']) def savePreset(self): - return {} + return { + 'video' : self.videoPath, + } def cancel(self): self.canceled = True def reset(self): self.canceled = False + + def pickVideo(self): + imgDir = self.settings.value("backgroundDir", os.path.expanduser("~")) + filename = QtGui.QFileDialog.getOpenFileName(self.page, + "Choose Video", imgDir, "Video Files (*.mp4)") + if filename: + self.settings.setValue("backgroundDir", os.path.dirname(filename)) + self.page.lineEdit_video.setText(filename) + self.update() diff --git a/components/video.ui b/components/video.ui index 73697f3..6a01368 100644 --- a/components/video.ui +++ b/components/video.ui @@ -41,20 +41,17 @@ - + 1 0 - - 12 - - + 0 @@ -114,7 +111,7 @@ - + 0 @@ -146,7 +143,7 @@ - + 0 @@ -183,7 +180,7 @@ - + Loop From 443c65455a1cae8ccaea0f0af7cdda3919c709f8 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 4 Jun 2017 00:19:10 -0400 Subject: [PATCH 3/9] half-finished video component will finish tomorrow --- components/video.py | 67 +++++++++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/components/video.py b/components/video.py index 561e40b..3d6ba7a 100644 --- a/components/video.py +++ b/components/video.py @@ -1,6 +1,6 @@ from PIL import Image, ImageDraw from PyQt4 import uic, QtGui, QtCore -import os +import os, subprocess from . import __base__ class Component(__base__.Component): @@ -24,25 +24,27 @@ class Component(__base__.Component): self.parent.drawPreview() def previewRender(self, previewWorker): - width = int(previewWorker.core.settings.value('outputWidth')) - height = int(previewWorker.core.settings.value('outputHeight')) - return self.drawFrame(width, height) + self.width = int(previewWorker.core.settings.value('outputWidth')) + self.height = int(previewWorker.core.settings.value('outputHeight')) + frames = self.getVideoFrames(True) + if frames: + im = Image.open(frames[0]) + im = self.resize(im) + return im + else: + return Image.new("RGBA", (self.width, self.height), (0, 0, 0, 0)) + + def preFrameRender(self, **kwargs): + super().__init__(**kwargs) + self.width = int(self.worker.core.settings.value('outputWidth')) + self.height = int(self.worker.core.settings.value('outputHeight')) + self.frames = self.getVideoFrames() def frameRender(self, moduleNo, frameNo): - width = int(self.worker.core.settings.value('outputWidth')) - height = int(self.worker.core.settings.value('outputHeight')) - return self.drawFrame(width, height) - - def drawFrame(self, width, height): - frame = Image.new("RGBA", (width, height), (0,0,0,0)) - ''' - if self.imagePath and os.path.exists(self.imagePath): - image = Image.open(self.imagePath) - if image.size != (width, height): - image = image.resize((width, height), Image.ANTIALIAS) - frame.paste(image) - ''' - return frame + i = frameNo if frameNo < len(self.frames)-1 else len(self.frames)-1 + im = Image.open(self.frames[i]) + im = self.resize(im) + return im def loadPreset(self, pr): self.page.lineEdit_video.setText(pr['video']) @@ -66,3 +68,32 @@ class Component(__base__.Component): self.settings.setValue("backgroundDir", os.path.dirname(filename)) self.page.lineEdit_video.setText(filename) self.update() + + def getVideoFrames(self, firstOnly=False): + # recreate the temporary directory so it is empty + # FIXME: don't dump too many frames at once + if not self.videoPath: + return + self.parent.core.deleteTempDir() + os.mkdir(self.parent.core.tempDir) + if firstOnly: + filename = 'preview%s.jpg' % os.path.basename(self.videoPath).split('.', 1)[0] + options = '-ss 10 -vframes 1' + else: + filename = '$frame%05d.jpg' + options = '' + subprocess.call( \ + '%s -i "%s" -y %s "%s"' % ( \ + self.parent.core.FFMPEG_BIN, + self.videoPath, + options, + os.path.join(self.parent.core.tempDir, filename) + ), + shell=True + ) + return sorted([os.path.join(self.parent.core.tempDir, f) for f in os.listdir(self.parent.core.tempDir)]) + + def resize(self, im): + if im.size != (self.width, self.height): + im = im.resize((self.width, self.height), Image.ANTIALIAS) + return im From 39e66ffa2d07b87b57ed90b369ab26aedf0a69e8 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 4 Jun 2017 13:00:36 -0400 Subject: [PATCH 4/9] video component almost working, rm hardcoded backgrounds --- background.jpg | Bin 14782 -> 0 bytes background.png | Bin 0 -> 3711 bytes components/__base__.py | 7 ++++ components/color.py | 16 ++++----- components/image.py | 14 ++++---- components/original.py | 20 +++--------- components/text.py | 8 +---- components/video.py | 68 ++++++++++++++++++++++++-------------- core.py | 61 ++++------------------------------ main.py | 50 +++++++++------------------- mainwindow.ui | 72 ++--------------------------------------- preview_thread.py | 20 +++--------- video_thread.py | 46 +++++++------------------- 13 files changed, 110 insertions(+), 272 deletions(-) delete mode 100644 background.jpg create mode 100644 background.png diff --git a/background.jpg b/background.jpg deleted file mode 100644 index f7464320176adb5a0176b305e4726baf72c8f3e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14782 zcmeHtXH*o?mhP#l&XI1KoSICNbIv(u$%2F?=Zt`WfPyG0K@dcd@o}wBif^Aek8(8kYid0T36Tn(S_4&h+v1V`Aq35fA_g(1C7n zXiB1dd{id*Uv1?9;A8?|Qt3~w|26D?9w3IXQ$hiNaGc@J(8T0a4({jt=^3es91P#$ z_!*&*!C@Tyl!FzMIRyfMkpB2X{)w4?aKt~c<{zBw>2AS^u?2uhiug}BS;9 zvHHwEH=OckqUrw>$5Li`MnN*?^!qn{z;;okQ|}%?@Ohqgk&kD1gmfosKl}(f@4Dy!q_b3KV1CL?VmFM z|I%YhxQ6;aToB0|0QBto`WI;cu$2I;?O9*{*|fgCc9^4+832ak|20415CFQ%9RHqw z`J@^FpcVnpcllqwkQxBaYzKhhu7fU<2?5KEN0FfdH@(1cD$C z%u!}22m|39jYe{`8N<>b6pm`sK?cYKSs)wafLyQzMEunlYn zC7=|Pft_F%C7qV1INJ$&<;Al zDbNYJKsV?Cy`T@A0cXKEFaXYjK`;b{!9_3vE`w2U1zZK!!3}T|+yZ0Z4j2b_!6di` z?t>{X4IY9=Up&igps2th@ zRX|nHKBxxT57k2lphl<(Is&yoN1t|4}E|>L0_O{=sUCu{ephO0ES@{ zjD_(q5hlY_m<}^wUYH*ighgO6SR9swrC~W(9#({vVKrC-)`E3meb^8-hRt9L*b26V z?O{jQ1$KiyU@zDQ_JcRVL2xh}28Y8@a10y=C&I~a8k_-V!8!02xBxDMx4|WF8N3VL z16RP+a1C4sH^7Z>6Wk29!pGnf@JYB6J`MN6{qO*M0Um}&;8FM*d;`7>-+?FK`|uR} z2z~<3!O!6r@B;h>eg}VmKf}xLH+U6ZgVzxlK_fVXfRGU`gd5>O_z@vQ6k#Hghz!C) z6cJ@a4ben&5Pif5F+t1`E5sIYK%5ab#1rvCe31Yo2nj*hNF)-2#3M;aDw2V0MsksS zWGhmPlptkDIkFe2LTZtE(O~P@X7nls_sE6@p@;B2lrZ1XKzt9hHU3MdhQ4P}@;usB%;V zsv5N))qpyLI)XZiI*vMt>O%FR`cdamL#RurE2tZ&G1Ohuebh8+2K5v*k6J*zK`o&^ zqn1%WP-|#_Mxk+NBASZkM)RTt(V}Pxv^1K9RzjgOaZ1CvjbC(sle1=>M@O&W=tEV z9n*#B#hk?qVn#4mFgG!GF!wOim>JA7%uCE0%o64^<{RcG7GP0WJeG{5V|lScSTU>= zRt~F#Rl{my^|2;c3#={H3G0US!unx@u%XyUY#cTTn~u%K=3$GlrPy8A3TzFw9(xGe zf<1;ki9L-ygFTPEh`oZnfxUyhhkb~Bf_;u%z`nzN!Y*T1vFkV#4v!<_=r}%{FispN zgHyn%;52c1IAfdz&KBo{bH{n(0&u~&a9j*75toMBjLX9n;YxAkxJq0t?f~vE?kKJu z*M;lD4d8}xqqrNmJGgtehqyW13)~`Z3HJr}9k+%@@HjjfPsj7&Meq`MS-c`%4X=YY z#GB!*@s4;mycgadAB+#j$KaFj>G*7X0e&0448Iq@4_}XO!nfj2;JfgB`1AOS_$&BZ z_`CQi{0#m%egXd;{~7-szea!wI0Bi#O%Nc65~K($f(k*CpieL%SP|?At^_ZFKOvY9 zPKYHW6EX<7ghE0Ip`1`f*iUFAv=ELHIthJ*^Ms3ptAyKxNy0Q?mhgh`hVX%~O!!HJ zh*%*>f=E~rnZ!*JATdcYBt?=sNta|yvLZQ< z+(_P}jifMAG%1mkPRb<}l1fRtN&83*q{E~((kW6eX@E3Bx<(o!-6uUJJtMs)Es>T< zKgkdoN2ZW@$UKwUOFN?V$Eh2dE>|8`QhhY3dyH z74Fx9$`g!_g`YrlB`eXV${Vn|q{Uhup!)wZG$Lr4P#~aEU%bUi#g?Ah8 zZr)nnCf?(`r+Ei>FZ15!eZV`%`PP!})|uo7?< z@DT_Wh!IE?*dnl9V6Q-(K(j!Hz!`yIfg1vo0#5{9349b-6+{Hdg1mxEL3u$9K_fwH zL03UP!BD|C!3@Cy!5xBCf(HeU3U&z&2#yMl2~G<>7knqUEVwR&7vdHY5t0>B719^7 z6mk~w5egBC6-pDz6Dk#|6gnVuRH#d6KxkCxj?hD)d7<}0--IDyqA-uJm@rFNL)b{z zM%Yc*UzjbND7;y?NVr_MR`{@RyKtZIMd6#m_l4(#7lpqH{}#cC&_#qrWJT0O3`DF% zTt)mu!bB28vP23+%0+5L4vTb%oDsPwa!cfa$TN|*BFmyclpx9@Dkdr~swrwLYA5O; z8Ymhmnj)GjS|VC0dO);Iv|IFo=vC1P(HYSN(a)m4#IRy?F<~)TF?BIRF&i;=u>i3M zv1G9vvF&0NVh6;Iigk-!5W6NeA@)SgY`lj@NglDa81B{eU#B(*Ayk)}(F zO0%T3q|KzAqNH0i#kzSV}%J9iZ%BaW~$k@nu z$OOs6$YjVA$?TD7kZF_Ykr|e`B{MDaLgu5)nk-(HM^;=`NmftRTGm}QP&QgNUA9nm zw`{%aQQ02ZVcA=<4`pAw{0{jV`DXbp`3v$l9NwH(uy)l znWikNtf;J~Y@_U{9ITw6oTI!$xkkA~xm$Ty`L^<7<=4t9Du@bAMN~ymMPJ26WrIqH zN}|dZl`@t6Do0g%R7O_+?rNLV;?;80cBs{=wW{@~ji`;Q&8odqTUE!Y^Qud! ztE-!;JF5q%N2_P4Z&$BYKce2HKBRtIeMbF_`gaYC217$qLrud(!$~7RBU&RnwK;uG@ojI&|K3Z zX$fkvv~;wrwLG;#wUV{+wRUSA&}!Ej(7LWQt+k-Ftc}v<))v=R)i%*~)(+5)(cY|G zs$HYqs@oI;fsUPyk4}V6x=yi9mCg~JZk>xd<2rLXOFC=1 zBwZn0d0jnSTU~G6aNRWBBHc>e!@6C%7j?&V=X95J*YrqwLV5~%`g(SHK6(*)8G76F zs`Z-ndh{;oP3S$-`=k%_srq92%KAq7PWl1*G5Xp1JM`=HkLma8U(=t`U(jDMz!>lt zNE>JxSQ>a3gc>9pY&FgP}ES#(8$oqFu*X@FvqaWu-@>5 z;W@(_hK~&282&KA8wnV(jP#6bjeLwEjWUf&jB1UJ8l5q^YBXiEVD!xxYs_aXYpi2z zW9)4lVVq&S-MGg1sPP%&tHx8t3&!6}uqJ#avL-qvHYVOC5hj@?B__2dZ6^ID*GwLo zESmf<#hVJ4%A4w&+MD{B#+YWCmYFt~wws0r$^u6hCGm4q0nTnZ-nTy#bvqZB3 zv%O}A&AQDlnN6C#F#BqbHs>*yG1oS?G50o)G|x0IHLo*2Za!ds(|pGKz4@92#X`(N z#lqCW%_7($*`m;*%A(n#*W!xBl*NL@cT2pbprwMPfu*D6M$351Jj*?nO_tr3BbJkv zFD#d>Fjjn4a#ng)_E!E@u~xZO%wv|aB+6o{8=$+~vH>n9DPlFRmC@epdxoBUe|~5Z5%FXKmnde#I+2YyndDC;&^YaGu2L25S z8;m!&ZD4Q6+)%dR;D)XZqZ_6-yxs8Ii|!@urR(M3737udRqVCjtKDnJYtrkL*Qz(! zo9V6TZR73l9q+x>yW0Dh_j&Je?|JWUJ_H|8A2lBFxE!tc93 z(O=A8!{5f=-#@{>(7(q2g#VELr2m5d&j4zGWPnb9L%^nh)PRzJhJen1%K_5??>0gk zc{Z{(8f|pj$lkblWBJCz8~Zli+&H)KOCT;#I8ZgvGSD|LE^uq$zQE&wLxGcl3xU6a zxPqjE^n#p&LV_}a%7P9B^#olHdJ^<`6K0dpCY4Q=n|wCKZQ8nN-=-6rhBn>Xw76+4 zm>w(}Y!K`k%nsfhTpoNRxIg%I@U!5R5Ml^3L@UHDBrqf;q$K1(NO#E9kjEh(L(!pv zp(>%4p}wKQIVaLOU!tRAFhW%!9vsr9o zwg)?soy)Fdx3MpIz9_wb1D-0+I_)_abgJ zo)#|~ZxrtlA03|`zc0Qc{!;wI_>T#g1mOgY1iOSy3F!$t6OJUDO&Cvjm9Un`ohYAZ zn&_Pvmspfom)MnfHE}laYZ5U@B1t#NB`G{9C#f>&c+zmvRMJv1Dp@F5J=r!nC^YZl2S@i8dLgGZl}CRSxu#-%B7m5dZost7NypwcBfuX zeVY0$jglslW|Zcc7L&F$ZGT#4+SRn#w3T#nx>UMBx_f$bdO><^`l9gs}8KexU z41)~!jOdJljM|LOjH?;587rCOOzBL+OwY`i%&nPqnO&LJGoNOD&!T3@W*KLBWyNO| zXC27u$-0#_pY?My_h$Lc=9~RCCv7g>+_brW^Z4f1*&v%QTP52jJ19FnyF9x!docS! z_EHWyM)MlbchW(~&cpGn4Z*my|1&Ynbbq8=G5{+mPFndpq}K?%EcfElOLg zwrt#zwq@6rmMw!@9&B04!{mwP>EyZOMdan>)#P>NUCVo#_amQ{&&oH;_sdVt-;sYL ze<1%}{<{KHfk=T?fpbB4L0&;kL1)3Wf@cLkw$iuCZ?)L!zcqF1&aEw52e&@h`k@e8 z$Sl+=bSsQ5EG(=q>?yom__FYK5nqvNk!?|M(dMGcqV}T8MNf)WimAnN#b(8R#mU8G z#m&VRiXRkz*oNCCzD<9d$F|sQ#oG>UJF{(k+v0Y3yU=#c?M~Yxw&!o(zrB0=&FwF? zua)qXsFv83gp_2LRF|AAxmxnHWVMvLRI${$G^jMQbZ_a2(#xf@rQddN?U3JLv18+o z^c}l*9NRInV`j&48MREV%)HFMEUm1(tgURM>~Y!hPRdTXo#s3Jcc$&!y|ZoS$j+Ia z%e$z%Si8)31?)=SwP)9{U6*z}+4ZfQR<2NPSsqxPSzb}zUVf$gY5D4I?%hheZFUFm z&fZMY6fZ^)O@NX)ymeI*KVxMsI92&sJ&J@U;BH%z<$mBF8ibR7wTcA%tOxbN^*Z(L^>Otj_09D|^^fY88)yxR4K@v-4O<%a zH}p1)H@rQ7Js@$w=z#Bm)B}4CoH%gh!1DuZ2L%pl9&|kzb8!2?!v_ZsK03JENNZGV zv~3J+%xkP~>}#B8Tsnk5Bz?&2P{5(gLsf@558XVp(1bLJHR(5bH6=HdHyv-f()6rp z?XbXMt;24I;|`Y|ZaIAM@RP$oj_@2&JK}UC>PYdCrXv@QJUp`8OlwwZwrgfL7c?Ji zKG*!9`Ev`UMZU$RCA1~4rM{)VxZMnqjEe~9+Cfhz7BOPNMvpN=XY|F9wWBteO9s6{ga$Nqn z&GE3~`Nt0&KX-iU_?Hu0CzMXupNKe7c;e8B3nw0(_}0#7S8aD{k8a=I-rRn%eYX8) z2Y-iFhkHjt$Igyp9alQ$PlA&oC-qNypG-Yjaq`s3TPGJ!VNOY&GCdV|D*IIJsoqnQ zr#^O)JLNlVJK3FEI~zMMbUy0**2UAM-sRdA*R`Xot!uPvz8iFlb{lm2cBgk&b$4~& z>3(;ba9Z}X)#;Ga`KJ$_9ytB*^hyt-N4>|TC$6Wgr>*Bo&x>BTSFG2l*S|NbcVBN$ z??ms%K1!crpIu)>Uvb}&zKeZxeQRfg&gh--K9hE)@=Vv6J7?ba6Z={HHvR1W!v3cI zq5dcRKhFxB)jjKVHuY@9+0L_LXWySAo@1S}ITwDe=-lCR!{_GCtqlkb=nwb|qz_aN zoF2G4@bNt5ywZ7x^U>!^&bOW)JwJZ|zQDX-a$)0zoD20A&R&?lurkOqs4?h1m^8S1 z@Z{j_!M8(%A=Z%1Q20>sQ1j5p(6eDMEH-RB959?cTsM4nczXETMV^bA7d*=#kQqwvnqNuP&i4NnNtI6mqHH(xFSkm*y`0zASRt@Us8q?8|kR z&t9Is{B4wPRC{#8XxeDi=;_gk(a%?CS5&XKUP-vJ`^w2Hx39dvO1i3e)!}N))g4!l zU%h^H@fz-$+%=nP5!bd|YrQsl?d5g!b*bx?*Tb$CUO#gE()H&z;2RP*%x(nVD7evd zWBA6?o8Tt%rs>U~n|U`IZw}p@ySaW#?3T%`z*}2x9lSMoYxdUf+oHFPZwKDqa{J)z z!P~R9e~*ccnT!RE<&8Ct4UNr>t>0nZF}<_tPX3)kcP`#}HV%!8kDHH&jBg!3Jbr0> z{w{J?>aOKo_TA#UEqAZneKmodke#rZh@2>yI5u&8;>{#sQeo0@GIny;WXI&#!I31_lGGDs~+|~eDLu5BY{WykNh9y zJUaMj=+V>1&|`_mmXFzww>@rqeEsp88RCr6jPp#w%-)&qnR_$KPk5i`KJk5${iNZ^ z;FGyoXjWp@a+W>2eYS1(#_YQ}@|?s<0&)m-n~)ZCA!f=>;f20qPu+Vu3&)0fXM z&*YxjKZ|{~>)EMicb|QE&huR7x$pDM&l{c(K7Tq7&r8i)&qvPhm~WpSoB#NN{zCJG z*Ne;-buZ4pn0*PolzeIRGU8?F%M&kezx?=${z~(e*Q=~o^{+0xnp=Puq!z3fq87>) zIu^zkKEGzX)_LvwI{Wp(*Tb*p7txDyi}s6gi@O)Q7w;{8dn541@J-;Gf;Y`?uDn@% zOMI*R*6nS|+kJ1(zMXmd`HW(k+>*kQ z^HS1M)zX=zM@wrTm>(=Ygnuag(Ej1hhtD5*KI(q-|G4F2)5p<|uRjq#DSvYNl=`Xm z)4-?M&(LS7&$gdqK9_&)`h5TM_b)qe!7BMkzH|ENm!{^>05cYvi42IjtqH?OQvyHv1d?E&JQycf#+g-~GR5*1@{;y4`x* zdc}I*`s4NWfA*Ddx&V+rtpG=+0PxHK!0|Q!`|ALZiQx1R1gt;ie7VP<&=@odgF$1k zSPYK9MIhku1O|ml;^JfQ^Ybxyc?CtJ#RP>Ug?V|IicCovS(ZFYKuk$hNlsN-jwSae z353OBIiFm(2?TCA&MQ;7|7%+x1~eQzjs^%M0bm-0(4h6poCbwIy+QxQa2z*->;LqE zKi>Zq`wuVezsEiTB!tr&K|^Sq_J!*IlK$_-Uk3g%@RxzV4E$x_F9UxW_{+dw2L3Ye gmw~?w{AJ)T1AiI#%fMd-{xa~Ff&UK}$XkE-Z@H1mFVU|*7>;E{Dfq{?5)5S5Q;?~=PjX-&Z!wY@} z$3J8UWdw0X!DtAKhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2DQ41x8GZW0WPt*n!r PLH_e}^>bP0l+XkKVb3HZ literal 0 HcmV?d00001 diff --git a/components/__base__.py b/components/__base__.py index 45c148d..f564aad 100644 --- a/components/__base__.py +++ b/components/__base__.py @@ -7,6 +7,13 @@ class Component: def version(self): # change this number to identify new versions of a component return 1 + + def cancel(self): + # make sure your component responds to these variables in frameRender() + self.canceled = True + + def reset(self): + self.canceled = False def preFrameRender(self, **kwargs): for var, value in kwargs.items(): diff --git a/components/color.py b/components/color.py index ae818e2..c2a49e2 100644 --- a/components/color.py +++ b/components/color.py @@ -42,13 +42,17 @@ class Component(__base__.Component): self.x = self.page.spinBox_x.value() self.y = self.page.spinBox_y.value() self.parent.drawPreview() - + def previewRender(self, previewWorker): width = int(previewWorker.core.settings.value('outputWidth')) height = int(previewWorker.core.settings.value('outputHeight')) return self.drawFrame(width, height) - - def frameRender(self, moduleNo, frameNo): + + def preFrameRender(self, **kwargs): + super().preFrameRender(**kwargs) + return ['static'] + + def frameRender(self, moduleNo, arrayNo, frameNo): width = int(self.worker.core.settings.value('outputWidth')) height = int(self.worker.core.settings.value('outputHeight')) return self.drawFrame(width, height) @@ -81,9 +85,3 @@ class Component(__base__.Component): else: self.page.lineEdit_color2.setText(RGBstring) self.page.pushButton_color2.setStyleSheet(btnStyle) - - def cancel(self): - self.canceled = True - - def reset(self): - self.canceled = False diff --git a/components/image.py b/components/image.py index 021bb9e..ffbb117 100644 --- a/components/image.py +++ b/components/image.py @@ -27,8 +27,12 @@ class Component(__base__.Component): width = int(previewWorker.core.settings.value('outputWidth')) height = int(previewWorker.core.settings.value('outputHeight')) return self.drawFrame(width, height) - - def frameRender(self, moduleNo, frameNo): + + def preFrameRender(self, **kwargs): + super().preFrameRender(**kwargs) + return ['static'] + + def frameRender(self, moduleNo, arrayNo, frameNo): width = int(self.worker.core.settings.value('outputWidth')) height = int(self.worker.core.settings.value('outputHeight')) return self.drawFrame(width, height) @@ -49,12 +53,6 @@ class Component(__base__.Component): return { 'image' : self.imagePath, } - - def cancel(self): - self.canceled = True - - def reset(self): - self.canceled = False def pickImage(self): imgDir = self.settings.value("backgroundDir", os.path.expanduser("~")) diff --git a/components/original.py b/components/original.py index 6903a5f..b0a7579 100644 --- a/components/original.py +++ b/components/original.py @@ -58,6 +58,8 @@ class Component(__base__.Component): self.smoothConstantUp = 0.8 self.lastSpectrum = None self.spectrumArray = {} + self.width = int(self.worker.core.settings.value('outputWidth')) + self.height = int(self.worker.core.settings.value('outputHeight')) for i in range(0, len(self.completeAudioArray), self.sampleSize): if self.canceled: @@ -72,12 +74,9 @@ class Component(__base__.Component): pStr = "Analyzing audio: "+ str(progress) +'%' self.progressBarSetText.emit(pStr) self.progressBarUpdate.emit(int(progress)) - - - def frameRender(self, moduleNo, frameNo): - width = int(self.worker.core.settings.value('outputWidth')) - height = int(self.worker.core.settings.value('outputHeight')) - return self.drawBars(width, height, self.spectrumArray[frameNo], self.visColor, self.layout) + + def frameRender(self, moduleNo, arrayNo, frameNo): + return self.drawBars(self.width, self.height, self.spectrumArray[arrayNo], self.visColor, self.layout) def pickColor(self): RGBstring, btnStyle = super().pickColor() @@ -154,12 +153,3 @@ class Component(__base__.Component): return im - def cancel(self): - self.canceled = True - - def reset(self): - self.canceled = False - - - - diff --git a/components/text.py b/components/text.py index d2e009d..2650935 100644 --- a/components/text.py +++ b/components/text.py @@ -104,7 +104,7 @@ class Component(__base__.Component): super().preFrameRender(**kwargs) return ['static'] - def frameRender(self, moduleNo, frameNo): + def frameRender(self, moduleNo, arrayNo, frameNo): width = int(self.worker.core.settings.value('outputWidth')) height = int(self.worker.core.settings.value('outputHeight')) return self.addText(width, height) @@ -137,9 +137,3 @@ class Component(__base__.Component): return self.page.lineEdit_textColor.setText(RGBstring) self.page.pushButton_textColor.setStyleSheet(btnStyle) - - def cancel(self): - self.canceled = True - - def reset(self): - self.canceled = False diff --git a/components/video.py b/components/video.py index 3d6ba7a..1a9f344 100644 --- a/components/video.py +++ b/components/video.py @@ -5,6 +5,10 @@ from . import __base__ class Component(__base__.Component): '''Video''' + def __init__(self): + super().__init__() + self.working = False + def widget(self, parent): self.parent = parent self.settings = parent.settings @@ -27,24 +31,35 @@ class Component(__base__.Component): self.width = int(previewWorker.core.settings.value('outputWidth')) self.height = int(previewWorker.core.settings.value('outputHeight')) frames = self.getVideoFrames(True) - if frames: - im = Image.open(frames[0]) - im = self.resize(im) - return im - else: - return Image.new("RGBA", (self.width, self.height), (0, 0, 0, 0)) + if not hasattr(self, 'staticFrame') or not self.working and frames: + frame = Image.new("RGBA", (self.width, self.height), (0, 0, 0, 0)) + if frames: + im = Image.open(frames[0]) + im = self.resize(im) + frame.paste(im) + if not self.working: + self.staticFrame = frame + return self.staticFrame def preFrameRender(self, **kwargs): - super().__init__(**kwargs) + super().preFrameRender(**kwargs) self.width = int(self.worker.core.settings.value('outputWidth')) self.height = int(self.worker.core.settings.value('outputHeight')) self.frames = self.getVideoFrames() + self.working = True - def frameRender(self, moduleNo, frameNo): - i = frameNo if frameNo < len(self.frames)-1 else len(self.frames)-1 - im = Image.open(self.frames[i]) - im = self.resize(im) - return im + def frameRender(self, moduleNo, arrayNo, frameNo): + print(frameNo) + try: + if frameNo < len(self.frames)-1: + self.staticFrame = Image.new("RGBA", (self.width, self.height), (0, 0, 0, 0)) + im = Image.open(self.frames[frameNo]) + im = self.resize(im) + self.staticFrame.paste(im) + except FileNotFoundError: + print("Video component encountered an error") + self.frames = [] + return self.staticFrame def loadPreset(self, pr): self.page.lineEdit_video.setText(pr['video']) @@ -53,12 +68,6 @@ class Component(__base__.Component): return { 'video' : self.videoPath, } - - def cancel(self): - self.canceled = True - - def reset(self): - self.canceled = False def pickVideo(self): imgDir = self.settings.value("backgroundDir", os.path.expanduser("~")) @@ -69,19 +78,27 @@ class Component(__base__.Component): self.page.lineEdit_video.setText(filename) self.update() - def getVideoFrames(self, firstOnly=False): + def getVideoFrames(self, preview=False): # recreate the temporary directory so it is empty - # FIXME: don't dump too many frames at once + # FIXME: don't dump all the frames at once, don't dump more than sound length + # FIXME: make cancellable, report status to user, etc etc etc if not self.videoPath: return + name = os.path.basename(self.videoPath).split('.', 1)[0] + if preview: + filename = 'preview%s.jpg' % name + if os.path.exists(os.path.join(self.parent.core.tempDir, filename)): + return False + else: + filename = name+'-frame%05d.jpg' + + # recreate tempDir and dump needed frame(s) self.parent.core.deleteTempDir() os.mkdir(self.parent.core.tempDir) - if firstOnly: - filename = 'preview%s.jpg' % os.path.basename(self.videoPath).split('.', 1)[0] - options = '-ss 10 -vframes 1' + if preview: + options = '-ss 10 -vframes 1' else: - filename = '$frame%05d.jpg' - options = '' + options = '' #'-vframes 99999' subprocess.call( \ '%s -i "%s" -y %s "%s"' % ( \ self.parent.core.FFMPEG_BIN, @@ -91,6 +108,7 @@ class Component(__base__.Component): ), shell=True ) + print('### Got Preview Frame From %s ###' % name if preview else '### Finished Dumping Frames From %s ###' % name) return sorted([os.path.join(self.parent.core.tempDir, f) for f in os.listdir(self.parent.core.tempDir)]) def resize(self, im): diff --git a/core.py b/core.py index 16ecb35..ecbf12c 100644 --- a/core.py +++ b/core.py @@ -13,11 +13,8 @@ from collections import OrderedDict class Core(): def __init__(self): - self.lastBackgroundImage = "" - self._image = None - self.FFMPEG_BIN = self.findFfmpeg() - self.tempDir = None + self.tempDir = os.path.join(tempfile.gettempdir(), 'audio-visualizer-python-data') atexit.register(self.deleteTempDir) def findFfmpeg(self): @@ -31,31 +28,6 @@ class Core(): except: return "avconv" - def parseBaseImage(self, backgroundImage, preview=False): - ''' determines if the base image is a single frame or list of frames ''' - if backgroundImage == "": - return [''] - else: - _, bgExt = os.path.splitext(backgroundImage) - if not bgExt == '.mp4': - return [backgroundImage] - else: - return self.getVideoFrames(backgroundImage, preview) - - def drawBaseImage(self, backgroundFile): - if backgroundFile == '': - im = Image.new("RGB", (int(self.settings.value('outputWidth')), int(self.settings.value('outputHeight'))), "black") - else: - im = Image.open(backgroundFile) - - if self._image == None or not self.lastBackgroundImage == backgroundFile: - self.lastBackgroundImage = backgroundFile - # resize if necessary - if not im.size == (int(self.settings.value('outputWidth')), int(self.settings.value('outputHeight'))): - im = im.resize((int(self.settings.value('outputWidth')), int(self.settings.value('outputHeight'))), Image.ANTIALIAS) - - return im - def readAudioFile(self, filename, parent): command = [ self.FFMPEG_BIN, '-i', filename] @@ -121,30 +93,11 @@ class Core(): return completeAudioArray def deleteTempDir(self): - if self.tempDir and os.path.exists(self.tempDir): - rmtree(self.tempDir) - - def getVideoFrames(self, videoPath, firstOnly=False): - self.tempDir = os.path.join(tempfile.gettempdir(), 'audio-visualizer-python-data') - # recreate the temporary directory so it is empty - self.deleteTempDir() - os.mkdir(self.tempDir) - if firstOnly: - filename = 'preview%s.jpg' % os.path.basename(videoPath).split('.', 1)[0] - options = '-ss 10 -vframes 1' - else: - filename = '$frame%05d.jpg' - options = '' - sp.call( \ - '%s -i "%s" -y %s "%s"' % ( \ - self.FFMPEG_BIN, - videoPath, - options, - os.path.join(self.tempDir, filename) - ), - shell=True - ) - return sorted([os.path.join(self.tempDir, f) for f in os.listdir(self.tempDir)]) + try: + if os.path.exists(self.tempDir): + rmtree(self.tempDir) + except FileNotFoundError: + pass def cancel(self): self.canceled = True @@ -153,6 +106,6 @@ class Core(): self.canceled = False @staticmethod - def sortedStringDict(dictionary): + def stringOrderedDict(dictionary): sorted_ = OrderedDict(sorted(dictionary.items(), key=lambda t: t[0])) return repr(sorted_) diff --git a/main.py b/main.py index 2aa7fa9..da72b1e 100644 --- a/main.py +++ b/main.py @@ -133,9 +133,9 @@ class PreviewWindow(QtGui.QLabel): class Main(QtCore.QObject): - newTask = QtCore.pyqtSignal(str, list) + newTask = QtCore.pyqtSignal(list) processTask = QtCore.pyqtSignal() - videoTask = QtCore.pyqtSignal(str, str, str, list) + videoTask = QtCore.pyqtSignal(str, str, list) def __init__(self, window): QtCore.QObject.__init__(self) @@ -172,14 +172,13 @@ class Main(QtCore.QObject): # begin decorating the window and connecting events window.toolButton_selectAudioFile.clicked.connect(self.openInputFileDialog) - window.toolButton_selectBackground.clicked.connect(self.openBackgroundFileDialog) window.toolButton_selectOutputFile.clicked.connect(self.openOutputFileDialog) window.progressBar_createVideo.setValue(0) window.pushButton_createVideo.clicked.connect(self.createAudioVisualisation) window.pushButton_Cancel.clicked.connect(self.stopVideo) window.setWindowTitle("Audio Visualizer") - self.previewWindow = PreviewWindow(self, os.path.join(os.path.dirname(os.path.realpath(__file__)), "background.jpg")) + self.previewWindow = PreviewWindow(self, os.path.join(os.path.dirname(os.path.realpath(__file__)), "background.png")) window.verticalLayout_previewWrapper.addWidget(self.previewWindow) self.modules = self.findComponents() @@ -260,17 +259,6 @@ class Main(QtCore.QObject): self.settings.setValue("outputDir", os.path.dirname(fileName)) self.window.lineEdit_outputFile.setText(fileName) - def openBackgroundFileDialog(self): - backgroundDir = self.settings.value("backgroundDir", expanduser("~")) - - fileName = QtGui.QFileDialog.getOpenFileName(self.window, - "Open Background Image", backgroundDir, "Image Files (*.jpg *.png);; Video Files (*.mp4)"); - - if not fileName == "": - self.settings.setValue("backgroundDir", os.path.dirname(fileName)) - self.window.lineEdit_background.setText(fileName) - self.drawPreview() - def stopVideo(self): print('stop') self.videoWorker.cancel() @@ -291,8 +279,7 @@ class Main(QtCore.QObject): self.videoWorker.imageCreated.connect(self.showPreviewImage) self.videoWorker.encoding.connect(self.changeEncodingStatus) self.videoThread.start() - self.videoTask.emit(self.window.lineEdit_background.text(), - self.window.lineEdit_audioFile.text(), + self.videoTask.emit(self.window.lineEdit_audioFile.text(), self.window.lineEdit_outputFile.text(), self.selectedComponents) else: @@ -323,10 +310,6 @@ class Main(QtCore.QObject): self.window.pushButton_savePreset.setEnabled(False) self.window.pushButton_openProject.setEnabled(False) self.window.listWidget_componentList.setEnabled(False) - - self.window.label_background.setEnabled(False) - self.window.lineEdit_background.setEnabled(False) - self.window.toolButton_selectBackground.setEnabled(False) else: self.window.pushButton_createVideo.setEnabled(True) self.window.pushButton_Cancel.setEnabled(False) @@ -349,12 +332,6 @@ class Main(QtCore.QObject): self.window.pushButton_openProject.setEnabled(True) self.window.listWidget_componentList.setEnabled(True) - self.window.label_background.setEnabled(True) - self.window.lineEdit_background.setEnabled(True) - self.window.toolButton_selectBackground.setEnabled(True) - - - def progressBarSetText(self, value): self.window.progressBar_createVideo.setFormat(value) @@ -370,7 +347,7 @@ class Main(QtCore.QObject): self.drawPreview() def drawPreview(self): - self.newTask.emit(self.window.lineEdit_background.text(), self.selectedComponents) + self.newTask.emit(self.selectedComponents) # self.processTask.emit() self.autosave() @@ -381,7 +358,7 @@ class Main(QtCore.QObject): def findComponents(): srcPath = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'components') if os.path.exists(srcPath): - for f in os.listdir(srcPath): + for f in sorted(os.listdir(srcPath)): name, ext = os.path.splitext(f) if name.startswith("__"): continue @@ -507,7 +484,7 @@ class Main(QtCore.QObject): if self.window.comboBox_openPreset.itemText(i) == filename: self.window.comboBox_openPreset.removeItem(i) with open(filepath, 'w') as f: - f.write(core.Core.sortedStringDict(saveValueStore)) + f.write(core.Core.stringOrderedDict(saveValueStore)) self.window.comboBox_openPreset.addItem(filename) self.window.comboBox_openPreset.setCurrentIndex(self.window.comboBox_openPreset.count()-1) @@ -550,12 +527,13 @@ class Main(QtCore.QObject): if not filepath.endswith(".avp"): filepath += '.avp' with open(filepath, 'w') as f: + print('creating %s' % filepath) f.write('[Components]\n') for comp in self.selectedComponents: saveValueStore = comp.savePreset() f.write('%s\n' % str(comp)) f.write('%s\n' % str(comp.version())) - f.write('%s\n' % core.Core.sortedStringDict(saveValueStore)) + f.write('%s\n' % core.Core.stringOrderedDict(saveValueStore)) if filepath != self.autosavePath: self.settings.setValue("projectDir", os.path.dirname(filepath)) self.settings.setValue("currentProject", filepath) @@ -604,14 +582,18 @@ class Main(QtCore.QObject): saveValueStore = dict(eval(line)) self.selectedComponents[-1].loadPreset(saveValueStore) i = 0 - except: + except (IndexError, ValueError, KeyError, NameError, SyntaxError, AttributeError, TypeError) as e: self.clear() - self.showMessage("Project file '%s' is corrupted." % filepath) + typ, value, _ = sys.exc_info() + msg = '%s: %s' % (typ.__name__, value) + self.showMessage("Project file '%s' is corrupted." % filepath, False, + QtGui.QMessageBox.Warning, msg) - def showMessage(self, string, showCancel=False, icon=QtGui.QMessageBox.Information): + def showMessage(self, string, showCancel=False, icon=QtGui.QMessageBox.Information, detail=None): msg = QtGui.QMessageBox() msg.setIcon(icon) msg.setText(string) + msg.setDetailedText(detail) if showCancel: msg.setStandardButtons(QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel) else: diff --git a/mainwindow.ui b/mainwindow.ui index f9e8f5e..b703997 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -6,8 +6,8 @@ 0 0 - 1008 - 575 + 1028 + 592 @@ -421,73 +421,6 @@ - - - - - - - 0 - 0 - - - - - 85 - 0 - - - - Background - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - 0 - 28 - - - - - 16777215 - 28 - - - - - - - - - 0 - 28 - - - - - 16777215 - 28 - - - - ... - - - - - @@ -621,7 +554,6 @@ - diff --git a/preview_thread.py b/preview_thread.py index 63d1ac5..5116707 100644 --- a/preview_thread.py +++ b/preview_thread.py @@ -22,10 +22,9 @@ class Worker(QtCore.QObject): @pyqtSlot(str, list) - def createPreviewImage(self, backgroundImage, components): + def createPreviewImage(self, components): # print('worker thread id: {}'.format(QtCore.QThread.currentThreadId())) dic = { - "backgroundImage": backgroundImage, "components": components, } self.queue.put(dic) @@ -40,25 +39,14 @@ class Worker(QtCore.QObject): except Empty: continue - bgImage = self.core.parseBaseImage(\ - nextPreviewInformation["backgroundImage"], - preview=True - ) - if bgImage == []: - bgImage = '' - else: - bgImage = bgImage[0] - - im = self.core.drawBaseImage(bgImage) width = int(self.core.settings.value('outputWidth')) height = int(self.core.settings.value('outputHeight')) - frame = Image.new("RGBA", (width, height),(0,0,0,255)) - frame.paste(im) + frame = Image.new("RGBA", (width, height),(0,0,0,0)) components = nextPreviewInformation["components"] for component in reversed(components): - newFrame = Image.alpha_composite(frame,component.previewRender(self)) - frame = Image.alpha_composite(frame,newFrame) + #newFrame = Image.alpha_composite(frame,) + frame = Image.alpha_composite(frame,component.previewRender(self)) self._image = ImageQt(frame) self.imageCreated.emit(QtGui.QImage(self._image)) diff --git a/video_thread.py b/video_thread.py index c97cc24..e74fffa 100644 --- a/video_thread.py +++ b/video_thread.py @@ -38,16 +38,17 @@ class Worker(QtCore.QObject): while not self.stopped: i = self.compositeQueue.get() - if self.imBackground is not None: - frame = self.imBackground - else: - frame = self.getBackgroundAtIndex(i[1]) + frame = Image.new( + "RGBA", + (self.width, self.height), + (0, 0, 0, 0) + ) for compNo, comp in reversed(list(enumerate(self.components))): if compNo in self.staticComponents and self.staticComponents[compNo] != None: frame = Image.alpha_composite(frame, self.staticComponents[compNo]) else: - frame = Image.alpha_composite(frame, comp.frameRender(compNo, i[0])) + frame = Image.alpha_composite(frame, comp.frameRender(compNo, i[0], i[1])) # frame.paste(compFrame, mask=compFrame) @@ -59,10 +60,8 @@ class Worker(QtCore.QObject): for i in range(0, len(self.completeAudioArray), self.sampleSize): self.compositeQueue.put([i, self.bgI]) - if not self.imBackground: - # increment background video frame for next iteration - if self.bgI < len(self.backgroundFrames)-1: - self.bgI += 1 + # increment tracked video frame for next iteration + self.bgI += 1 def previewDispatch(self): while not self.stopped: @@ -74,39 +73,18 @@ class Worker(QtCore.QObject): self.previewQueue.task_done() - def getBackgroundAtIndex(self, i): - background = Image.new( - "RGBA", - (self.width, self.height), - (0, 0, 0, 255) - ) - layer = self.core.drawBaseImage(self.backgroundFrames[i]) - background.paste(layer) - return background - - @pyqtSlot(str, str, str, list) - def createVideo(self, backgroundImage, inputFile, outputFile, components): + @pyqtSlot(str, str, list) + def createVideo(self, inputFile, outputFile, components): self.encoding.emit(True) self.components = components self.outputFile = outputFile + self.bgI = 0 # tracked video frame self.reset() self.width = int(self.core.settings.value('outputWidth')) self.height = int(self.core.settings.value('outputHeight')) # print('worker thread id: {}'.format(QtCore.QThread.currentThreadId())) progressBarValue = 0 self.progressBarUpdate.emit(progressBarValue) - self.progressBarSetText.emit('Loading background imageā€¦') - - self.backgroundImage = backgroundImage - - self.backgroundFrames = self.core.parseBaseImage(backgroundImage) - if len(self.backgroundFrames) < 2: - # the base image is not a video so we can draw it now - self.imBackground = self.getBackgroundAtIndex(0) - else: - # base images will be drawn while drawing the audio bars - self.imBackground = None - self.bgI = 0 self.progressBarSetText.emit('Loading audio file...') self.completeAudioArray = self.core.readAudioFile(inputFile, self) @@ -165,7 +143,7 @@ class Worker(QtCore.QObject): ) if properties and 'static' in properties: - self.staticComponents[compNo] = copy(comp.frameRender(compNo, 0)) + self.staticComponents[compNo] = copy(comp.frameRender(compNo, 0, 0)) self.progressBarUpdate.emit(100) self.compositeQueue = Queue() From 277e86f2795093deaa12f294c29ac2f951ae65cd Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 4 Jun 2017 20:27:43 -0400 Subject: [PATCH 5/9] not dumping frames anymore, but not working yet either will finish later --- components/video.py | 69 ++++++++++++++++++++++++++++----------------- core.py | 5 ++-- main.py | 2 +- video_thread.py | 1 - 4 files changed, 47 insertions(+), 30 deletions(-) diff --git a/components/video.py b/components/video.py index 1a9f344..97b824c 100644 --- a/components/video.py +++ b/components/video.py @@ -30,11 +30,11 @@ class Component(__base__.Component): def previewRender(self, previewWorker): self.width = int(previewWorker.core.settings.value('outputWidth')) self.height = int(previewWorker.core.settings.value('outputHeight')) - frames = self.getVideoFrames(True) - if not hasattr(self, 'staticFrame') or not self.working and frames: + frame1 = self.getPreviewFrame() + if not hasattr(self, 'staticFrame') or not self.working and frame1: frame = Image.new("RGBA", (self.width, self.height), (0, 0, 0, 0)) - if frames: - im = Image.open(frames[0]) + if frame1: + im = Image.open(frame1) im = self.resize(im) frame.paste(im) if not self.working: @@ -77,39 +77,56 @@ class Component(__base__.Component): self.settings.setValue("backgroundDir", os.path.dirname(filename)) self.page.lineEdit_video.setText(filename) self.update() - - def getVideoFrames(self, preview=False): - # recreate the temporary directory so it is empty - # FIXME: don't dump all the frames at once, don't dump more than sound length - # FIXME: make cancellable, report status to user, etc etc etc + + def getPreviewFrame(self): if not self.videoPath: return name = os.path.basename(self.videoPath).split('.', 1)[0] - if preview: - filename = 'preview%s.jpg' % name - if os.path.exists(os.path.join(self.parent.core.tempDir, filename)): - return False - else: - filename = name+'-frame%05d.jpg' - - # recreate tempDir and dump needed frame(s) - self.parent.core.deleteTempDir() - os.mkdir(self.parent.core.tempDir) - if preview: - options = '-ss 10 -vframes 1' - else: - options = '' #'-vframes 99999' + filename = 'preview%s.jpg' % name + if os.path.exists(os.path.join(self.parent.core.tempDir, filename)): + # no, we don't need a new preview frame + return False + + # get a preview frame subprocess.call( \ '%s -i "%s" -y %s "%s"' % ( \ self.parent.core.FFMPEG_BIN, self.videoPath, - options, + '-ss 10 -vframes 1', os.path.join(self.parent.core.tempDir, filename) ), shell=True ) - print('### Got Preview Frame From %s ###' % name if preview else '### Finished Dumping Frames From %s ###' % name) - return sorted([os.path.join(self.parent.core.tempDir, f) for f in os.listdir(self.parent.core.tempDir)]) + print('### Got Preview Frame From %s ###' % name) + return os.path.join(self.parent.core.tempDir, filename) + + def getVideoFrames(self): + # FIXME: make cancellable, report status to user, etc etc etc + if not self.videoPath: + return + + command = [ + self.parent.core.FFMPEG_BIN, + '-i', self.videoPath, + '-f', 'image2pipe', + '-vcodec', 'rawvideo', '-', + '-pix_fmt', 'rgba', + ] + + # pipe in video frames from ffmpeg + in_pipe = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=8**10) + # maybe bufsize=4*self.width*self.height+100 ? + chunk = 4*self.width*self.height + + frames = [] + while True: + byteFrame = in_pipe.stdout.read(chunk) + if len(byteFrame) == 0: + break + img = Image.frombytes('RGBA', (self.width, self.height), byteFrame, 'raw', 'RGBa') + frames.append(img) + + return frames def resize(self, im): if im.size != (self.width, self.height): diff --git a/core.py b/core.py index ecbf12c..99403f1 100644 --- a/core.py +++ b/core.py @@ -15,6 +15,8 @@ class Core(): def __init__(self): self.FFMPEG_BIN = self.findFfmpeg() self.tempDir = os.path.join(tempfile.gettempdir(), 'audio-visualizer-python-data') + if not os.path.exists(self.tempDir): + os.makedirs(self.tempDir) atexit.register(self.deleteTempDir) def findFfmpeg(self): @@ -94,8 +96,7 @@ class Core(): def deleteTempDir(self): try: - if os.path.exists(self.tempDir): - rmtree(self.tempDir) + rmtree(self.tempDir) except FileNotFoundError: pass diff --git a/main.py b/main.py index da72b1e..637ece8 100644 --- a/main.py +++ b/main.py @@ -253,7 +253,7 @@ class Main(QtCore.QObject): outputDir = self.settings.value("outputDir", expanduser("~")) fileName = QtGui.QFileDialog.getSaveFileName(self.window, - "Set Output Video File", outputDir, "Video Files (*.mkv)"); + "Set Output Video File", outputDir, "Video Files (*.mkv *.mp4)"); if not fileName == "": self.settings.setValue("outputDir", os.path.dirname(fileName)) diff --git a/video_thread.py b/video_thread.py index e74fffa..0542bc2 100644 --- a/video_thread.py +++ b/video_thread.py @@ -228,7 +228,6 @@ class Worker(QtCore.QObject): self.error = False self.canceled = False self.parent.drawPreview() - self.core.deleteTempDir() self.stopped = True self.encoding.emit(False) self.videoCreated.emit() From e58a1d0b2d499aca72af8472ec33c02580d2dffd Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 4 Jun 2017 22:57:19 -0400 Subject: [PATCH 6/9] frames are taken straight from the in_pipe --- components/video.py | 49 ++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/components/video.py b/components/video.py index 97b824c..b009baa 100644 --- a/components/video.py +++ b/components/video.py @@ -1,6 +1,7 @@ from PIL import Image, ImageDraw from PyQt4 import uic, QtGui, QtCore import os, subprocess +import numpy from . import __base__ class Component(__base__.Component): @@ -35,6 +36,7 @@ class Component(__base__.Component): frame = Image.new("RGBA", (self.width, self.height), (0, 0, 0, 0)) if frame1: im = Image.open(frame1) + self.realSize = im.size im = self.resize(im) frame.paste(im) if not self.working: @@ -45,20 +47,26 @@ class Component(__base__.Component): super().preFrameRender(**kwargs) self.width = int(self.worker.core.settings.value('outputWidth')) self.height = int(self.worker.core.settings.value('outputHeight')) - self.frames = self.getVideoFrames() self.working = True + self.frames = self.getVideoFrames() def frameRender(self, moduleNo, arrayNo, frameNo): - print(frameNo) - try: - if frameNo < len(self.frames)-1: - self.staticFrame = Image.new("RGBA", (self.width, self.height), (0, 0, 0, 0)) - im = Image.open(self.frames[frameNo]) - im = self.resize(im) - self.staticFrame.paste(im) - except FileNotFoundError: - print("Video component encountered an error") - self.frames = [] + # don't make a new frame + if not self.working: + return self.staticFrame + byteFrame = self.frames.stdout.read(self.chunkSize) + if len(byteFrame) == 0: + self.working = False + self.frames.kill() + return self.staticFrame + + # make a new frame + width, height = self.realSize + image = numpy.fromstring(byteFrame, dtype='uint8') + image = image.reshape((width, height, 4)) + image = Image.frombytes('RGBA', (width, height), image, 'raw', 'RGBa') + image = self.resize(image) + self.staticFrame = image return self.staticFrame def loadPreset(self, pr): @@ -101,7 +109,6 @@ class Component(__base__.Component): return os.path.join(self.parent.core.tempDir, filename) def getVideoFrames(self): - # FIXME: make cancellable, report status to user, etc etc etc if not self.videoPath: return @@ -109,24 +116,16 @@ class Component(__base__.Component): self.parent.core.FFMPEG_BIN, '-i', self.videoPath, '-f', 'image2pipe', - '-vcodec', 'rawvideo', '-', '-pix_fmt', 'rgba', + '-vcodec', 'rawvideo', '-', ] # pipe in video frames from ffmpeg - in_pipe = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=8**10) - # maybe bufsize=4*self.width*self.height+100 ? - chunk = 4*self.width*self.height - - frames = [] - while True: - byteFrame = in_pipe.stdout.read(chunk) - if len(byteFrame) == 0: - break - img = Image.frombytes('RGBA', (self.width, self.height), byteFrame, 'raw', 'RGBa') - frames.append(img) + in_pipe = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8) + width, height = self.realSize + self.chunkSize = 4*width*height - return frames + return in_pipe def resize(self, im): if im.size != (self.width, self.height): From be18deece5843ac8d2c7af64704e3fb360a05a25 Mon Sep 17 00:00:00 2001 From: DH4 Date: Mon, 5 Jun 2017 04:54:58 -0500 Subject: [PATCH 7/9] Performance Tuning. FIXME: Video component frames are rendered out of order. Video component creates a severe performance bottleneck. --- components/video.py | 21 ++++++++++----------- main.py | 4 ++-- video_thread.py | 20 ++++++++++---------- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/components/video.py b/components/video.py index b009baa..422b952 100644 --- a/components/video.py +++ b/components/video.py @@ -36,8 +36,6 @@ class Component(__base__.Component): frame = Image.new("RGBA", (self.width, self.height), (0, 0, 0, 0)) if frame1: im = Image.open(frame1) - self.realSize = im.size - im = self.resize(im) frame.paste(im) if not self.working: self.staticFrame = frame @@ -61,11 +59,9 @@ class Component(__base__.Component): return self.staticFrame # make a new frame - width, height = self.realSize - image = numpy.fromstring(byteFrame, dtype='uint8') - image = image.reshape((width, height, 4)) - image = Image.frombytes('RGBA', (width, height), image, 'raw', 'RGBa') - image = self.resize(image) + width = self.width + height = self.height + image = Image.frombytes('RGBA', (width, height), byteFrame) self.staticFrame = image return self.staticFrame @@ -80,7 +76,7 @@ class Component(__base__.Component): def pickVideo(self): imgDir = self.settings.value("backgroundDir", os.path.expanduser("~")) filename = QtGui.QFileDialog.getOpenFileName(self.page, - "Choose Video", imgDir, "Video Files (*.mp4)") + "Choose Video", imgDir, "Video Files (*.mp4 *.mov)") if filename: self.settings.setValue("backgroundDir", os.path.dirname(filename)) self.page.lineEdit_video.setText(filename) @@ -97,10 +93,11 @@ class Component(__base__.Component): # get a preview frame subprocess.call( \ - '%s -i "%s" -y %s "%s"' % ( \ + '%s -i "%s" -y %s %s "%s"' % ( \ self.parent.core.FFMPEG_BIN, self.videoPath, '-ss 10 -vframes 1', + '-filter:v scale='+str(self.width)+':'+str(self.height), os.path.join(self.parent.core.tempDir, filename) ), shell=True @@ -114,16 +111,18 @@ class Component(__base__.Component): command = [ self.parent.core.FFMPEG_BIN, + '-thread_queue_size', '512', '-i', self.videoPath, '-f', 'image2pipe', '-pix_fmt', 'rgba', + '-filter:v', 'scale='+str(self.width)+':'+str(self.height), '-vcodec', 'rawvideo', '-', ] # pipe in video frames from ffmpeg in_pipe = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8) - width, height = self.realSize - self.chunkSize = 4*width*height + #width, height = self.realSize + self.chunkSize = 4*self.width*self.height return in_pipe diff --git a/main.py b/main.py index 637ece8..36fc989 100644 --- a/main.py +++ b/main.py @@ -243,7 +243,7 @@ class Main(QtCore.QObject): inputDir = self.settings.value("inputDir", expanduser("~")) fileName = QtGui.QFileDialog.getOpenFileName(self.window, - "Open Music File", inputDir, "Music Files (*.mp3 *.wav *.ogg *.flac)"); + "Open Music File", inputDir, "Music Files (*.mp3 *.wav *.ogg *.fla *.aac)"); if not fileName == "": self.settings.setValue("inputDir", os.path.dirname(fileName)) @@ -253,7 +253,7 @@ class Main(QtCore.QObject): outputDir = self.settings.value("outputDir", expanduser("~")) fileName = QtGui.QFileDialog.getSaveFileName(self.window, - "Set Output Video File", outputDir, "Video Files (*.mkv *.mp4)"); + "Set Output Video File", outputDir, "Video Files (*.mp4 *.mov *.mkv *.avi *.webm *.flv)"); if not fileName == "": self.settings.setValue("outputDir", os.path.dirname(fileName)) diff --git a/video_thread.py b/video_thread.py index 0542bc2..ac4162c 100644 --- a/video_thread.py +++ b/video_thread.py @@ -37,20 +37,19 @@ class Worker(QtCore.QObject): def renderNode(self): while not self.stopped: i = self.compositeQueue.get() - - frame = Image.new( - "RGBA", - (self.width, self.height), - (0, 0, 0, 0) - ) + frame = None for compNo, comp in reversed(list(enumerate(self.components))): if compNo in self.staticComponents and self.staticComponents[compNo] != None: - frame = Image.alpha_composite(frame, self.staticComponents[compNo]) + if frame is None: + frame = self.staticComponents[compNo] + else: + frame = Image.alpha_composite(frame, self.staticComponents[compNo]) else: - frame = Image.alpha_composite(frame, comp.frameRender(compNo, i[0], i[1])) - - # frame.paste(compFrame, mask=compFrame) + if frame is None: + frame = comp.frameRender(compNo, i[0], i[1]) + else: + frame = Image.alpha_composite(frame, comp.frameRender(compNo, i[0], i[1])) self.renderQueue.put([i[0], frame]) self.compositeQueue.task_done() @@ -98,6 +97,7 @@ class Worker(QtCore.QObject): ffmpegCommand = [ self.core.FFMPEG_BIN, + '-thread_queue_size', '512', '-y', # (optional) means overwrite the output file if it already exists. '-f', 'rawvideo', '-vcodec', 'rawvideo', From 47509ae2b19e5a05211ae06114ba4675aa60a793 Mon Sep 17 00:00:00 2001 From: tassaron Date: Tue, 6 Jun 2017 01:40:26 -0400 Subject: [PATCH 8/9] added framebuffer to keep frames in order NOT WORKING: end of video detection --- components/video.py | 144 +++++++++++++++++++++----------------------- main.py | 13 ++-- 2 files changed, 76 insertions(+), 81 deletions(-) diff --git a/components/video.py b/components/video.py index 422b952..0ae5348 100644 --- a/components/video.py +++ b/components/video.py @@ -1,15 +1,56 @@ from PIL import Image, ImageDraw from PyQt4 import uic, QtGui, QtCore -import os, subprocess -import numpy +import os, subprocess, threading +from queue import PriorityQueue from . import __base__ +class Video: + '''Video Component Frame-Fetcher''' + def __init__(self, ffmpeg, videoPath, width, height, frameRate, chunkSize, parent): + self.parent = parent + self.chunkSize = chunkSize + self.size = (width, height) + self.frameNo = -1 + self.command = [ + ffmpeg, + '-thread_queue_size', '512', + '-r', frameRate, + '-i', videoPath, + '-f', 'image2pipe', + '-pix_fmt', 'rgba', + '-filter:v', 'scale='+str(width)+':'+str(height), + '-vcodec', 'rawvideo', '-', + ] + + self.frameBuffer = PriorityQueue() + self.frameBuffer.maxsize = int(frameRate) + self.finishedFrames = {} + + self.thread = threading.Thread(target=self.fillBuffer, name=self.__doc__) + self.thread.daemon = True + self.thread.start() + + def frame(self, num): + while True: + if num in self.finishedFrames: + image = self.finishedFrames.pop(num) + return Image.frombytes('RGBA', self.size, image) + i, image = self.frameBuffer.get() + self.finishedFrames[i] = image + self.frameBuffer.task_done() + + def fillBuffer(self): + self.pipe = subprocess.Popen(self.command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8) + while True: + if self.parent.canceled: + break + self.frameNo += 1 + image = self.pipe.stdout.read(self.chunkSize) + print('creating frame #%s' % str(self.frameNo)) + self.frameBuffer.put((self.frameNo, image)) + class Component(__base__.Component): '''Video''' - def __init__(self): - super().__init__() - self.working = False - def widget(self, parent): self.parent = parent self.settings = parent.settings @@ -29,41 +70,22 @@ class Component(__base__.Component): self.parent.drawPreview() def previewRender(self, previewWorker): - self.width = int(previewWorker.core.settings.value('outputWidth')) - self.height = int(previewWorker.core.settings.value('outputHeight')) - frame1 = self.getPreviewFrame() - if not hasattr(self, 'staticFrame') or not self.working and frame1: - frame = Image.new("RGBA", (self.width, self.height), (0, 0, 0, 0)) - if frame1: - im = Image.open(frame1) - frame.paste(im) - if not self.working: - self.staticFrame = frame - return self.staticFrame + width = int(previewWorker.core.settings.value('outputWidth')) + height = int(previewWorker.core.settings.value('outputHeight')) + self.chunkSize = 4*width*height + return self.getPreviewFrame(width, height) def preFrameRender(self, **kwargs): super().preFrameRender(**kwargs) - self.width = int(self.worker.core.settings.value('outputWidth')) - self.height = int(self.worker.core.settings.value('outputHeight')) - self.working = True - self.frames = self.getVideoFrames() + width = int(self.worker.core.settings.value('outputWidth')) + height = int(self.worker.core.settings.value('outputHeight')) + self.chunkSize = 4*width*height + self.video = Video(self.parent.core.FFMPEG_BIN, self.videoPath, + width, height, self.settings.value("outputFrameRate"), + self.chunkSize, self.parent) def frameRender(self, moduleNo, arrayNo, frameNo): - # don't make a new frame - if not self.working: - return self.staticFrame - byteFrame = self.frames.stdout.read(self.chunkSize) - if len(byteFrame) == 0: - self.working = False - self.frames.kill() - return self.staticFrame - - # make a new frame - width = self.width - height = self.height - image = Image.frombytes('RGBA', (width, height), byteFrame) - self.staticFrame = image - return self.staticFrame + return self.video.frame(frameNo) def loadPreset(self, pr): self.page.lineEdit_video.setText(pr['video']) @@ -82,51 +104,21 @@ class Component(__base__.Component): self.page.lineEdit_video.setText(filename) self.update() - def getPreviewFrame(self): - if not self.videoPath: - return - name = os.path.basename(self.videoPath).split('.', 1)[0] - filename = 'preview%s.jpg' % name - if os.path.exists(os.path.join(self.parent.core.tempDir, filename)): - # no, we don't need a new preview frame - return False - - # get a preview frame - subprocess.call( \ - '%s -i "%s" -y %s %s "%s"' % ( \ - self.parent.core.FFMPEG_BIN, - self.videoPath, - '-ss 10 -vframes 1', - '-filter:v scale='+str(self.width)+':'+str(self.height), - os.path.join(self.parent.core.tempDir, filename) - ), - shell=True - ) - print('### Got Preview Frame From %s ###' % name) - return os.path.join(self.parent.core.tempDir, filename) - - def getVideoFrames(self): - if not self.videoPath: - return - + def getPreviewFrame(self, width, height): command = [ self.parent.core.FFMPEG_BIN, '-thread_queue_size', '512', '-i', self.videoPath, '-f', 'image2pipe', '-pix_fmt', 'rgba', - '-filter:v', 'scale='+str(self.width)+':'+str(self.height), + '-filter:v', 'scale='+str(width)+':'+str(height), '-vcodec', 'rawvideo', '-', + '-ss', '90', + '-vframes', '1', ] - - # pipe in video frames from ffmpeg - in_pipe = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8) - #width, height = self.realSize - self.chunkSize = 4*self.width*self.height - - return in_pipe - - def resize(self, im): - if im.size != (self.width, self.height): - im = im.resize((self.width, self.height), Image.ANTIALIAS) - return im + pipe = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8) + byteFrame = pipe.stdout.read(self.chunkSize) + image = Image.frombytes('RGBA', (width, height), byteFrame) + pipe.stdout.close() + pipe.kill() + return image diff --git a/main.py b/main.py index 36fc989..c75a7f7 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,4 @@ -import sys, io, os, shutil, atexit, string, signal, filecmp +import sys, io, os, shutil, atexit, string, signal, filecmp, time from os.path import expanduser from queue import Queue from importlib import import_module @@ -145,6 +145,7 @@ class Main(QtCore.QObject): self.core = core.Core() self.pages = [] self.selectedComponents = [] + self.lastAutosave = time.time() # create data directory, load/create settings self.dataDir = QDesktopServices.storageLocation(QDesktopServices.DataLocation) @@ -235,9 +236,11 @@ class Main(QtCore.QObject): self.autosave() def autosave(self): - if os.path.exists(self.autosavePath): - os.remove(self.autosavePath) - self.createProjectFile(self.autosavePath) + if time.time() - self.lastAutosave >= 1.0: + if os.path.exists(self.autosavePath): + os.remove(self.autosavePath) + self.createProjectFile(self.autosavePath) + self.lastAutosave = time.time() def openInputFileDialog(self): inputDir = self.settings.value("inputDir", expanduser("~")) @@ -423,7 +426,7 @@ class Main(QtCore.QObject): def moveComponentDown(self): row = self.window.listWidget_componentList.currentRow() - if row < len(self.pages) + 1: + if row != -1 and row < len(self.pages)+1: module = self.selectedComponents[row] self.selectedComponents.pop(row) self.selectedComponents.insert(row + 1,module) From 7946e98f222784e25ea9c6dc00713f431e238609 Mon Sep 17 00:00:00 2001 From: DH4 Date: Tue, 6 Jun 2017 01:57:48 -0500 Subject: [PATCH 9/9] When out of frames, send last frame to buffer. Added ability to loop video. --- components/video.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/components/video.py b/components/video.py index 0ae5348..3162279 100644 --- a/components/video.py +++ b/components/video.py @@ -6,15 +6,21 @@ from . import __base__ class Video: '''Video Component Frame-Fetcher''' - def __init__(self, ffmpeg, videoPath, width, height, frameRate, chunkSize, parent): + def __init__(self, ffmpeg, videoPath, width, height, frameRate, chunkSize, parent, loopVideo): self.parent = parent self.chunkSize = chunkSize self.size = (width, height) self.frameNo = -1 + self.currentFrame = 'None' + if loopVideo: + self.loopValue = '-1' + else: + self.loopValue = '0' self.command = [ ffmpeg, '-thread_queue_size', '512', '-r', frameRate, + '-stream_loop', self.loopValue, '-i', videoPath, '-f', 'image2pipe', '-pix_fmt', 'rgba', @@ -45,9 +51,17 @@ class Video: if self.parent.canceled: break self.frameNo += 1 - image = self.pipe.stdout.read(self.chunkSize) - print('creating frame #%s' % str(self.frameNo)) - self.frameBuffer.put((self.frameNo, image)) + + # If we run out of frames, use the last good frame and loop. + if len(self.currentFrame) == 0: + self.frameBuffer.put((self.frameNo-1, self.lastFrame)) + continue + + self.currentFrame = self.pipe.stdout.read(self.chunkSize) + #print('creating frame #%s' % str(self.frameNo)) + if len(self.currentFrame) != 0: + self.frameBuffer.put((self.frameNo, self.currentFrame)) + self.lastFrame = self.currentFrame class Component(__base__.Component): '''Video''' @@ -58,15 +72,18 @@ class Component(__base__.Component): self.videoPath = '' self.x = 0 self.y = 0 + self.loopVideo = False page.lineEdit_video.textChanged.connect(self.update) page.pushButton_video.clicked.connect(self.pickVideo) + page.checkBox_loop.stateChanged.connect(self.update) self.page = page return page def update(self): self.videoPath = self.page.lineEdit_video.text() + self.loopVideo = self.page.checkBox_loop.isChecked() self.parent.drawPreview() def previewRender(self, previewWorker): @@ -82,7 +99,7 @@ class Component(__base__.Component): self.chunkSize = 4*width*height self.video = Video(self.parent.core.FFMPEG_BIN, self.videoPath, width, height, self.settings.value("outputFrameRate"), - self.chunkSize, self.parent) + self.chunkSize, self.parent, self.loopVideo) def frameRender(self, moduleNo, arrayNo, frameNo): return self.video.frame(frameNo)