Addition Features: Python Addons

You can now use Python scripts as addons in SRS.
Remark: Only available in Ubuntu Linux x86_64 version.
Not available in other platform (Cgywin64, MacOS_Unix, Docker)

After modification in requirements.txt, please reconfigure the
SRS source code by "./trunk/configure"
This commit is contained in:
Jason-JP-Yang 2025-05-28 22:20:07 +08:00
parent b7cc493a5d
commit 241a33b7b5
13 changed files with 909 additions and 6 deletions

View File

@ -36,5 +36,5 @@ This is the guide for the OpenAI Codex agent.
- `trunk/Dockerfile.builds` is used to verify builds on different target platforms.
## Testing Instructions
- Run CI tests defined in `.github/workflows/test.yml` file.
- Add or update tests for the code you change, even if nobody asked.
- Run CI tests by WSL Bash `cd trunk && bash -c "./configure && make"`
- Run the SRS Service by WSL Bash `bash -c "./objs/srs -c ./conf/console.conf"`

66
trunk/conf/python.conf Normal file
View File

@ -0,0 +1,66 @@
# SRS configuration with Python addons
# This demonstrates how to configure Python addons that start with SRS
# and terminate when SRS gracefully quits.
# The Python addons use SRS's built-in Python environment created during compilation.
listen 1935;
max_connections 1000;
daemon off;
srs_log_tank console;
pid ./objs/srs.python.pid;
# Python addon configuration
python_addons {
# Enable or disable Python addon management
enabled on;
# Python addon definitions
# Each addon block defines a Python script to run
addon {
# Script path relative to SRS working directory
script "./python/monitor.py";
# Command line arguments (optional)
args "--config ./conf/python.conf --verbose";
# Working directory (optional, defaults to SRS working directory)
work_dir "./";
}
addon {
script "./python/analytics.py";
args "--port 8888";
}
addon {
script "./python/http_addon.py";
args "--port 9999 --verbose";
}
}
http_api {
enabled on;
listen 1985;
}
http_server {
enabled on;
listen 8080;
dir ./objs/nginx/html;
}
vhost __defaultVhost__ {
enabled on;
gop_cache on;
hls {
enabled on;
hls_path ./objs/nginx/html;
hls_fragment 5;
hls_window 30;
}
http_hooks {
enabled on;
on_publish http://127.0.0.1:8085/api/v1/streams;
on_unpublish http://127.0.0.1:8085/api/v1/streams;
}
}

95
trunk/configure vendored
View File

