tinyblast/tinyblast.py

473 lines
18 KiB
Python

import maya.cmds as cmds
import maya.OpenMaya as om
import maya.OpenMayaMPx as ompx
import os
import subprocess
import sys
import tkinter as tk
from tkinter import filedialog
from ui.tinyblast_options import Ui_TinyblastOptions
from PySide6 import QtCore
from PySide6.QtWidgets import QMainWindow, QFileDialog
from PySide6.QtCore import QCoreApplication, QSettings
import shiboken6
# Global variable to store the scriptJob ID
playblast_job_id = None
original_playblast = cmds.playblast
tinyblast_instance = None
def get_plugin_directory():
# Get the path of the currently loaded plugin
plugin_name = "tinyblast"
plugin_path = cmds.pluginInfo(plugin_name, query=True, path=True)
return os.path.dirname(plugin_path)
# def custom_playblast(*args, **kwargs):
# print("Running tinyblast...")
#
# kwargs['format'] = 'avi'
# kwargs['percent'] = 100
# kwargs['quality'] = 100
# kwargs['widthHeight'] = (1920, 1080)
#
# result = original_playblast(*args, **kwargs)
# print(f"{result}")
#
# if result:
# try:
# ffmpeg_path = os.path.join(get_plugin_directory(), 'ffmpeg.exe')
# print(f"ffmpeg path: {ffmpeg_path}")
# if not os.path.exists(ffmpeg_path):
# raise FileNotFoundError(f"FFmpeg binary not found at {ffmpeg_path}")
#
# input_file = result # The file output by playblast
# #output_directory = os.path.dirname(result) # Get the directory path
# output_directory = os.path.dirname(cmds.file(query=True, sceneName=True))
# input_filename = os.path.basename(result) # Get the filename with extension
#
# # Change the extension to .mp4
# output_filename = os.path.splitext(input_filename)[0] + ".mp4"
#
# # Define the full path for the converted output file
# output_file = os.path.join(output_directory, output_filename)
#
# # Run FFmpeg conversion
# subprocess.run([ffmpeg_path,
# '-i', input_file,
# '-vcodec', 'libx264',
# '-pix_fmt', 'yuv420p',
# '-strict', 'experimental',
# '-b:v', '1m',
# output_file,
# '-y'], check=True, shell=True)
# print(f"Video conversion to {output_file} successful!")
# # os.remove(input_file) # Running into permission issues trying to delete from AppData
# # print(f"Original playblast deleted: {input_file}")
# except subprocess.CalledProcessError as e:
# print(f"Error during FFmpeg conversion: {e}")
class WindowWatcher:
""" A class to watch for a particular window in Maya """
def __init__(self, window_title, on_open_callback, on_close_callback=None):
self.window_title = window_title
self.on_open_callback = on_open_callback
self.on_close_callback = on_close_callback
self.window_opened = False
def check_for_window_open(self):
if not self.window_opened:
window = self.get_window_by_title(self.window_title)
if window:
self.on_open_callback()
self.window_opened = True
else:
window = self.get_window_by_title(self.window_title)
if not window:
self.window_opened = False
if self.on_close_callback:
self.on_close_callback()
def get_window_by_title(self, title):
# Check all open windows and return the one that matches the title
windows = cmds.lsUI(windows=True)
for window in windows:
if cmds.window(window, query=True, title=True) == title:
return window
return None
def add_custom_button_to_playblast():
# Get the Playblast Options window
window = get_playblast_options_window()
if window:
# Find the layout of the Playblast Options window
layout = cmds.columnLayout(adjustableColumn=True)
if layout:
# Add a custom button below existing UI
cmds.setParent(layout) # Set the parent to the top-level layout
cmds.columnLayout(adjustableColumn=True)
cmds.button(label="Tinyblast", command=custom_button_action)
else:
print("Couldn't find the layout for the Playblast Options window.")
else:
print("Playblast Options window not found.")
def custom_button_action(*args):
cmds.playblast()
def get_playblast_options_window():
# Check if the Playblast Options window is open
windows = cmds.lsUI(windows=True)
for window in windows:
if cmds.window(window, query=True, title=True) == "Playblast Options": # Exact title match
return window
return None
def setup_script_job():
global playblast_job_id
# Kill any previously running scriptJob
if playblast_job_id is not None and cmds.scriptJob(exists=playblast_job_id):
cmds.scriptJob(kill=playblast_job_id, force=True)
print(f"Killed previous scriptJob with ID: {playblast_job_id}")
# Watch for the Playblast Options window by title
playblast_watcher = WindowWatcher(
window_title="Playblast Options", # Exact window title to look for
on_open_callback=add_custom_button_to_playblast
)
# Set up a new scriptJob
playblast_job_id = cmds.scriptJob(event=["idle", playblast_watcher.check_for_window_open])
class Tinyblast(ompx.MPxCommand):
def __init__(self):
ompx.MPxCommand.__init__(self)
self.format = 'mp4'
self.codec = 'libx265'
self.bitrate = '5m'
self.pixel_format = 'yuv420p'
self.resolution = (1920, 1080)
# self.resolution = (cmds.getAttr("defaultResolution.width"), cmds.getAttr("defaultResolution.height"))
self.percent = '100'
self.path = ''
self.save = True
def apply_settings(self, format, codec, quality, resolution, scale, file_path, save):
self.format = format
self.codec = self.get_codec(codec)
self.bitrate = f"{round(8 * (resolution[0] * resolution[1]) / (1920 * 1080) * (float(quality) / 100), 1)}M"
self.resolution = resolution
self.percent = int(scale * 100)
self.path = file_path
self.save = save
def get_codec(self, pretty_name):
if pretty_name == 'HEVC (H.265)':
return 'libx265'
if pretty_name == 'H.264':
return 'libx264'
if pretty_name == 'AV1':
return 'libaom-av1'
if pretty_name == 'MPEG-4':
return 'mpeg4'
if pretty_name == 'P8':
return 'libvpx'
if pretty_name == 'VP9':
return 'libvpx-vp9'
if pretty_name == 'Theora':
return 'libtheora'
if pretty_name == 'DNxHD':
return 'dnxhd'
if pretty_name == 'DNxHR':
return 'dnxhr'
if pretty_name == 'Motion JPEG':
return 'mjpeg'
return 'Not a codec'
def doIt(selfself, args):
print("Tinyblasting...")
cmds.playblast()
@staticmethod
def cmdCreator():
return ompx.asMPxPtr(Tinyblast())
def custom_playblast(self, *args, **kwargs):
kwargs['format'] = 'avi'
kwargs['percent'] = int(self.percent)
kwargs['quality'] = 100
kwargs['widthHeight'] = self.resolution
result = original_playblast(*args, **kwargs)
if result and self.save:
self.blastIt(result)
def blastIt(self, input_path):
try:
ffmpeg_path = os.path.join(get_plugin_directory(), 'ffmpeg.exe')
if not os.path.exists(ffmpeg_path):
raise FileNotFoundError(f"FFmpeg binary not found at {ffmpeg_path}")
input_file = input_path # The file output by playblast
#output_directory = os.path.dirname(result) # Get the directory path
output_directory = os.path.dirname(cmds.file(query=True, sceneName=True))
input_filename = os.path.basename(input_path) # Get the filename with extension
# Change the extension to .mp4
output_filename = os.path.splitext(input_filename)[0] + ".mp4"
# Define the full path for the converted output file
output_file = self.path
# Run FFmpeg conversion
subprocess.run([ffmpeg_path,
'-i', input_file,
'-vcodec', self.codec,
'-pix_fmt', self.pixel_format,
'-strict', 'experimental',
'-b:v', self.bitrate,
output_file,
'-y'], check=True, shell=True)
print(f"Video conversion to {output_file} successful!")
# os.remove(input_file) # Running into permission issues trying to delete from AppData
# print(f"Original playblast deleted: {input_file}")
except subprocess.CalledProcessError as e:
print(f"Error during FFmpeg conversion: {e}")
def get_maya_window():
import maya.OpenMayaUI as omui
from PySide6 import QtWidgets
ptr = omui.MQtUtil.mainWindow()
if ptr is not None:
return shiboken6.wrapInstance(int(ptr), QtWidgets.QWidget)
else:
return None
class TinyblastOptionsWindow(QMainWindow):
def __init__(self, parent=None):
super(TinyblastOptionsWindow, self).__init__(parent or get_maya_window())
self.ui = Ui_TinyblastOptions()
self.ui.setupUi(self)
self.resize(1280, 720)
self.restore_settings()
self.ui.formattingComboBox.currentIndexChanged.connect(self.update_format)
self.ui.tinyblastButton.clicked.connect(self.tinyblast)
self.ui.applyButton.clicked.connect(self.apply_settings)
self.ui.quitButton.clicked.connect(self.quit_window)
self.ui.qualitySlider.valueChanged.connect(self.update_quality_slider)
self.ui.qualitySpinBox.valueChanged.connect(self.update_quality_spinbox)
self.ui.displaySizeComboBox.currentIndexChanged.connect(self.update_display_size)
self.ui.scaleSlider.valueChanged.connect(self.update_scale_slider)
self.ui.scaleSpinBox.valueChanged.connect(self.update_scale_spinbox)
self.ui.framePaddingSlider.valueChanged.connect(self.update_frame_padding_slider)
self.ui.framePaddingSpinBox.valueChanged.connect(self.update_frame_padding_spinbox)
self.ui.saveToFileCheckBox.toggled.connect(self.save_to_file_toggle)
self.ui.browseButton.clicked.connect(self.browse_files)
def restore_settings(self):
settings = QSettings("Jack Christensen", "Tinyblast")
geometry = settings.value("windowGeometry")
if geometry:
self.restoreGeometry(geometry)
def closeEvent(self, event):
settings = QSettings("Jack Christensen", "Tinyblast")
settings.setValue("windowGeometry", self.saveGeometry())
def update_format(self, index):
self.ui.encodingComboBox.clear()
# MP4
if index == 0:
self.ui.encodingComboBox.addItems([
QCoreApplication.translate("TinyblastOptions", u"HEVC (H.265)", None),
QCoreApplication.translate("TinyblastOptions", u"H.264", None),
QCoreApplication.translate("TinyblastOptions", u"AV1", None),
QCoreApplication.translate("TinyblastOptions", u"MPEG-4", None),
QCoreApplication.translate("TinyblastOptions", u"VP9", None)
])
if self.ui.filePathTextBox.text():
split_path = self.ui.filePathTextBox.text().rsplit('.', 1)
self.ui.filePathTextBox.setText(f"{split_path[0]}.mp4")
# MKV
if index == 1:
self.ui.encodingComboBox.addItems([
QCoreApplication.translate("TinyblastOptions", u"HEVC (H.265)", None),
QCoreApplication.translate("TinyblastOptions", u"H.264", None),
QCoreApplication.translate("TinyblastOptions", u"AV1", None),
QCoreApplication.translate("TinyblastOptions", u"VP9", None),
QCoreApplication.translate("TinyblastOptions", u"VP8", None),
QCoreApplication.translate("TinyblastOptions", u"Theora", None)
])
if self.ui.filePathTextBox.text():
split_path = self.ui.filePathTextBox.text().rsplit('.', 1)
self.ui.filePathTextBox.setText(f"{split_path[0]}.mkv")
# MOV
if index == 2:
self.ui.encodingComboBox.addItems([
QCoreApplication.translate("TinyblastOptions", u"HEVC (H.265)", None),
QCoreApplication.translate("TinyblastOptions", u"H.264", None),
QCoreApplication.translate("TinyblastOptions", u"DNxHD", None),
QCoreApplication.translate("TinyblastOptions", u"DNxHR", None),
QCoreApplication.translate("TinyblastOptions", u"MPEG-4", None)
])
if self.ui.filePathTextBox.text():
split_path = self.ui.filePathTextBox.text().rsplit('.', 1)
self.ui.filePathTextBox.setText(f"{split_path[0]}.mov")
# AVI
if index == 3:
self.ui.encodingComboBox.addItems([
QCoreApplication.translate("TinyblastOptions", u"H.264", None),
QCoreApplication.translate("TinyblastOptions", u"MPEG-4", None),
QCoreApplication.translate("TinyblastOptions", u"Motion JPEG", None),
])
if self.ui.filePathTextBox.text():
split_path = self.ui.filePathTextBox.text().rsplit('.', 1)
self.ui.filePathTextBox.setText(f"{split_path[0]}.avi")
# WEBM
if index == 4:
self.ui.encodingComboBox.addItems([
QCoreApplication.translate("TinyblastOptions", u"AV1", None),
QCoreApplication.translate("TinyblastOptions", u"VP9", None),
QCoreApplication.translate("TinyblastOptions", u"VP8", None)
])
if self.ui.filePathTextBox.text():
split_path = self.ui.filePathTextBox.text().rsplit('.', 1)
self.ui.filePathTextBox.setText(f"{split_path[0]}.webm")
def tinyblast(self):
print("Tinyblasting...")
self.apply_settings()
tinyblast_instance.custom_playblast()
def apply_settings(self):
# Directly call the `apply_settings` method of the Tinyblast instance
if tinyblast_instance is not None:
tinyblast_instance.apply_settings(
self.ui.formattingComboBox.currentText(), # Use the selected format
self.ui.encodingComboBox.currentText(), # Use the selected codec
self.ui.qualitySpinBox.value(), # Quality as a percentage
(self.ui.widthSpinBox.value(), self.ui.heightSpinBox.value()), # Resolution
self.ui.scaleSpinBox.value(), # Scale
self.ui.filePathTextBox.text(), # File path
self.ui.saveToFileCheckBox.isChecked()
)
def quit_window(self):
tb_window.close()
def update_quality_slider(self, value):
self.ui.qualitySpinBox.setValue(value)
def update_quality_spinbox(self, value):
self.ui.qualitySlider.setValue(value)
def update_display_size(self, index):
if index == 0:
self.ui.widthSpinBox.setEnabled(True)
self.ui.heightSpinBox.setEnabled(True)
if index == 1:
self.ui.widthSpinBox.setEnabled(False)
self.ui.heightSpinBox.setEnabled(False)
def update_scale_slider(self, value):
self.ui.scaleSpinBox.setValue(float(value/1000.0))
def update_scale_spinbox(self, value):
self.ui.scaleSlider.setValue(int(value*1000))
def update_frame_padding_slider(self, value):
self.ui.framePaddingSpinBox.setValue(value)
def update_frame_padding_spinbox(self, value):
self.ui.framePaddingSlider.setValue(value)
def save_to_file_toggle(self):
if self.ui.saveToFileCheckBox.isChecked():
self.ui.browseButton.setEnabled(True)
self.ui.filePathTextBox.setEnabled(True)
else:
self.ui.browseButton.setEnabled(False)
self.ui.filePathTextBox.setEnabled(False)
def browse_files(self):
path = self.choose_save_path()
self.ui.filePathTextBox.setText(f"{path}")
def choose_save_path(self):
current_container = str(self.ui.formattingComboBox.currentText()).lower()
save_path, _ = QFileDialog.getSaveFileName(
None,
"Choose Save Location",
"",
f"Video (*.{current_container});;All Files (*)"
)
return save_path
def show_my_window():
global tb_window
try:
tb_window.close()
tb_window.deleteLater()
except:
pass
tb_window = TinyblastOptionsWindow()
tb_window.show()
class OpenTinyblastOptions(ompx.MPxCommand):
def __init__(self):
ompx.MPxCommand.__init__(self)
def doIt(self, args):
show_my_window()
def cmdCreator():
return ompx.asMPxPtr(OpenTinyblastOptions())
def initializePlugin(mobject):
global tinyblast_instance
tinyblast_instance = Tinyblast()
try:
mplugin = ompx.MFnPlugin(mobject, "Jack Christensen", "2.0.0", "Any")
mplugin.registerCommand("tinyblast", Tinyblast.cmdCreator)
mplugin.registerCommand("openTinyblastOptions", cmdCreator)
om.MGlobal.displayInfo("Tinyblast plugin loaded.")
setup_script_job()
cmds.playblast = tinyblast_instance.custom_playblast
except Exception as e:
om.MGlobal.displayError(f"Failed to initialize plugin: {str(e)}")
raise
def uninitializePlugin(mobject):
try:
mplugin = ompx.MFnPlugin(mobject)
tb_window.close()
mplugin.deregisterCommand("tinyblast")
mplugin.deregisterCommand("openTinyblastOptions")
om.MGlobal.displayInfo("Tinyblast plugin unloaded.")
except Exception as e:
om.MGlobal.displayError(f"Failed to uninitialize plugin: {str(e)}")
raise