Skip to content

Commit 1914454

Browse files
authored
Merge pull request #3416 from OpenShot/blender-stdout-encoding
Fix Blender subprocess pipe decoding
2 parents 27aee18 + fcefc20 commit 1914454

File tree

1 file changed

+109
-109
lines changed

1 file changed

+109
-109
lines changed

src/windows/views/blender_listview.py

+109-109
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,14 @@
3333
import xml.dom.minidom as xml
3434
import functools
3535

36-
from PyQt5.QtCore import QSize, Qt, QEvent, QObject, QThread, pyqtSlot, pyqtSignal, QMetaObject, Q_ARG, QTimer
37-
from PyQt5.QtGui import *
38-
from PyQt5.QtWidgets import *
36+
from PyQt5.QtCore import (
37+
Qt, QObject, pyqtSlot, pyqtSignal, QMetaObject, Q_ARG, QThread, QTimer, QSize,
38+
)
39+
from PyQt5.QtWidgets import (
40+
QApplication, QListView, QMessageBox, QColorDialog,
41+
QComboBox, QDoubleSpinBox, QLabel, QPushButton, QLineEdit, QPlainTextEdit,
42+
)
43+
from PyQt5.QtGui import QColor, QImage, QPixmap
3944

4045
from classes import info
4146
from classes.logger import log
@@ -44,11 +49,9 @@
4449
from classes.app import get_app
4550
from windows.models.blender_model import BlenderModel
4651

47-
import json
48-
4952

5053
class BlenderListView(QListView):
51-
""" A TreeView QWidget used on the animated title window """
54+
""" A ListView QWidget used on the animated title window """
5255

5356
def currentChanged(self, selected, deselected):
5457
# Get selected item
@@ -74,7 +77,7 @@ def currentChanged(self, selected, deselected):
7477
self.generateUniqueFolder()
7578

7679
# Loop through params
77-
for param in animation.get("params",[]):
80+
for param in animation.get("params", []):
7881
log.info('Using parameter %s: %s' % (param["name"], param["title"]))
7982

8083
# Is Hidden Param?
@@ -295,7 +298,6 @@ def init_slider_values(self):
295298
def btnRefresh_clicked(self, checked):
296299

297300
# Render current frame
298-
preview_frame_number = self.win.sliderPreview.value()
299301
self.preview_timer.start()
300302

301303
@pyqtSlot()
@@ -360,62 +362,40 @@ def get_animation_details(self):
360362

361363
# Get list of params
362364
animation = {"title": animation_title, "path": xml_path, "service": service, "params": []}
363-
xml_params = xmldoc.getElementsByTagName("param")
364365

365366
# Loop through params
366-
for param in xml_params:
367-
param_item = {}
367+
for param in xmldoc.getElementsByTagName("param"):
368+
# Set up item dict, "default" key is required
369+
param_item = {"default": ""}
368370

369371
# Get details of param
370-
if param.attributes["title"]:
371-
param_item["title"] = param.attributes["title"].value
372-
373-
if param.attributes["description"]:
374-
param_item["description"] = param.attributes["description"].value
375-
376-
if param.attributes["name"]:
377-
param_item["name"] = param.attributes["name"].value
378-
379-
if param.attributes["type"]:
380-
param_item["type"] = param.attributes["type"].value
381-
382-
if param.getElementsByTagName("min"):
383-
param_item["min"] = param.getElementsByTagName("min")[0].childNodes[0].data
384-
385-
if param.getElementsByTagName("max"):
386-
param_item["max"] = param.getElementsByTagName("max")[0].childNodes[0].data
372+
for att in ["title", "description", "name", "type"]:
373+
if param.attributes[att]:
374+
param_item[att] = param.attributes[att].value
387375

388-
if param.getElementsByTagName("step"):
389-
param_item["step"] = param.getElementsByTagName("step")[0].childNodes[0].data
376+
for tag in ["min", "max", "step", "digits", "default"]:
377+
for p in param.getElementsByTagName(tag):
378+
if p.childNodes:
379+
param_item[tag] = p.firstChild.data
390380

