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:
parent
b7cc493a5d
commit
241a33b7b5
|
|
@ -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
66
trunk/conf/python.conf
Normal 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
95
trunk/configure
vendored
|
|
@ -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
122
trunk/python/analytics.py
Normal 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
142
trunk/python/http_addon.py
Normal 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
80
trunk/python/monitor.py
Normal 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()
|
||||
7
trunk/python/requirements.txt
Normal file
7
trunk/python/requirements.txt
Normal 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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
259
trunk/src/app/srs_app_python_manager.cpp
Normal file
259
trunk/src/app/srs_app_python_manager.cpp
Normal 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 "";
|
||||
}
|
||||
54
trunk/src/app/srs_app_python_manager.hpp
Normal file
54
trunk/src/app/srs_app_python_manager.hpp
Normal 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
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user