diff --git a/AGENTS.md b/AGENTS.md index 56af0b49e..e3e97997d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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"` diff --git a/trunk/conf/python.conf b/trunk/conf/python.conf new file mode 100644 index 000000000..4b78303ed --- /dev/null +++ b/trunk/conf/python.conf @@ -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; + } +} diff --git a/trunk/configure b/trunk/configure index 49a645e86..b8b9b7865 100755 --- a/trunk/configure +++ b/trunk/configure @@ -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" \ No newline at end of file diff --git a/trunk/python/analytics.py b/trunk/python/analytics.py new file mode 100644 index 000000000..7b36439b9 --- /dev/null +++ b/trunk/python/analytics.py @@ -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() diff --git a/trunk/python/http_addon.py b/trunk/python/http_addon.py new file mode 100644 index 000000000..b57c18818 --- /dev/null +++ b/trunk/python/http_addon.py @@ -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 = ''' + +
+This is a simple HTTP server running as an SRS addon.
+Status: Running
+Time: {}
+ + + '''.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() \ No newline at end of file diff --git a/trunk/python/monitor.py b/trunk/python/monitor.py new file mode 100644 index 000000000..e718f53ff --- /dev/null +++ b/trunk/python/monitor.py @@ -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() diff --git a/trunk/python/requirements.txt b/trunk/python/requirements.txt new file mode 100644 index 000000000..2438804f1 --- /dev/null +++ b/trunk/python/requirements.txt @@ -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 diff --git a/trunk/src/app/srs_app_config.cpp b/trunk/src/app/srs_app_config.cpp index 5f6fb7791..c56dad6bc 100644 --- a/trunk/src/app/srs_app_config.cpp +++ b/trunk/src/app/srs_app_config.cpp @@ -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