@ -331,7 +331,7 @@ MODULE_FILES=("srs_app_server" "srs_app_conn" "srs_app_rtmp_conn" "srs_app_sourc
"srs_app_mpegts_udp" "srs_app_listener" "srs_app_async_call"
"srs_app_caster_flv" "srs_app_latest_version" "srs_app_uuid" "srs_app_process" "srs_app_ng_exec"
"srs_app_hourglass" "srs_app_dash" "srs_app_fragment" "srs_app_dvr"
"srs_app_coworkers" "srs_app_hybrid" "srs_app_threads")
"srs_app_coworkers" "srs_app_hybrid" "srs_app_threads" "srs_app_python_manager")
if [[ $SRS_SRT == YES ]]; then
MODULE_FILES+=("srs_app_srt_server" "srs_app_srt_listener" "srs_app_srt_conn" "srs_app_srt_utility" "srs_app_srt_source")
fi
@ -708,6 +708,96 @@ echo 'Configure ok! '
# create objs/logs for ffmpeg to write log.
mkdir -p ${SRS_OBJS}/logs
#####################################################################################
# Setup Python virtual environment for addons
#####################################################################################
echo -e "${GREEN}Setting up Python virtual environment for addons...${BLACK}"
# Create Python virtual environment directory
PYTHON_VENV_DIR="${SRS_OBJS}/python_venv"
mkdir -p ${PYTHON_VENV_DIR}
# Check if python3 is available
if command -v python3 >/dev/null 2>&1; then
PYTHON_CMD="python3"
elif command -v python >/dev/null 2>&1; then
PYTHON_CMD="python"
else
echo -e "${YELLOW}Warning: Python not found, Python addons will be disabled.${BLACK}"
SRS_PYTHON_AVAILABLE=NO
fi
if [[ -n $PYTHON_CMD ]]; then
# Create virtual environment
echo "Creating Python virtual environment with $PYTHON_CMD..."
echo "This may take a moment, please wait..."
$PYTHON_CMD -m venv ${PYTHON_VENV_DIR}
if [[ $? -eq 0 ]]; then
echo -e "${GREEN}Virtual environment created successfully.${BLACK}"
# Activate virtual environment and install requirements
if [[ -f ${PYTHON_VENV_DIR}/bin/activate ]]; then
source ${PYTHON_VENV_DIR}/bin/activate
VENV_PYTHON="${PYTHON_VENV_DIR}/bin/python"
elif [[ -f ${PYTHON_VENV_DIR}/Scripts/activate ]]; then
# Windows path
source ${PYTHON_VENV_DIR}/Scripts/activate
VENV_PYTHON="${PYTHON_VENV_DIR}/Scripts/python.exe"
else
echo -e "${YELLOW}Warning: Virtual environment activation script not found.${BLACK}"
VENV_PYTHON="$PYTHON_CMD"
fi
# Install requirements if file exists
if [[ -f ${SRS_WORKDIR}/python/requirements.txt ]]; then
echo "Installing Python requirements..."
echo "Upgrading pip..."
$VENV_PYTHON -m pip install --upgrade pip
if [[ $? -eq 0 ]]; then
echo -e "${GREEN}Pip upgraded successfully.${BLACK}"
echo "Installing packages from requirements.txt..."
echo "This may take several minutes depending on your network connection..."
$VENV_PYTHON -m pip install -r ${SRS_WORKDIR}/python/requirements.txt
if [[ $? -eq 0 ]]; then
echo -e "${GREEN}Python requirements installed successfully.${BLACK}"
SRS_PYTHON_AVAILABLE=YES
else
echo -e "${YELLOW}Warning: Failed to install Python requirements.${BLACK}"
SRS_PYTHON_AVAILABLE=NO
fi
else
echo -e "${YELLOW}Warning: Failed to upgrade pip.${BLACK}"
echo "Attempting to install requirements with current pip version..."
$VENV_PYTHON -m pip install -r ${SRS_WORKDIR}/python/requirements.txt
if [[ $? -eq 0 ]]; then
echo -e "${GREEN}Python requirements installed successfully.${BLACK}"
SRS_PYTHON_AVAILABLE=YES
else
echo -e "${YELLOW}Warning: Failed to install Python requirements.${BLACK}"
SRS_PYTHON_AVAILABLE=NO
fi
fi
else
echo -e "${GREEN}No requirements.txt found, skipping package installation.${BLACK}"
SRS_PYTHON_AVAILABLE=YES
fi
deactivate 2>/dev/null || true
else
echo -e "${YELLOW}Warning: Failed to create Python virtual environment.${BLACK}"
SRS_PYTHON_AVAILABLE=NO
fi
fi
if [[ $SRS_PYTHON_AVAILABLE == YES ]]; then
echo -e "${GREEN}Python virtual environment setup completed.${BLACK}"
# Write Python path to a file for runtime use
echo "${VENV_PYTHON}" > ${SRS_OBJS}/python_path.txt
else
echo -e "${YELLOW}Python virtual environment setup skipped.${BLACK}"
fi
#####################################################################################
# configure summary
#####################################################################################
@ -871,5 +961,4 @@ fi
echo ""
echo "You can build SRS:"
echo "\" make \" to build the SRS server"
echo "\" make help \" to get some help"
echo "\" make help \" to get some help"

122
trunk/python/analytics.py Normal file
View File

