-
-
Save miabrahams/69cfe8a8dd74ffc3e87203467c64973e to your computer and use it in GitHub Desktop.
import obspython as obs | |
import subprocess | |
import os | |
import re | |
import datetime | |
# Info for potential OBS Python hackers! | |
# Tip 1 - Read the "OBS Studio Backend Design" documentation page. Read the documentation table of contents. | |
# Tip 2 - be sure to add obspython.py to your script path to enable completion. | |
# Tip 3 - Some of the Python API is generated at runtime, so it won't show up in obspython.py. | |
# To search the full API for e.g. "frontend" functions, uncomment this line and reload your script: | |
# [print(i) for i in dir(obs) if i.lower().find("frontend") > -1] | |
# Tip 4 - Here's a set of ffmpeg flags to produce mobile-ready Telegram mp4s: | |
# -an -vf scale=720:-1:flags=lanczos -vprofile baseline -pix_fmt yuv420p | |
class OBSDataModel: | |
"""Interact with an obs_data more comfortably using Python type checking. | |
This class models a single obs_data object at a time. | |
This ffmpeg plugin only ever interacts with a single "properties" data object, corresponding to user input boxes | |
in the script settings. | |
There are two access methods: | |
1. Store config data persistently with store_data() and load_data(). | |
2. Non-persistent access with [] syntax: useful when the obs_data object is unavailable, eg. external callbacks. | |
Initialized with a dict containing the names and default values of the data items. | |
""" | |
getter_fun = {bool: obs.obs_data_get_bool, str: obs.obs_data_get_string, | |
int: obs.obs_data_get_int, float: obs.obs_data_get_double} | |
setter_fun = {bool: obs.obs_data_set_bool, str: obs.obs_data_set_string, | |
int: obs.obs_data_set_int, float: obs.obs_data_set_double} | |
default_fun = {bool: obs.obs_data_set_default_bool, str: obs.obs_data_set_default_string, | |
int: obs.obs_data_set_default_int, float: obs.obs_data_set_default_double} | |
data_dict = None | |
def __init__(self, default_dict): | |
self.data_dict = default_dict | |
def __getitem__(self, arg): | |
return self.data_dict[arg] | |
def __setitem__(self, arg, value): | |
self.data_dict[arg] = value | |
def store_data(self, obs_data, name, value): | |
self.data_dict[name] = value | |
self.setter_fun[type(value)](obs_data, name, value) | |
def load_data(self, obs_data, name): | |
self.data_dict[name] = self.getter_fun[type(self.data_dict[name])](obs_data, name) | |
return self.data_dict[name] | |
def set_data_defaults(self, obs_data): | |
"""First, default values for obs_data items according to contents of data_dict. | |
Then synchronizes data_dict with obs_data.""" | |
for name, value in self.data_dict.items(): | |
self.default_fun[type(value)](obs_data, name, value) | |
self.load_data(obs_data, name) | |
class OBSPluginFfmpeg(OBSDataModel): | |
""" Main class implementing OBS Ffmpeg Python Plugin.""" | |
# Default values for script options | |
_defaults = ({"debug_enabled": False, | |
"auto_convert": True, | |
"src_regex": "", | |
"dst_regex": "", | |
"src_regex_validated": str(datetime.datetime.now().year), | |
"dst_regex_validated": "%SCENE%_" + str(datetime.datetime.now().year), | |
"record_folder": os.path.expanduser("~") + os.path.sep + "Videos", | |
"custom_flags": "-vf scale=-1:720:flags=lanczos" | |
}) | |
description = "<b>Ffmpeg Auto-converter</b>" + \ | |
"<hr>" + \ | |
"Automatically transcode OBS output with ffmpeg. Supports renaming with Python regular expressions. " + \ | |
"Make sure ffmpeg is in the system path!<br/><br/>" + \ | |
"Additional renaming tokens:<br/>" + \ | |
"%SCENE% - name of current scene <br/>" + \ | |
"%SRC1% - input source 1 (SRC2 for source 2, etc.)" + \ | |
"<br/><br/>" + \ | |
"©2018 Michael Abrahams. GPLv3 license." + \ | |
"<hr>" | |
# We install a callback in this signal handler to run ffmpeg when recording is finished | |
recording_signal_handler = None | |
last_ffmpeg_output = "" | |
def __init__(self): | |
super().__init__(self._defaults) | |
def debug(self, text): | |
if self['debug_enabled']: | |
print(text) | |
def set_defaults(self, settings): | |
self.set_data_defaults(settings) | |
self.debug(f"_____ script_defaults()\n Saved settings data:\n {obs.obs_data_get_json(settings)}") | |
def load(self, settings): | |
# Initialize text fields to the last fully validated version | |
self.store_data(settings, "src_regex", self["src_regex_validated"]) | |
self.store_data(settings, "dst_regex", self["dst_regex_validated"]) | |
def save(self, settings): | |
"""Store any validation errors from ffmpeg_convert when we couldn't write settings.""" | |
# XXX: We can't trigger this every time plugin is reloaded, only when the user moves around in prefs menu. | |
self.store_data(settings, "src_regex_validated", self["src_regex_validated"]) | |
self.store_data(settings, "dst_regex_validated", self["dst_regex_validated"]) | |
def update_settings(self, settings_data): | |
""" Read updated data when user changes input fields.""" | |
self.debug("_____ update_settings()") | |
[self.load_data(settings_data, d) for d in ["debug_enabled", "auto_convert", "custom_flags", | |
"record_folder", "src_regex", "dst_regex"]] | |
def validate(self, properties, prop_id, settings_data): | |
""" Validate user input regex with re.compile() """ | |
run_button = obs.obs_properties_get(properties, "run_button") | |
try: | |
re.compile(self["src_regex"]) | |
obs.obs_property_set_enabled(run_button, True) | |
obs.obs_property_set_description(run_button, "Run") | |
self.store_data(settings_data, "src_regex_validated", self["src_regex"]) | |
self.store_data(settings_data, "dst_regex_validated", self["dst_regex"]) | |
except Exception: | |
self.debug("Invalid regex!") | |
obs.obs_property_set_enabled(run_button, False) | |
obs.obs_property_set_description(run_button, "Invalid regular expression!") | |
self.debug(f"src validated: {self['src_regex_validated']} dst validated: {self['dst_regex_validated']}") | |
def recording_finished(self, stop_code): | |
""" Implements post-recording callback. """ | |
self.debug(f"Recording finished with stop_code: {stop_code}") | |
if self['auto_convert'] is True and stop_code is 0: | |
self.ffmpeg_convert() | |
def find_latest_obs_capture(self, capture_dir): | |
# TODO: handle this better. | |
def new_video_sort_key(f): | |
if f.name[-3:] in ['flv', 'mp4', 'mov', 'mkv'] and f.name is not self.last_ffmpeg_output: | |
return f.stat().st_mtime | |
return 0 | |
newest_video_file = sorted(os.scandir(capture_dir), key=new_video_sort_key, reverse=True)[0] | |
if new_video_sort_key(newest_video_file) is 0: | |
print("Could not find any video files!") | |
return None | |
return newest_video_file.name | |
def ffmpeg_convert(self): | |
"""Search $HOME/videos for most recently created file. Convert using custom ffmpeg command. """ | |
# XXX: We can get an object of type config_t but no Python methods are set up to access it. | |
# Instead we have to prompt user to specify their video folder. | |
# cfg = obs.obs_frontend_get_profile_config() | |
print("Converting ") | |
capture_dir = self['record_folder'] + os.path.sep | |
obs_capture_file_name = self.find_latest_obs_capture(capture_dir) | |
if obs_capture_file_name is None: | |
return False | |
# Get current scene and source name for %SUBSTITUTION% | |
current_scene = obs.obs_frontend_get_current_scene() # Note - returns an "obs_source" object | |
scene_name = obs.obs_source_get_name(current_scene) | |
current_scene = obs.obs_scene_from_source(current_scene) | |
scene_items = obs.obs_scene_enum_items(current_scene) | |
source_names = [obs.obs_source_get_name(obs.obs_sceneitem_get_source(i)) for i in scene_items] | |
obs.sceneitem_list_release(scene_items) | |
# Do regexp and token substitutions | |
out_file_name = obs_capture_file_name[0:-4] | |
if self["src_regex_validated"] is not None: | |
try: | |
dst_regex_out = self["dst_regex_validated"].replace("%SCENE%", scene_name) | |
for n, src in enumerate(source_names): | |
repl_str = f"%SOURCE{n + 1}%" | |
dst_regex_out = dst_regex_out.replace(repl_str, src) | |
out_file_name = re.sub(self["src_regex_validated"], dst_regex_out, out_file_name) | |
except Exception: | |
### XXX: Should invalidate src_regex and dst_regex if this happens | |
print("Regular expression replacement failed!") | |
out_file_name = out_file_name + ".mp4" | |
# Run ffmpeg | |
ffmpeg_command = f'ffmpeg -i "{capture_dir + obs_capture_file_name}" {self["custom_flags"]} "{capture_dir + out_file_name}"' | |
self.debug(f"ffmpeg command: {ffmpeg_command}") | |
try: | |
res = subprocess.run(ffmpeg_command, check=True, universal_newlines=True, | |
stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
self.debug(res.stderr) | |
self.last_ffmpeg_output = out_file_name | |
except subprocess.CalledProcessError as e: | |
# Todo: send a louder message if this happens (and for failure of regex above) | |
print(f"Ffmpeg failed! Command was: \n{ffmpeg_command}\n\nOutput was:") | |
print(e.stderr) | |
# Instance plugin | |
ffplug = OBSPluginFfmpeg() | |
# Define global callbacks | |
def cb_button_pressed(properties, button): | |
# XXX: unlike in search_str_callback, editing the button text in this callback doesn't work. | |
ffplug.ffmpeg_convert() | |
return True | |
def cb_search_text_changed(*args): | |
ffplug.validate(*args) | |
def cb_recording_finished(callback_data): | |
stop_code = obs.calldata_int(callback_data, "code") | |
ffplug.recording_finished(stop_code) | |
return True | |
def update_recording_callback(reconnect = True): | |
if ffplug.recording_signal_handler is not None: | |
obs.signal_handler_disconnect(ffplug.recording_signal_handler, "stop", cb_recording_finished) | |
if reconnect: | |
ffplug.recording_signal_handler = obs.obs_output_get_signal_handler(obs.obs_frontend_get_recording_output()) | |
obs.signal_handler_connect(ffplug.recording_signal_handler, "stop", cb_recording_finished) | |
# OBS API Hooks Start Below | |
def script_defaults(settings): | |
# obs.obs_data_clear(settings) # Clear saved plugin data. Useful for debugging. | |
ffplug.set_defaults(settings) | |
def script_load(settings): | |
ffplug.load(settings) | |
update_recording_callback() | |
# XXX: there's no good way to trigger callbacks when profiles are changed. | |
def script_description(): | |
return ffplug.description | |
def script_update(settings): | |
ffplug.update_settings(settings) | |
def script_unload(): | |
ffplug.debug("_____ script_unload()") | |
update_recording_callback(False) | |
def script_save(settings): | |
ffplug.save(settings) | |
def script_properties(): | |
ffplug.debug("_____ script_properties()") | |
p = obs.obs_properties_create() | |
obs.obs_properties_add_bool(p, "auto_convert", "Autorun after recording") | |
obs.obs_properties_add_bool(p, "debug_enabled", "Debug Mode") | |
search_area = obs.obs_properties_add_text(p, "src_regex", "Find (regex)", 0) | |
obs.obs_property_set_modified_callback(search_area, cb_search_text_changed) | |
replace_area = obs.obs_properties_add_text(p, "dst_regex", "Replace (regex)", 0) | |
obs.obs_property_set_modified_callback(replace_area, cb_search_text_changed) | |
obs.obs_properties_add_path(p, "record_folder", "Recording folder", | |
obs.OBS_PATH_DIRECTORY, "", os.path.expanduser("~")) | |
obs.obs_properties_add_text(p, "custom_flags", "Ffmpeg flags:", 0) | |
obs.obs_properties_add_button(p, "run_button", "Run", cb_button_pressed) | |
return p |
Hello there,
Long time reader first time poster.
OK i had the same problem above. I found Python 6 works with OBS. Now i can see the settings and change them but i get errors.
Script log:
[FfmpegPostprocess.py] ffmpeg command: ffmpeg -i "C:\Users\TrinityDiy\Videos\2018-08-28 16-37-56.flv" -vf scale=-1:720:flags=lanczos "C:\Users\TrinityDiy\Videos\Scene_2018-08-28 16-37-56.mp4"
[FfmpegPostprocess.py] Traceback (most recent call last):
[FfmpegPostprocess.py] File "C:/Program Files/obs-studio/data/obs-plugins/frontend-tools/scripts\FfmpegPostprocess.py", line 215, in cb_recording_finished
[FfmpegPostprocess.py]
[FfmpegPostprocess.py] File "C:/Program Files/obs-studio/data/obs-plugins/frontend-tools/scripts\FfmpegPostprocess.py", line 138, in recording_finished
[FfmpegPostprocess.py] self.ffmpeg_convert()
[FfmpegPostprocess.py] File "C:/Program Files/obs-studio/data/obs-plugins/frontend-tools/scripts\FfmpegPostprocess.py", line 189, in ffmpeg_convert
[FfmpegPostprocess.py] stdout=subprocess.PIPE, stderr=subprocess.PIPE)
[FfmpegPostprocess.py] File "C:\Users\TrinityDiy\AppData\Local\Programs\Python\Python36\lib\subprocess.py", line 403, in run
[FfmpegPostprocess.py] with Popen(*popenargs, **kwargs) as process:
[FfmpegPostprocess.py] File "C:\Users\TrinityDiy\AppData\Local\Programs\Python\Python36\lib\subprocess.py", line 709, in init
[FfmpegPostprocess.py] restore_signals, start_new_session)
[FfmpegPostprocess.py] File "C:\Users\TrinityDiy\AppData\Local\Programs\Python\Python36\lib\subprocess.py", line 997, in _execute_child
[FfmpegPostprocess.py] startupinfo)
[FfmpegPostprocess.py] FileNotFoundError: [WinError 2] The system cannot find the file specified
[FfmpegPostprocess.py] Recording finished with stop_code: 0
[FfmpegPostprocess.py] ffmpeg command: ffmpeg -i "C:\Users\TrinityDiy\Videos\2018-08-28 16-37-56.flv" -vf scale=-1:720:flags=lanczos "C:\Users\TrinityDiy\Videos\Scene_2018-08-28 16-37-56.mp4"
Thank you in advance
SOLVED - A reboot fixed my issue. Gotta love Windows!
Any help would be greatly appreciated.
I am also having issues running the subprocess command within OBS. I can run it fine (with a basic ffmpeg command) from the command line (within Idle).
Here is the Script Log output:
[FfmpegPostprocess.py] Recording finished with stop_code: 0
[FfmpegPostprocess.py] Converting
[FfmpegPostprocess.py] ffmpeg command: ffmpeg -i "C:/Users/jeffa/Dropbox/Golf_Stuff/Golf Swing Videos/Captures\2019-12-19 17-05-39.mp4" -an -vf scale=720:-1:flags=lanczos -vprofile baseline -pix_fmt yuv420p "C:/Users/jeffa/Dropbox/Golf_Stuff/Golf Swing Videos/Captures\2019-12-19 17-05-39.mp4"
[FfmpegPostprocess.py] DEBUG 1 ffmpeg -i "C:/Users/jeffa/Dropbox/Golf_Stuff/Golf Swing Videos/Captures\2019-12-19 17-05-39.mp4" -an -vf scale=720:-1:flags=lanczos -vprofile baseline -pix_fmt yuv420p "C:/Users/jeffa/Dropbox/Golf_Stuff/Golf Swing Videos/Captures\2019-12-19 17-05-39.mp4"
[FfmpegPostprocess.py] Traceback (most recent call last):
[FfmpegPostprocess.py] File "C:/Program Files/obs-studio/data/obs-plugins/frontend-tools/scripts\FfmpegPostprocess.py", line 221, in cb_recording_finished
[FfmpegPostprocess.py] ffplug.recording_finished(stop_code)
[FfmpegPostprocess.py] File "C:/Program Files/obs-studio/data/obs-plugins/frontend-tools/scripts\FfmpegPostprocess.py", line 139, in recording_finished
[FfmpegPostprocess.py] self.ffmpeg_convert()
[FfmpegPostprocess.py] File "C:/Program Files/obs-studio/data/obs-plugins/frontend-tools/scripts\FfmpegPostprocess.py", line 194, in ffmpeg_convert
[FfmpegPostprocess.py] res = subprocess.run(ffmpeg_command, universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
[FfmpegPostprocess.py] File "C:\Program Files\Python36\lib\subprocess.py", line 403, in run
[FfmpegPostprocess.py] with Popen(*popenargs, **kwargs) as process:
[FfmpegPostprocess.py] File "C:\Program Files\Python36\lib\subprocess.py", line 709, in init
[FfmpegPostprocess.py] restore_signals, start_new_session)
[FfmpegPostprocess.py] File "C:\Program Files\Python36\lib\subprocess.py", line 997, in _execute_child
[FfmpegPostprocess.py] startupinfo)
[FfmpegPostprocess.py] FileNotFoundError: [WinError 2] The system cannot find the file specified
Here is the output from Idle:
import subprocess
res = subprocess.run(["ffmpeg", "-h"], check=True, universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print (res.stdout)
Hyper fast Audio and Video encoder
usage: ffmpeg [options] [[infile options] -i infile]... {[outfile options] outfile}...
Getting help:
-h -- print basic options
-h long -- print more options
-h full -- print all options (including all format and codec specific options, very long)
-h type=name -- print all options for the named decoder/encoder/demuxer/muxer/filter/bsf
See man ffmpeg for detailed description of the options.
Print help / information / capabilities:
-L show license
-h topic show help
-? topic show help
-help topic show help
--help topic show help
-version show version
-buildconf show build configuration
-formats show available formats
-muxers show available muxers
-demuxers show available demuxers
-devices show available devices
-codecs show available codecs
-decoders show available decoders
-encoders show available encoders
-bsfs show available bit stream filters
-protocols show available protocols
-filters show available filters
-pix_fmts show available pixel formats
-layouts show standard channel layouts
-sample_fmts show available audio sample formats
-colors show available color names
-sources device list sources of the input device
-sinks device list sinks of the output device
-hwaccels show available HW acceleration methods
Global options (affect whole program instead of just one file:
-loglevel loglevel set logging level
-v loglevel set logging level
-report generate a report
-max_alloc bytes set maximum size of a single allocated block
-y overwrite output files
-n never overwrite output files
-ignore_unknown Ignore unknown stream types
-filter_threads number of non-complex filter threads
-filter_complex_threads number of threads for -filter_complex
-stats print progress report during encoding
-max_error_rate maximum error rate ratio of errors (0.0: no errors, 1.0: 100% errors) above which ffmpeg returns an error instead of success.
-bits_per_raw_sample number set the number of bits per raw sample
-vol volume change audio volume (256=normal)
Per-file main options:
-f fmt force format
-c codec codec name
-codec codec codec name
-pre preset preset name
-map_metadata outfile[,metadata]:infile[,metadata] set metadata information of outfile from infile
-t duration record or transcode "duration" seconds of audio/video
-to time_stop record or transcode stop time
-fs limit_size set the limit file size in bytes
-ss time_off set the start time offset
-sseof time_off set the start time offset relative to EOF
-seek_timestamp enable/disable seeking by timestamp with -ss
-timestamp time set the recording timestamp ('now' to set the current time)
-metadata string=string add metadata
-program title=string:st=number... add program with specified streams
-target type specify target file type ("vcd", "svcd", "dvd", "dv" or "dv50" with optional prefixes "pal-", "ntsc-" or "film-")
-apad audio pad
-frames number set the number of frames to output
-filter filter_graph set stream filtergraph
-filter_script filename read stream filtergraph description from a file
-reinit_filter reinit filtergraph on input parameter changes
-discard discard
-disposition disposition
Video options:
-vframes number set the number of video frames to output
-r rate set frame rate (Hz value, fraction or abbreviation)
-s size set frame size (WxH or abbreviation)
-aspect aspect set aspect ratio (4:3, 16:9 or 1.3333, 1.7777)
-bits_per_raw_sample number set the number of bits per raw sample
-vn disable video
-vcodec codec force video codec ('copy' to copy stream)
-timecode hh:mm:ss[:;.]ff set initial TimeCode value.
-pass n select the pass number (1 to 3)
-vf filter_graph set video filters
-ab bitrate audio bitrate (please use -b:a)
-b bitrate video bitrate (please use -b:v)
-dn disable data
Audio options:
-aframes number set the number of audio frames to output
-aq quality set audio quality (codec-specific)
-ar rate set audio sampling rate (in Hz)
-ac channels set number of audio channels
-an disable audio
-acodec codec force audio codec ('copy' to copy stream)
-vol volume change audio volume (256=normal)
-af filter_graph set audio filters
Subtitle options:
-s size set frame size (WxH or abbreviation)
-sn disable subtitle
-scodec codec force subtitle codec ('copy' to copy stream)
-stag fourcc/tag force subtitle tag/fourcc
-fix_sub_duration fix subtitles duration
-canvas_size size set canvas size (WxH or abbreviation)
-spre preset set the subtitle options to the indicated preset
Hi there!
try to use it on 21.1.2 obs x64 and it dont works( help me plz.
Am i understand right? that i record any video in *.mkv. And after i press stop recording my file automatically reencode to mp4?
i have *same 2 files.
it very useful for sony vegas. it doesnt use mkv and avi. only mp4. it s so hard to reencode every file again and again.
i found it here
i dont have settings like this