391-
if param.getElementsByTagName("digits"):
392-
param_item["digits"] = param.getElementsByTagName("digits")[0].childNodes[0].data
393-
394-
if param.getElementsByTagName("default"):
395-
if param.getElementsByTagName("default")[0].childNodes:
396-
param_item["default"] = param.getElementsByTagName("default")[0].childNodes[0].data
397-
else:
398-
param_item["default"] = ""
399-
400-
param_item["values"] = {}
401-
values = param.getElementsByTagName("value")
402-
for value in values:
403-
# Get list of values
404-
name = ""
405-
num = ""
406-
407-
if value.attributes["name"]:
408-
name = value.attributes["name"].value
409-
410-
if value.attributes["num"]:
411-
num = value.attributes["num"].value
412-
413-
# add to parameter
414-
param_item["values"][name] = num
381+
try:
382+
# Build values dict from list of (name, num) tuples
383+
param_item["values"] = dict([
384+
(p.attributes["name"].value, p.attributes["num"].value)
385+
for p in param.getElementsByTagName("value") if (
386+
"name" in p.attributes and "num" in p.attributes
387+
)
388+
])
389+
except (TypeError, AttributeError) as ex:
390+
log.warn("XML parser: {}".format(ex))
391+
pass
415392

416393
# Append param object to list
417394
animation["params"].append(param_item)
418395

396+
# Free up XML document memory
397+
xmldoc.unlink()
398+
419399
# Return animation dictionary
420400
return animation
421401

@@ -588,7 +568,7 @@ def Render(self, frame=None):
588568

589569
def __init__(self, *args):
590570
# Invoke parent init
591-
QTreeView.__init__(self, *args)
571+
super().__init__(*args)
592572

593573
# Get a reference to the window object
594574
self.app = get_app()
@@ -633,7 +613,6 @@ def __init__(self, *args):
633613
# Refresh view
634614
self.refresh_view()
635615

636-
637616
# Background Worker Thread (for Blender process)
638617
self.background = QThread(self)
639618
self.worker = Worker() # no parent!
@@ -695,20 +674,21 @@ def Render(self, blend_file_path, target_script, preview_mode=False):
695674
# Init regex expression used to determine blender's render progress
696675
s = settings.get_settings()
697676

677+
_ = get_app()._tr
678+
698679
# get the blender executable path
699680
self.blender_exec_path = s.get("blender_command")
700-
self.blender_frame_expression = re.compile(r"Fra:([0-9,]*).*Mem:(.*?) .*Sce:")
701-
self.blender_saved_expression = re.compile(r"Saved: '(.*.png)(.*)'")
702-
self.blender_version = re.compile(r"Blender (.*?) ")
703-
self.blend_file_path = blend_file_path
704-
self.target_script = target_script
705681
self.preview_mode = preview_mode
706682
self.frame_detected = False
683+
self.last_frame = 0
707684
self.version = None
708685
self.command_output = ""
709686
self.process = None
710-
self.is_running = True
711-
_ = get_app()._tr
687+
self.is_running = False
688+
689+
blender_frame_re = re.compile(r"Fra:([0-9,]*)")
690+
blender_saved_re = re.compile(r"Saved: '(.*\.png)")
691+
blender_version_re = re.compile(r"Blender (.*?) ")
712692

713693
startupinfo = None
714694
if sys.platform == 'win32':
@@ -718,79 +698,99 @@ def Render(self, blend_file_path, target_script, preview_mode=False):
718698
try:
719699
# Shell the blender command to create the image sequence
720700
command_get_version = [self.blender_exec_path, '-v']
721-
command_render = [self.blender_exec_path, '-b', self.blend_file_path, '-P', self.target_script]
701+
command_render = [self.blender_exec_path, '-b', blend_file_path, '-P', target_script]
722702

723703
# Check the version of Blender
724704
import shlex
725-
log.info("Checking Blender version, command: {}".format(" ".join([shlex.quote(x) for x in command_get_version])))
705+
log.info("Checking Blender version, command: {}".format(
706+
" ".join([shlex.quote(x) for x in command_get_version])))
726707

727-
self.process = subprocess.Popen(command_get_version, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=startupinfo, universal_newlines=True)
708+
proc = subprocess.Popen(
709+
command_get_version,
710+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
711+
startupinfo=startupinfo,
712+
)
728713