@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""
SRS Analytics Script
This demonstrates another Python process that can run alongside SRS.
"""
import time
import signal
import sys
import argparse
import logging
import json
from http.server import HTTPServer, BaseHTTPRequestHandler
from threading import Thread
from datetime import datetime
class AnalyticsHandler(BaseHTTPRequestHandler):
def do_GET(self):
"""Handle GET requests for analytics data"""
if self.path == '/stats':
# Return sample analytics data
stats = {
'timestamp': datetime.now().isoformat(),
'connections': 42,
'streams': 5,
'bandwidth': '1.2 Mbps',
'uptime': '2h 30m'
}
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(stats, indent=2).encode())
else:
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
"""Override to use our logger"""
pass
class SRSAnalytics:
def __init__(self, port=8888):
self.port = port
self.running = True
self.server = None
self.server_thread = None
# Set up logging
logging.basicConfig(
level=logging.INFO,
format='[%(asctime)s] [Python Analytics] %(levelname)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
self.logger = logging.getLogger(__name__)
# Set up signal handlers
signal.signal(signal.SIGTERM, self.signal_handler)
signal.signal(signal.SIGINT, self.signal_handler)
def signal_handler(self, signum, frame):
"""Handle shutdown signals from SRS"""
self.logger.info(f"Received signal {signum}, shutting down gracefully...")
self.running = False
if self.server:
self.server.shutdown()
def start_http_server(self):
"""Start the HTTP analytics server"""
try:
self.server = HTTPServer(('localhost', self.port), AnalyticsHandler)
self.logger.info(f"Analytics server started on http://localhost:{self.port}")
self.server.serve_forever()
except Exception as e:
self.logger.error(f"Error starting HTTP server: {e}")
def run(self):
"""Main analytics loop"""
self.logger.info(f"SRS Analytics started on port {self.port}")
try:
# Start HTTP server in a separate thread
self.server_thread = Thread(target=self.start_http_server)
self.server_thread.daemon = True
self.server_thread.start()
# Main analytics loop
while self.running:
# Simulate analytics work
self.logger.debug("Processing analytics data...")
# You can add your analytics logic here:
# - Collect stream metrics
# - Process viewer statistics
# - Generate reports
# - Store data to database
time.sleep(10) # Process every 10 seconds
except Exception as e:
self.logger.error(f"Error in analytics loop: {e}")
finally:
self.cleanup()
def cleanup(self):
"""Cleanup before shutdown"""
self.logger.info("Cleaning up Analytics server...")
if self.server:
self.server.shutdown()
self.logger.info("Analytics server stopped")
def main():
parser = argparse.ArgumentParser(description='SRS Analytics Server')
parser.add_argument('--port', type=int, default=8888, help='HTTP server port')
args = parser.parse_args()
analytics = SRSAnalytics(args.port)
analytics.run()
if __name__ == '__main__':
main()

142
trunk/python/http_addon.py Normal file
View File

@ -0,0 +1,142 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
SRS Python Addon - Simple HTTP Server
This addon demonstrates how to create a simple HTTP server as an SRS addon.
"""
import sys
import signal
import time
import logging
import argparse
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler
# Set up logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('srs_http_addon')
class SRSAddonHandler(BaseHTTPRequestHandler):
"""Simple HTTP request handler for SRS addon."""
def do_GET(self):
"""Handle GET requests."""
if self.path == '/':
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
response = '''
<html>
<body>
<h1>SRS Python Addon - HTTP Server</h1>
<p>This is a simple HTTP server running as an SRS addon.</p>
<p>Status: Running</p>
<p>Time: {}</p>
</body>
</html>
'''.format(time.strftime('%Y-%m-%d %H:%M:%S'))
self.wfile.write(response.encode())
elif self.path == '/status':
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
response = '{"status": "running", "time": "%s"}' % time.strftime('%Y-%m-%d %H:%M:%S')
self.wfile.write(response.encode())
else:
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
"""Override to use our logger."""
logger.info("%s - %s" % (self.address_string(), format % args))
class SRSHTTPAddon:
"""SRS HTTP Addon main class."""
def __init__(self, port=8888):
self.port = port
self.server = None
self.running = False
self.server_thread = None
def start(self):
"""Start the HTTP server."""
try:
self.server = HTTPServer(('', self.port), SRSAddonHandler)
self.running = True
# Start server in a separate thread
self.server_thread = threading.Thread(target=self._run_server)
self.server_thread.daemon = True
self.server_thread.start()
logger.info(f"SRS HTTP addon started on port {self.port}")
return True
except Exception as e:
logger.error(f"Failed to start HTTP addon: {e}")
return False
def stop(self):
"""Stop the HTTP server."""
if self.server and self.running:
self.running = False
self.server.shutdown()
self.server.server_close()
if self.server_thread:
self.server_thread.join(timeout=5)
logger.info("SRS HTTP addon stopped")
def _run_server(self):
"""Run the HTTP server."""
try:
self.server.serve_forever()
except Exception as e:
if self.running:
logger.error(f"HTTP server error: {e}")
def signal_handler(signum, frame):
"""Handle termination signals."""
logger.info(f"Received signal {signum}, shutting down...")
if 'addon' in globals():
addon.stop()
sys.exit(0)
def main():
"""Main function."""
parser = argparse.ArgumentParser(description='SRS HTTP Addon')
parser.add_argument('--port', type=int, default=8888, help='HTTP server port')
parser.add_argument('--verbose', action='store_true', help='Enable verbose logging')
args = parser.parse_args()
if args.verbose:
logger.setLevel(logging.DEBUG)
# Set up signal handlers
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
# Create and start the addon
global addon
addon = SRSHTTPAddon(args.port)
if addon.start():
logger.info("SRS HTTP addon is running, press Ctrl+C to stop")
try:
while addon.running:
time.sleep(1)
except KeyboardInterrupt:
logger.info("Interrupted by user")
finally:
addon.stop()
else:
logger.error("Failed to start SRS HTTP addon")
sys.exit(1)
if __name__ == '__main__':
main()

