diff --git a/deployment/docker/docker-compose.production.yml b/deployment/docker/docker-compose.production.yml index 6aaa6f6db..a9ebee5d6 100644 --- a/deployment/docker/docker-compose.production.yml +++ b/deployment/docker/docker-compose.production.yml @@ -142,6 +142,94 @@ services: cpus: '0.25' memory: 128M + # Media Processing Daemon (Job Queue, Radio, TV) + media-daemon: + build: + context: ../../services/media_daemon + dockerfile: Dockerfile + container_name: metabuilder-media-prod + environment: + MEDIA_BIND_ADDRESS: 0.0.0.0 + MEDIA_PORT: 8090 + MEDIA_WORKERS: 4 + DBAL_URL: http://dbal-daemon:8080 + DBAL_API_KEY: ${DBAL_API_KEY:-} + MEDIA_VIDEO_WORKERS: 2 + MEDIA_AUDIO_WORKERS: 4 + MEDIA_DOC_WORKERS: 4 + MEDIA_IMAGE_WORKERS: 8 + MEDIA_RADIO_ENABLED: "true" + MEDIA_TV_ENABLED: "true" + ICECAST_HOST: icecast + ICECAST_PASSWORD: ${ICECAST_PASSWORD:-hackme} + volumes: + - media_library:/data/media:ro + - media_output:/data/output + - media_cache:/data/cache + - hls_output:/data/hls + - media_temp:/data/temp + ports: + - "8090:8090" + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8090/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + depends_on: + dbal-daemon: + condition: service_healthy + networks: + - metabuilder-network + deploy: + resources: + limits: + cpus: '4' + memory: 4G + reservations: + cpus: '1' + memory: 1G + + # Icecast Server for Radio Streaming + icecast: + image: libretime/icecast:2.4.4 + container_name: metabuilder-icecast-prod + environment: + ICECAST_SOURCE_PASSWORD: ${ICECAST_PASSWORD:-hackme} + ICECAST_ADMIN_PASSWORD: ${ICECAST_ADMIN_PASSWORD:-hackme} + ICECAST_RELAY_PASSWORD: ${ICECAST_PASSWORD:-hackme} + ICECAST_HOSTNAME: ${ICECAST_HOSTNAME:-localhost} + ICECAST_MAX_CLIENTS: 1000 + ICECAST_MAX_SOURCES: 20 + volumes: + - icecast_logs:/var/log/icecast2 + ports: + - "8000:8000" + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/status-json.xsl"] + interval: 30s + timeout: 5s + retries: 3 + networks: + - metabuilder-network + + # HLS/DASH Streaming Server + nginx-stream: + image: nginx:alpine + container_name: metabuilder-nginx-stream-prod + volumes: + - hls_output:/data/hls:ro + - ../../services/media_daemon/config/nginx-stream.conf:/etc/nginx/conf.d/default.conf:ro + ports: + - "8088:80" + depends_on: + - media-daemon + restart: unless-stopped + networks: + - metabuilder-network + # Redis Cache (Optional but recommended) redis: image: redis:7-alpine @@ -183,6 +271,19 @@ volumes: driver: local nginx_logs: driver: local + # Media daemon volumes + media_library: + driver: local + media_output: + driver: local + media_cache: + driver: local + media_temp: + driver: local + hls_output: + driver: local + icecast_logs: + driver: local networks: metabuilder-network: diff --git a/services/media_daemon/plugins/examples/my_custom_plugin.cpp b/services/media_daemon/plugins/examples/my_custom_plugin.cpp new file mode 100644 index 000000000..349224538 --- /dev/null +++ b/services/media_daemon/plugins/examples/my_custom_plugin.cpp @@ -0,0 +1,320 @@ +#include "media/plugin.hpp" +#include +#include +#include +#include +#include +#include + +namespace media { +namespace plugins { + +/** + * Example Custom Plugin + * + * This demonstrates how to create a custom plugin for the media daemon. + * Plugins are loaded dynamically at runtime from shared libraries. + * + * Build with: + * g++ -shared -fPIC -o my_custom_plugin.so my_custom_plugin.cpp -std=c++17 + */ +class MyCustomPlugin : public Plugin { +public: + MyCustomPlugin() = default; + ~MyCustomPlugin() override = default; + + PluginInfo info() const override { + return PluginInfo{ + .id = "my_custom_plugin", + .name = "My Custom Plugin", + .version = "1.0.0", + .author = "Your Name", + .description = "Example custom plugin for media processing", + .type = PluginType::PROCESSOR, + .supported_formats = {"txt", "json", "xml"}, + .capabilities = {"text_transform", "json_validate"}, + .is_loaded = initialized_, + .is_builtin = false + }; + } + + PluginCapabilities capabilities() const override { + PluginCapabilities caps; + caps.supports_video = false; + caps.supports_audio = false; + caps.supports_image = false; + caps.supports_document = true; + caps.supports_streaming = false; + caps.supports_hardware_accel = false; + caps.input_formats = {"txt", "json", "xml"}; + caps.output_formats = {"txt", "json", "xml"}; + return caps; + } + + Result initialize(const std::string& config_path) override { + std::cout << "[MyCustomPlugin] Initializing with config: " << config_path << std::endl; + + // Load configuration if provided + if (!config_path.empty()) { + std::ifstream config_file(config_path); + if (config_file.is_open()) { + // Parse config... + config_file.close(); + } + } + + initialized_ = true; + return Result::ok(); + } + + void shutdown() override { + std::cout << "[MyCustomPlugin] Shutting down" << std::endl; + initialized_ = false; + } + + bool is_healthy() const override { + return initialized_; + } + + bool can_handle(JobType type, const JobParams& params) const override { + // This plugin only handles custom jobs + if (type != JobType::CUSTOM) { + return false; + } + + // Check if params contain our supported operation + if (auto* custom_params = std::get_if>(¶ms)) { + auto it = custom_params->find("operation"); + if (it != custom_params->end()) { + return it->second == "text_transform" || + it->second == "json_validate"; + } + } + + return false; + } + + Result process( + const JobRequest& request, + JobProgressCallback progress_callback + ) override { + if (!initialized_) { + return Result::error( + ErrorCode::SERVICE_UNAVAILABLE, + "Plugin not initialized" + ); + } + + // Extract custom params + auto* custom_params = std::get_if>(&request.params); + if (!custom_params) { + return Result::error( + ErrorCode::VALIDATION_ERROR, + "Invalid parameters for custom plugin" + ); + } + + std::string operation = (*custom_params)["operation"]; + std::string input_path = (*custom_params)["input_path"]; + std::string output_path = (*custom_params)["output_path"]; + + // Report starting + if (progress_callback) { + progress_callback(request.id, JobProgress{ + .percent = 0.0, + .stage = "starting", + .eta = "calculating..." + }); + } + + // Process based on operation + Result result; + if (operation == "text_transform") { + result = process_text_transform(input_path, output_path, *custom_params, request.id, progress_callback); + } else if (operation == "json_validate") { + result = process_json_validate(input_path, output_path, *custom_params, request.id, progress_callback); + } else { + return Result::error( + ErrorCode::VALIDATION_ERROR, + "Unknown operation: " + operation + ); + } + + // Report completion + if (progress_callback) { + progress_callback(request.id, JobProgress{ + .percent = 100.0, + .stage = "completed" + }); + } + + return result; + } + + Result cancel(const std::string& job_id) override { + // Mark job as cancelled + std::lock_guard lock(jobs_mutex_); + auto it = active_jobs_.find(job_id); + if (it != active_jobs_.end()) { + it->second = true; // cancelled + return Result::ok(); + } + return Result::error(ErrorCode::NOT_FOUND, "Job not found"); + } + +private: + Result process_text_transform( + const std::string& input_path, + const std::string& output_path, + const std::map& params, + const std::string& job_id, + JobProgressCallback progress_callback + ) { + // Read input file + std::ifstream input(input_path); + if (!input.is_open()) { + return Result::error( + ErrorCode::NOT_FOUND, + "Cannot open input file: " + input_path + ); + } + + std::stringstream buffer; + buffer << input.rdbuf(); + std::string content = buffer.str(); + input.close(); + + if (progress_callback) { + progress_callback(job_id, JobProgress{.percent = 30.0, .stage = "processing"}); + } + + // Get transform type + std::string transform = "uppercase"; + auto it = params.find("transform"); + if (it != params.end()) { + transform = it->second; + } + + // Apply transformation + if (transform == "uppercase") { + for (char& c : content) { + c = std::toupper(c); + } + } else if (transform == "lowercase") { + for (char& c : content) { + c = std::tolower(c); + } + } else if (transform == "reverse") { + std::reverse(content.begin(), content.end()); + } + + if (progress_callback) { + progress_callback(job_id, JobProgress{.percent = 70.0, .stage = "writing"}); + } + + // Write output file + std::ofstream output(output_path); + if (!output.is_open()) { + return Result::error( + ErrorCode::STORAGE_ERROR, + "Cannot create output file: " + output_path + ); + } + + output << content; + output.close(); + + return Result::ok(output_path); + } + + Result process_json_validate( + const std::string& input_path, + const std::string& output_path, + const std::map& params, + const std::string& job_id, + JobProgressCallback progress_callback + ) { + // Read input file + std::ifstream input(input_path); + if (!input.is_open()) { + return Result::error( + ErrorCode::NOT_FOUND, + "Cannot open input file: " + input_path + ); + } + + std::stringstream buffer; + buffer << input.rdbuf(); + std::string content = buffer.str(); + input.close(); + + if (progress_callback) { + progress_callback(job_id, JobProgress{.percent = 50.0, .stage = "validating"}); + } + + // Simple JSON validation (check for balanced braces) + int brace_count = 0; + int bracket_count = 0; + bool in_string = false; + bool escaped = false; + + for (char c : content) { + if (escaped) { + escaped = false; + continue; + } + if (c == '\\') { + escaped = true; + continue; + } + if (c == '"') { + in_string = !in_string; + continue; + } + if (!in_string) { + if (c == '{') brace_count++; + else if (c == '}') brace_count--; + else if (c == '[') bracket_count++; + else if (c == ']') bracket_count--; + } + } + + bool valid = (brace_count == 0 && bracket_count == 0 && !in_string); + + // Write validation result + std::ofstream output(output_path); + if (!output.is_open()) { + return Result::error( + ErrorCode::STORAGE_ERROR, + "Cannot create output file: " + output_path + ); + } + + output << "{\n"; + output << " \"valid\": " << (valid ? "true" : "false") << ",\n"; + output << " \"input_file\": \"" << input_path << "\",\n"; + output << " \"brace_balance\": " << brace_count << ",\n"; + output << " \"bracket_balance\": " << bracket_count << "\n"; + output << "}\n"; + output.close(); + + if (!valid) { + return Result::error( + ErrorCode::VALIDATION_ERROR, + "JSON validation failed" + ); + } + + return Result::ok(output_path); + } + + bool initialized_ = false; + std::mutex jobs_mutex_; + std::map active_jobs_; // job_id -> cancelled +}; + +} // namespace plugins +} // namespace media + +// Export plugin functions +MEDIA_PLUGIN_EXPORT(media::plugins::MyCustomPlugin)