feat: Add media processing daemon and Icecast server configuration to docker-compose

This commit is contained in:
2025-12-30 11:45:05 +00:00
parent 29ded23578
commit f219cb38dc
2 changed files with 421 additions and 0 deletions

View File

@@ -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:

View File

@@ -0,0 +1,320 @@
#include "media/plugin.hpp"
#include <iostream>
#include <fstream>
#include <sstream>
#include <array>
#include <memory>
#include <cstdio>
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<void> 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<void>::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<std::map<std::string, std::string>>(&params)) {
auto it = custom_params->find("operation");
if (it != custom_params->end()) {
return it->second == "text_transform" ||
it->second == "json_validate";
}
}
return false;
}
Result<std::string> process(
const JobRequest& request,
JobProgressCallback progress_callback
) override {
if (!initialized_) {
return Result<std::string>::error(
ErrorCode::SERVICE_UNAVAILABLE,
"Plugin not initialized"
);
}
// Extract custom params
auto* custom_params = std::get_if<std::map<std::string, std::string>>(&request.params);
if (!custom_params) {
return Result<std::string>::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<std::string> 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<std::string>::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<void> cancel(const std::string& job_id) override {
// Mark job as cancelled
std::lock_guard<std::mutex> lock(jobs_mutex_);
auto it = active_jobs_.find(job_id);
if (it != active_jobs_.end()) {
it->second = true; // cancelled
return Result<void>::ok();
}
return Result<void>::error(ErrorCode::NOT_FOUND, "Job not found");
}
private:
Result<std::string> process_text_transform(
const std::string& input_path,
const std::string& output_path,
const std::map<std::string, std::string>& params,
const std::string& job_id,
JobProgressCallback progress_callback
) {
// Read input file
std::ifstream input(input_path);
if (!input.is_open()) {
return Result<std::string>::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<std::string>::error(
ErrorCode::STORAGE_ERROR,
"Cannot create output file: " + output_path
);
}
output << content;
output.close();
return Result<std::string>::ok(output_path);
}
Result<std::string> process_json_validate(
const std::string& input_path,
const std::string& output_path,
const std::map<std::string, std::string>& params,
const std::string& job_id,
JobProgressCallback progress_callback
) {
// Read input file
std::ifstream input(input_path);
if (!input.is_open()) {
return Result<std::string>::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<std::string>::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<std::string>::error(
ErrorCode::VALIDATION_ERROR,
"JSON validation failed"
);
}
return Result<std::string>::ok(output_path);
}
bool initialized_ = false;
std::mutex jobs_mutex_;
std::map<std::string, bool> active_jobs_; // job_id -> cancelled
};
} // namespace plugins
} // namespace media
// Export plugin functions
MEDIA_PLUGIN_EXPORT(media::plugins::MyCustomPlugin)