729714
# Check the version of Blender
730-
self.version = self.blender_version.findall(self.process.stdout.readline())
715+
try:
716+
# Give Blender up to 10 seconds to respond
717+
(out, err) = proc.communicate(timeout=10)
718+
except subprocess.TimeoutExpired:
719+
self.blender_error_nodata.emit()
720+
return
721+
722+
ver_string = out.decode('utf-8')
723+
ver_match = blender_version_re.search(ver_string)
724+
725+
if not ver_match:
726+
raise Exception("No Blender version detected in output")
731727

732-
if self.version:
733-
if self.version[0] < info.BLENDER_MIN_VERSION:
734-
# change cursor to "default" and stop running blender command
735-
self.is_running = False
728+
self.version = ver_match.group(1)
729+
log.info("Found Blender version {}".format(self.version))
736730

737-
# Wrong version of Blender.
738-
self.blender_version_error.emit(self.version[0])
739-
return
731+
if self.version < info.BLENDER_MIN_VERSION:
732+
# Wrong version of Blender.
733+
self.blender_version_error.emit(self.version)
734+
return
740735

741736
# debug info
742-
log.info("Running Blender, command: {}".format(" ".join([shlex.quote(x) for x in command_render])))
737+
log.info("Running Blender, command: {}".format(
738+
" ".join([shlex.quote(x) for x in command_render])))
743739
log.info("Blender output:")
744740

745741
# Run real command to render Blender project
746-
self.process = subprocess.Popen(command_render, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, startupinfo=startupinfo, universal_newlines=True)
747-
748-
except:
742+
proc = subprocess.Popen(
743+
command_render, bufsize=512,
744+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
745+
startupinfo=startupinfo,
746+
)
747+
self.process = proc
748+
self.is_running = True
749+
750+
except subprocess.SubprocessError:
749751
# Error running command. Most likely the blender executable path in
750752
# the settings is incorrect, or is not a supported Blender version
751753
self.is_running = False
752754
self.blender_error_nodata.emit()
755+
raise
756+
except Exception as ex:
757+
log.error("{}".format(ex))
753758
return
754759

755-
while self.is_running and self.process.poll() is None:
756-
757-
# Look for progress info in the Blender Output
758-
line = self.process.stdout.readline().strip()
760+
while self.is_running and proc.poll() is None:
761+
for outline in iter(proc.stdout.readline, b''):
762+
line = outline.decode('utf-8').strip()
759763

760-
# Skip blank lines
761-
if not line:
762-
continue
764+
# Skip blank output
765+
if not line:
766+
continue
763767

764-
# append all output into a variable, and log
765-
self.command_output = self.command_output + line + "\n"
766-
log.info(" {}".format(line))
768+
# append all output into a variable, and log
769+
self.command_output = self.command_output + line + "\n"
770+
log.info(" {}".format(line))
767771

768-
output_frame = self.blender_frame_expression.findall(line)
772+
# Look for progress info in the Blender Output
773+
output_frame = blender_frame_re.search(line)
774+
output_saved = blender_saved_re.search(line)
769775

770-
# Does it have a match?
771-
if output_frame:
772-
# Yes, we have a match
773-
self.frame_detected = True
774-
current_frame = output_frame[0][0]
775-
memory = output_frame[0][1]
776+
# Does it have a match?
777+
if output_frame or output_saved:
778+
# Yes, we have a match
779+
self.frame_detected = True
776780

777-
# Update progress bar
778-
if not self.preview_mode:
779-
# only update progress if in 'render' mode
780-
self.progress.emit(int(current_frame))
781+
if output_frame:
782+
current_frame = int(output_frame.group(1))
781783

782-
# Look for progress info in the Blender Output
783-
output_saved = self.blender_saved_expression.findall(line)
784+
# Update progress bar
785+
if current_frame != self.last_frame and not self.preview_mode:
786+
# update progress on frame change, if in 'render' mode
787+
self.progress.emit(current_frame)
784788

785-
# Does it have a match?
786-
if output_saved:
787-
# Yes, we have a match
788-
self.frame_detected = True
789-
image_path = output_saved[0][0]
790-
time_saved = output_saved[0][1]
789+
self.last_frame = current_frame
791790

792-
# Update preview image
793-
self.image_updated.emit(image_path)
791+
if output_saved:
792+
# Update preview image
793+
self.image_updated.emit(output_saved.group(1))
794794

795795
log.info("Blender process exited.")
796796

0 commit comments

Comments
 (0)