80
trunk/python/monitor.py Normal file
View File

@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""
SRS Python Monitor Script
This is an example Python script that runs alongside SRS server.
It demonstrates how Python processes can be managed by SRS.
"""
import time
import signal
import sys
import argparse
import logging
from datetime import datetime
class SRSMonitor:
def __init__(self, config_file=None, verbose=False):
self.running = True
self.config_file = config_file
self.verbose = verbose
# Set up logging
level = logging.DEBUG if verbose else logging.INFO
logging.basicConfig(
level=level,
format='[%(asctime)s] [Python Monitor] %(levelname)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
self.logger = logging.getLogger(__name__)
# Set up signal handlers
signal.signal(signal.SIGTERM, self.signal_handler)
signal.signal(signal.SIGINT, self.signal_handler)
def signal_handler(self, signum, frame):
"""Handle shutdown signals from SRS"""
self.logger.info(f"Received signal {signum}, shutting down gracefully...")
self.running = False
def run(self):
"""Main monitoring loop"""
self.logger.info("SRS Python Monitor started")
if self.config_file:
self.logger.info(f"Using config file: {self.config_file}")
try:
while self.running:
# Simulate monitoring work
self.logger.debug(f"Monitor heartbeat at {datetime.now()}")
# You can add your monitoring logic here:
# - Check stream status
# - Monitor server health
# - Send alerts
# - Log analytics
time.sleep(5) # Check every 5 seconds
except Exception as e:
self.logger.error(f"Error in monitor loop: {e}")
finally:
self.cleanup()
def cleanup(self):
"""Cleanup before shutdown"""
self.logger.info("Cleaning up Python Monitor...")
# Add any cleanup logic here
self.logger.info("Python Monitor stopped")
def main():
parser = argparse.ArgumentParser(description='SRS Python Monitor')
parser.add_argument('--config', help='Configuration file path')
parser.add_argument('--verbose', action='store_true', help='Enable verbose logging')
args = parser.parse_args()
monitor = SRSMonitor(args.config, args.verbose)
monitor.run()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,7 @@
# Python packages required for SRS Python addons
requests>=2.25.0
psutil>=5.8.0
pyyaml>=5.4.0
websockets>=10.0
aiohttp>=3.8.0
numpy>=1.20.0

View File

@ -932,6 +932,50 @@ SrsConfDirective* SrsConfDirective::get_or_create(string n)
return conf;
}
// python_addons section
bool SrsConfig::get_python_addons_enabled()
{
SRS_OVERWRITE_BY_ENV_BOOL("srs.python_addons.enabled"); // SRS_PYTHON_ADDONS_ENABLED
static bool DEFAULT = false;
SrsConfDirective* conf = root->get("python_addons");
if (!conf) {
return DEFAULT;
}
conf = conf->get("enabled");
if (!conf) {
return DEFAULT;
}
return SRS_CONF_PREFER_FALSE(conf->arg0());
}
vector<SrsConfDirective*> SrsConfig::get_python_addons_processes()
{
vector<SrsConfDirective*> processes;
SrsConfDirective* conf = root->get("python_addons");
if (!conf) {
return processes;
}
for (int i = 0; i < (int)conf->directives.size(); i++) {
SrsConfDirective* directive = conf->directives[i];
if (directive->name == "addon") {
processes.push_back(directive);
}
}
return processes;
}
SrsConfDirective* SrsConfig::get_python_addons_on()
{
return root->get("python_addons");
}
SrsConfDirective* SrsConfDirective::get_or_create(string n, string a0)
{
SrsConfDirective* conf = get(n, a0);
@ -2367,7 +2411,7 @@ srs_error_t SrsConfig::check_normal_config()
&& n != "inotify_auto_reload" && n != "auto_reload_for_docker" && n != "tcmalloc_release_rate"
&& n != "query_latest_version" && n != "first_wait_for_qlv" && n != "threads"
&& n != "circuit_breaker" && n != "is_full" && n != "in_docker" && n != "tencentcloud_cls"
&& n != "exporter"
&& n != "exporter" && n != "python_addons"
) {
return srs_error_new(ERROR_SYSTEM_CONFIG_INVALID, "illegal directive %s", n.c_str());
}
@ -2463,6 +2507,15 @@ srs_error_t SrsConfig::check_normal_config()
}
}
}
if (true) {
SrsConfDirective* conf = root->get("python_addons");
for (int i = 0; conf && i < (int)conf->directives.size(); i++) {
string n = conf->at(i)->name;
if (n != "enabled" && n != "addon") {
return srs_error_new(ERROR_SYSTEM_CONFIG_INVALID, "illegal python_addons.%s", n.c_str());
}
}
}
////////////////////////////////////////////////////////////////////////
// check listen for rtmp.

View File

@ -1142,6 +1142,14 @@ public:
virtual std::string get_exporter_listen();
virtual std::string get_exporter_label();
virtual std::string get_exporter_tag();
// python_addons section
public:
// Whether python_addons processes are enabled.
virtual bool get_python_addons_enabled();
// Get the python_addons processes configuration.
virtual std::vector<SrsConfDirective*> get_python_addons_processes();
// Get the python_addons directive.
virtual SrsConfDirective* get_python_addons_on();
};
#endif

View File

@ -0,0 +1,259 @@
//
// Copyright (c) 2013-2025 The SRS Authors
//
// SPDX-License-Identifier: MIT
//
#include <srs_app_python_manager.hpp>
#include <srs_kernel_log.hpp>
#include <srs_kernel_error.hpp>
#include <srs_app_config.hpp>
#include <srs_app_process.hpp>
#include <srs_kernel_utility.hpp>
#include <sstream>
#include <unistd.h>
#include <cstdio>
#include <cstring>
SrsPythonManager::SrsPythonManager()
{
enabled_ = false;
disposed_ = false;
}
SrsPythonManager::~SrsPythonManager()
{
dispose();
}
srs_error_t SrsPythonManager::initialize()
{
srs_error_t err = srs_success;
if ((err = parse_config()) != srs_success) {
return srs_error_wrap(err, "parse python config");
}
return err;
}
srs_error_t SrsPythonManager::start()
{
srs_error_t err = srs_success;
if (!enabled_) {
return err;
}
srs_trace("Python manager starting %d processes", (int)processes_.size());
for (std::vector<SrsProcess*>::iterator it = processes_.begin(); it != processes_.end(); ++it) {
SrsProcess* process = *it;
if ((err = process->start()) != srs_success) {
srs_error("Failed to start Python process: %s", srs_error_desc(err).c_str());
// Continue starting other processes even if one fails
srs_freep(err);
continue;
}
srs_trace("Python process started, pid=%d", process->get_pid());
}
srs_trace("Python manager started successfully");
return err;
}
void SrsPythonManager::stop()
{
if (!enabled_ || disposed_) {
return;
}
srs_trace("Python manager stopping %d processes", (int)processes_.size());
// First, send SIGTERM to all processes for graceful shutdown
for (std::vector<SrsProcess*>::iterator it = processes_.begin(); it != processes_.end(); ++it) {
SrsProcess* process = *it;
if (process->started()) {
process->fast_stop();
srs_trace("Sent SIGTERM to Python process, pid=%d", process->get_pid());
}
}
// Wait a moment for graceful shutdown
srs_usleep(500 * SRS_UTIME_MILLISECONDS);
// Then stop all processes (will SIGKILL if necessary)
for (std::vector<SrsProcess*>::iterator it = processes_.begin(); it != processes_.end(); ++it) {
SrsProcess* process = *it;
if (process->started()) {
process->stop();
srs_trace("Python process stopped, pid=%d", process->get_pid());
}
}
srs_trace("Python manager stopped");
}
void SrsPythonManager::dispose()
{
if (disposed_) {
return;
}
disposed_ = true;
stop();
// Clean up all process objects
for (std::vector<SrsProcess*>::iterator it = processes_.begin(); it != processes_.end(); ++it) {
SrsProcess* process = *it;
srs_freep(process);
}
processes_.clear();
srs_trace("Python manager disposed");
}
srs_error_t SrsPythonManager::parse_config()
{
srs_error_t err = srs_success;
// Check if Python addons are enabled in config
SrsConfDirective* conf = _srs_config->get_python_addons_on();
if (!conf || !_srs_config->get_python_addons_enabled()) {
enabled_ = false;
srs_trace("Python addon manager disabled in config");
return err;
}
enabled_ = true;
srs_trace("Python addon manager enabled in config");
// Get Python executable path from built environment
std::string python_exe = get_builtin_python_path();
if (python_exe.empty()) {
srs_warn("Built-in Python environment not found, disabling Python addons");
enabled_ = false;
return err;
}
// Get Python addon processes configuration
std::vector<SrsConfDirective*> addon_confs = _srs_config->get_python_addons_processes();
srs_trace("Parsed %d Python addons from config", (int)addon_confs.size());
for (std::vector<SrsConfDirective*>::iterator it = addon_confs.begin(); it != addon_confs.end(); ++it) {
SrsConfDirective* addon_conf = *it;
// Get script path
SrsConfDirective* script_conf = addon_conf->get("script");
if (!script_conf || script_conf->args.empty()) {
srs_warn("Python addon missing script directive, skipping");
continue;
}
std::string script_path = script_conf->arg0();
// Build command line arguments
std::vector<std::string> args;
args.push_back(python_exe); // Use built-in Python executable
args.push_back(script_path); // Script path
// Add additional arguments if specified
SrsConfDirective* args_conf = addon_conf->get("args");
if (args_conf && !args_conf->args.empty()) {
// Split the args string into individual arguments
std::string args_str = args_conf->arg0();
std::istringstream iss(args_str);
std::string arg;
while (iss >> arg) {
args.push_back(arg);
}
}
// Get working directory (optional)
std::string work_dir = "./";
SrsConfDirective* work_dir_conf = addon_conf->get("work_dir");
if (work_dir_conf && !work_dir_conf->args.empty()) {
work_dir = work_dir_conf->arg0();
}
if ((err = create_process(script_path, args, work_dir)) != srs_success) {
return srs_error_wrap(err, "create python addon process for %s", script_path.c_str());
}
}
srs_trace("Parsed %d Python addon processes from config", (int)processes_.size());
return err;
}
srs_error_t SrsPythonManager::create_process(const std::string& script_path, const std::vector<std::string>& args, const std::string& work_dir)
{
srs_error_t err = srs_success;
SrsProcess* process = new SrsProcess();
// Initialize the process with python executable and arguments
if ((err = process->initialize(args[0], args)) != srs_success) {
srs_freep(process);
return srs_error_wrap(err, "initialize python addon process");
}
processes_.push_back(process);
srs_trace("Created Python addon process for script: %s with executable: %s, work_dir: %s",
script_path.c_str(), args[0].c_str(), work_dir.c_str());
return err;
}
std::string SrsPythonManager::get_builtin_python_path()
{
// Try to read the Python path from the file created during build
std::string python_path_file = "./objs/python_path.txt";
FILE* fp = fopen(python_path_file.c_str(), "r");
if (!fp) {
srs_warn("Cannot open Python path file: %s", python_path_file.c_str());
return "";
}
char path[1024] = {0};
if (fgets(path, sizeof(path), fp) != NULL) {
// Remove newline characters
size_t len = strlen(path);
while (len > 0 && (path[len-1] == '\n' || path[len-1] == '\r')) {
path[--len] = '\0';
}
fclose(fp);
// Verify the Python executable exists
if (access(path, X_OK) == 0) {
srs_trace("Using built-in Python: %s", path);
return std::string(path);
} else {
srs_warn("Built-in Python executable not accessible: %s", path);
}
} else {
srs_warn("Failed to read Python path from file: %s", python_path_file.c_str());
}
fclose(fp);
// Fallback: try common locations
const char* fallback_paths[] = {
"./objs/python_venv/bin/python",
"./objs/python_venv/Scripts/python.exe",
"python3",
"python"
};
for (int i = 0; i < 4; i++) {
if (access(fallback_paths[i], X_OK) == 0) {
srs_trace("Using fallback Python: %s", fallback_paths[i]);
return std::string(fallback_paths[i]);
}
}
srs_warn("No suitable Python executable found");
return "";
}

View File

@ -0,0 +1,54 @@
//
// Copyright (c) 2013-2025 The SRS Authors
//
// SPDX-License-Identifier: MIT
//
#ifndef SRS_APP_PYTHON_MANAGER_HPP
#define SRS_APP_PYTHON_MANAGER_HPP
#include <srs_core.hpp>
#include <string>
#include <vector>
class SrsProcess;
// Manage Python processes that run alongside SRS server.
// This manager starts Python processes when SRS starts and stops them during graceful shutdown.
class SrsPythonManager
{
private:
bool enabled_;
bool disposed_;
std::vector<SrsProcess*> processes_;
public:
SrsPythonManager();
virtual ~SrsPythonManager();
public:
// Initialize the Python manager with configuration.
virtual srs_error_t initialize();
// Start all configured Python processes.
virtual srs_error_t start();
// Stop all Python processes gracefully.
virtual void stop();
// Dispose and cleanup all resources.
virtual void dispose();
private:
// Parse configuration and create Python processes.
virtual srs_error_t parse_config();
// Create a single Python process from configuration.
virtual srs_error_t create_process(const std::string& script_path, const std::vector<std::string>& args, const std::string& work_dir = "./");
// Get the path to the built-in Python executable.
virtual std::string get_builtin_python_path();
};
#endif

View File

@ -333,6 +333,7 @@ SrsServer::SrsServer()
signal_manager = new SrsSignalManager(this);
conn_manager = new SrsResourceManager("TCP", true);
latest_version_ = new SrsLatestVersion();
python_manager_ = new SrsPythonManager();
ppid = ::getppid();
rtmp_listener_ = new SrsMultipleTcpListeners(this);
@ -391,6 +392,7 @@ void SrsServer::destroy()
srs_freep(signal_manager);
srs_freep(latest_version_);
srs_freep(python_manager_);
srs_freep(conn_manager);
srs_freep(rtmp_listener_);
srs_freep(api_listener_);
@ -427,6 +429,9 @@ void SrsServer::dispose()
// Fast stop to notify FFMPEG to quit, wait for a while then fast kill.
ingester->dispose();
// Stop Python processes.
python_manager_->stop();
// dispose the source for hls and dvr.
_srs_sources->dispose();
@ -460,6 +465,10 @@ void SrsServer::gracefully_dispose()
ingester->stop();
srs_trace("ingesters stopped");
// Stop Python processes gracefully.
python_manager_->stop();
srs_trace("python processes stopped");
// Wait for connections to quit.
// While gracefully quiting, user can requires SRS to fast quit.
int wait_step = 1;
@ -534,6 +543,11 @@ srs_error_t SrsServer::initialize()
if ((err = http_server->initialize()) != srs_success) {
return srs_error_wrap(err, "http server initialize");
}
// Initialize Python process manager.
if ((err = python_manager_->initialize()) != srs_success) {
return srs_error_wrap(err, "python manager initialize");
}
return err;
}
@ -846,6 +860,11 @@ srs_error_t SrsServer::start(SrsWaitGroup* wg)
return srs_error_wrap(err, "tick");
}
// Start Python process manager.
if ((err = python_manager_->start()) != srs_success) {
return srs_error_wrap(err, "python manager start");
}
// OK, we start SRS server.
wg_ = wg;
wg->add(1);

View File

@ -21,6 +21,7 @@
#include <srs_protocol_st.hpp>
#include <srs_app_hourglass.hpp>
#include <srs_app_hybrid.hpp>
#include <srs_app_python_manager.hpp>
class SrsServer;
class ISrsHttpServeMux;
@ -41,6 +42,7 @@ class SrsMultipleTcpListeners;
class SrsHttpFlvListener;
class SrsUdpCasterListener;
class SrsGbListener;
class SrsPythonManager;
// Convert signal to io,
// @see: st-1.9/docs/notes.html
@ -146,6 +148,8 @@ private:
SrsSignalManager* signal_manager;
// To query the latest available version of SRS.
SrsLatestVersion* latest_version_;
// Python process manager for handling external Python processes.
SrsPythonManager* python_manager_;
// User send the signal, convert to variable.
bool signal_reload;
bool signal_persistence_config;