#include "compiler.h"
#include <errno.h>
#include <math.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
typedef struct {
bool known;
double seconds;
} Duration;
typedef enum {
NODE_SOURCE,
NODE_SLICE,
NODE_TAKE,
NODE_CONCAT,
NODE_DELAY,
NODE_VOLUME,
NODE_MIX,
NODE_SILENCE,
NODE_SPEED,
NODE_FADE_IN,
NODE_FADE_OUT,
} NodeKind;
typedef struct Node {
NodeKind kind;
ValueType type;
struct Node* input[2];
size_t id;
size_t source_index;
bool reachable;
unsigned consumers;
unsigned next_consumer;
double first;
double second;
Duration duration;
} Node;
typedef struct {
const char* path;
SourceKind kind;
size_t input_index;
} Input;
typedef struct {
ValueType type;
double number;
Node* video;
Node* audio;
ssize_t subtitle_input;
} Value;
typedef struct {
char* data;
size_t length;
size_t capacity;
} StringBuilder;
typedef struct {
Arena* arena;
Node** nodes;
size_t node_count;
size_t node_capacity;
Input* inputs;
size_t input_count;
size_t input_capacity;
Value stack[1024];
size_t stack_count;
ProbeCache* probes;
Error* error;
} Compiler;
static bool compile_error(Compiler* compiler, int line, const char* format, ...) {
compiler->error->line = line;
va_list args;
va_start(args, format);
vsnprintf(compiler->error->message, sizeof(compiler->error->message), format, args);
va_end(args);
return false;
}
static bool builder_reserve(StringBuilder* builder, size_t extra) {
if (extra > SIZE_MAX - builder->length - 1) {
return false;
}
size_t needed = builder->length + extra + 1;
if (needed <= builder->capacity) {
return true;
}
size_t capacity = builder->capacity == 0 ? 1024 : builder->capacity;
while (capacity < needed) {
if (capacity > SIZE_MAX / 2) {
capacity = needed;
break;
}
capacity *= 2;
}
char* data = realloc(builder->data, capacity);
if (data == NULL) {
return false;
}
builder->data = data;
builder->capacity = capacity;
return true;
}
static bool builder_append(StringBuilder* builder, const char* text) {
size_t length = strlen(text);
if (!builder_reserve(builder, length)) {
return false;
}
memcpy(builder->data + builder->length, text, length + 1);
builder->length += length;
return true;
}
static bool builder_printf(StringBuilder* builder, const char* format, ...) {
va_list args;
va_start(args, format);
va_list copy;
va_copy(copy, args);
int count = vsnprintf(NULL, 0, format, copy);
va_end(copy);
if (count < 0 || !builder_reserve(builder, (size_t)count)) {
va_end(args);
return false;
}
vsnprintf(builder->data + builder->length, (size_t)count + 1, format, args);
va_end(args);
builder->length += (size_t)count;
return true;
}
static char* arena_string(Arena* arena, const char* text) {
size_t length = strlen(text) + 1;
char* copy = arena_alloc(arena, length);
if (copy != NULL) {
memcpy(copy, text, length);
}
return copy;
}
static bool grow_array(Arena* arena, void** array, size_t count, size_t* capacity, size_t item_size,
size_t alignment) {
if (count < *capacity) {
return true;
}
size_t new_capacity = *capacity == 0 ? 16 : *capacity * 2;
if (new_capacity > SIZE_MAX / item_size) {
return false;
}
void* grown = arena_alloc_aligned(arena, new_capacity * item_size, alignment);
if (grown == NULL) {
return false;
}
if (*array != NULL) {
memcpy(grown, *array, count * item_size);
}
*array = grown;
*capacity = new_capacity;
return true;
}
static Node* new_node(Compiler* compiler, NodeKind kind, ValueType type, Node* left, Node* right) {
if (!grow_array(compiler->arena, (void** )&compiler->nodes, compiler->node_count,
&compiler->node_capacity, sizeof(*compiler->nodes), _Alignof(Node* ))) {
return NULL;
}
Node* node = arena_alloc(compiler->arena, sizeof(*node));
if (node == NULL) {
return NULL;
}
*node = (Node){
.kind = kind,
.type = type,
.input = {left, right},
.id = compiler->node_count,
};
compiler->nodes[compiler->node_count++] = node;
return node;
}
static bool add_input(Compiler* compiler, const char* path, SourceKind kind, size_t* index) {
if (!grow_array(compiler->arena, (void** )&compiler->inputs, compiler->input_count,
&compiler->input_capacity, sizeof(*compiler->inputs), _Alignof(Input))) {
return false;
}
*index = compiler->input_count;
compiler->inputs[compiler->input_count++] = (Input){
.path = path,
.kind = kind,
.input_index = *index,
};
return true;
}
static Duration duration_sum(Duration left, Duration right) {
if (!left.known || !right.known) {
return (Duration){0};
}
return (Duration){.known = true, .seconds = left.seconds + right.seconds};
}
static Duration duration_max(Duration left, Duration right) {
if (!left.known || !right.known) {
return (Duration){0};
}
return (Duration){.known = true, .seconds = fmax(left.seconds, right.seconds)};
}
static Node* source_node(Compiler* compiler, ValueType type, size_t source_index, Duration duration) {
Node* node = new_node(compiler, NODE_SOURCE, type, NULL, NULL);
if (node != NULL) {
node->source_index = source_index;
node->duration = duration;
}
return node;
}
static bool push_value(Compiler* compiler, Value value, int line) {
if (compiler->stack_count == sizeof(compiler->stack) / sizeof(compiler->stack[0])) {
return compile_error(compiler, line, "compiler stack limit exceeded");
}
compiler->stack[compiler->stack_count++] = value;
return true;
}
static Value pop_value(Compiler* compiler) {
return compiler->stack[--compiler->stack_count];
}
static Duration value_duration(Value value) {
if (value.type == TYPE_VIDEO) {
return value.video->duration;
}
if (value.type == TYPE_AUDIO) {
return value.audio->duration;
}
return duration_max(value.video->duration, value.audio->duration);
}
static bool compile_source(Compiler* compiler, Instruction instruction) {
size_t input_index = 0;
if (!add_input(compiler, instruction.as.source.path, instruction.as.source.kind, &input_index)) {
return compile_error(compiler, instruction.line, "out of memory");
}
Value value = {.type = (ValueType)instruction.as.source.kind, .subtitle_input = -1};
if (instruction.as.source.kind == SRC_SUBTITLES) {
value.subtitle_input = (ssize_t)input_index;
return push_value(compiler, value, instruction.line);
}
ProbeResult probe;
if (!probe_cache_get(compiler->probes, instruction.as.source.path, &probe, compiler->error)) {
compiler->error->line = instruction.line;
return false;
}
if ((instruction.as.source.kind == SRC_VIDEO || instruction.as.source.kind == SRC_COMBINED) &&
!probe.has_video) {
return compile_error(compiler, instruction.line, "source '%s' has no video stream",
instruction.as.source.path);
}
if ((instruction.as.source.kind == SRC_AUDIO || instruction.as.source.kind == SRC_COMBINED) &&
!probe.has_audio) {
return compile_error(compiler, instruction.line, "source '%s' has no audio stream",
instruction.as.source.path);
}
if (instruction.as.source.kind == SRC_VIDEO || instruction.as.source.kind == SRC_COMBINED) {
Duration duration = {probe.video_duration_known, probe.video_duration};
value.video = source_node(compiler, TYPE_VIDEO, input_index, duration);
}
if (instruction.as.source.kind == SRC_AUDIO || instruction.as.source.kind == SRC_COMBINED) {
Duration duration = {probe.audio_duration_known, probe.audio_duration};
value.audio = source_node(compiler, TYPE_AUDIO, input_index, duration);
}
if ((value.video == NULL && instruction.as.source.kind != SRC_AUDIO) ||
(value.audio == NULL && instruction.as.source.kind != SRC_VIDEO)) {
return compile_error(compiler, instruction.line, "out of memory");
}
return push_value(compiler, value, instruction.line);
}
static Node* stream_node(Value value) {
return value.type == TYPE_VIDEO ? value.video : value.audio;
}
static Value stream_value(ValueType type, Node* node) {
Value value = {.type = type, .subtitle_input = -1};
if (type == TYPE_VIDEO) {
value.video = node;
} else {
value.audio = node;
}
return value;
}
static bool compile_stack_operation(Compiler* compiler, Operation operation, int line) {
Value* stack = compiler->stack;
size_t count = compiler->stack_count;
(void)line;
switch (operation) {
case OP_DUP:
return push_value(compiler, stack[count - 1], line);
case OP_DROP:
compiler->stack_count--;
return true;
case OP_SWAP: {
Value temporary = stack[count - 1];
stack[count - 1] = stack[count - 2];
stack[count - 2] = temporary;
return true;
}
case OP_OVER:
return push_value(compiler, stack[count - 2], line);
case OP_ROT: {
Value first = stack[count - 3];
stack[count - 3] = stack[count - 2];
stack[count - 2] = stack[count - 1];
stack[count - 1] = first;
return true;
}
default:
return false;
}
}
static bool compile_operation(Compiler* compiler, Instruction instruction) {
Operation operation = instruction.as.op;
int line = instruction.line;
if (operation <= OP_ROT) {
return compile_stack_operation(compiler, operation, line);
}
if (operation == OP_GET_VIDEO || operation == OP_GET_AUDIO || operation == OP_SPLIT) {
Value combined = pop_value(compiler);
if (operation != OP_GET_AUDIO && !push_value(compiler, stream_value(TYPE_VIDEO, combined.video), line)) {
return false;
}
if (operation != OP_GET_VIDEO && !push_value(compiler, stream_value(TYPE_AUDIO, combined.audio), line)) {
return false;
}
return true;
}
if (operation == OP_LENGTH) {
Duration duration = value_duration(pop_value(compiler));
if (!duration.known) {
return compile_error(compiler, line, "stream duration is unknown");
}
return push_value(compiler, (Value){.type = TYPE_DURATION, .number = duration.seconds}, line);
}
if (operation == OP_SILENCE) {
Value duration = pop_value(compiler);
Node* node = new_node(compiler, NODE_SILENCE, TYPE_AUDIO, NULL, NULL);
if (node == NULL) {
return compile_error(compiler, line, "out of memory");
}
node->first = duration.number;
node->duration = (Duration){true, duration.number};
return push_value(compiler, stream_value(TYPE_AUDIO, node), line);
}
if (operation == OP_MUX) {
Value audio = pop_value(compiler);
Value video = pop_value(compiler);
Value combined = {.type = TYPE_COMBINED, .video = video.video, .audio = audio.audio,
.subtitle_input = -1};
return push_value(compiler, combined, line);
}
if (operation == OP_ATTACH) {
Value subtitles = pop_value(compiler);
Value combined = pop_value(compiler);
combined.subtitle_input = subtitles.subtitle_input;
return push_value(compiler, combined, line);
}
if (operation >= OP_ADD && operation <= OP_GE) {
Value right = pop_value(compiler);
Value left = pop_value(compiler);
Value result = {0};
if (operation == OP_ADD) result.number = left.number + right.number;
if (operation == OP_SUB) result.number = left.number - right.number;
if (operation == OP_MUL) result.number = left.number * right.number;
if (operation == OP_DIV) {
if (right.number == 0) return compile_error(compiler, line, "division by zero");
result.number = left.number / right.number;
}
if (operation >= OP_EQ) {
result.type = TYPE_BOOL;
if (operation == OP_EQ) result.number = left.number == right.number;
if (operation == OP_NE) result.number = left.number != right.number;
if (operation == OP_LT) result.number = left.number < right.number;
if (operation == OP_LE) result.number = left.number <= right.number;
if (operation == OP_GT) result.number = left.number > right.number;
if (operation == OP_GE) result.number = left.number >= right.number;
} else {
result.type = left.type;
if (!isfinite(result.number)) return compile_error(compiler, line, "non-finite arithmetic result");
if (result.type == TYPE_DURATION && result.number < 0) {
return compile_error(compiler, line, "duration arithmetic produced a negative value");
}
}
return push_value(compiler, result, line);
}
if (operation == OP_AND || operation == OP_OR) {
Value right = pop_value(compiler);
Value left = pop_value(compiler);
double result = operation == OP_AND ? left.number && right.number : left.number || right.number;
return push_value(compiler, (Value){.type = TYPE_BOOL, .number = result}, line);
}
if (operation == OP_NOT) {
Value value = pop_value(compiler);
value.number = !value.number;
return push_value(compiler, value, line);
}
Value parameter = pop_value(compiler);
if (operation == OP_SPEED && parameter.number <= 0) {
return compile_error(compiler, line, "speed must be greater than zero");
}
Value right = {0};
Value left = pop_value(compiler);
NodeKind kind;
Node* left_node = stream_node(left);
Node* right_node = NULL;
ValueType result_type = left.type;
if (operation == OP_CONCAT || operation == OP_MIX) {
right = parameter;
right_node = stream_node(right);
kind = operation == OP_CONCAT ? NODE_CONCAT : NODE_MIX;
} else {
switch (operation) {
case OP_SLICE:
right = left;
left = pop_value(compiler);
left_node = stream_node(left);
result_type = left.type;
kind = NODE_SLICE;
break;
case OP_TAKE: kind = NODE_TAKE; break;
case OP_DELAY: kind = NODE_DELAY; break;
case OP_VOLUME: kind = NODE_VOLUME; break;
case OP_SPEED: kind = NODE_SPEED; break;
case OP_FADE_IN: kind = NODE_FADE_IN; break;
case OP_FADE_OUT: kind = NODE_FADE_OUT; break;
default: return compile_error(compiler, line, "unsupported operation");
}
}
Node* node = new_node(compiler, kind, result_type, left_node, right_node);
if (node == NULL) {
return compile_error(compiler, line, "out of memory");
}
node->first = parameter.number;
node->duration = left_node->duration;
if (operation == OP_SLICE) {
node->first = right.number;
node->second = parameter.number;
if (left_node->duration.known) {
double remaining = fmax(0, left_node->duration.seconds - node->first);
node->duration = (Duration){true, fmin(node->second, remaining)};
} else {
node->duration = (Duration){true, node->second};
}
} else if (operation == OP_TAKE) {
node->duration = left_node->duration.known
? (Duration){true, fmin(left_node->duration.seconds, parameter.number)}
: (Duration){true, parameter.number};
} else if (operation == OP_CONCAT) {
node->duration = duration_sum(left_node->duration, right_node->duration);
} else if (operation == OP_DELAY) {
node->duration = left_node->duration.known
? (Duration){true, left_node->duration.seconds + parameter.number}
: (Duration){0};
} else if (operation == OP_MIX) {
node->duration = duration_max(left_node->duration, right_node->duration);
} else if (operation == OP_SPEED) {
node->duration = left_node->duration.known
? (Duration){true, left_node->duration.seconds / parameter.number}
: (Duration){0};
} else if (operation == OP_FADE_OUT) {
if (!left_node->duration.known) {
return compile_error(compiler, line, "fade-out requires a known stream duration");
}
node->second = fmax(0, left_node->duration.seconds - parameter.number);
}
return push_value(compiler, stream_value(result_type, node), line);
}
static bool operation_at(const Program* program, size_t index, Operation operation) {
return program->items[index].kind == INST_OPERATION && program->items[index].as.op == operation;
}
static bool find_if(const Program* program, size_t start, size_t limit,
size_t* else_index, size_t* end_index, Error* error) {
unsigned depth = 1;
*else_index = SIZE_MAX;
for (size_t i = start + 1; i < limit; i++) {
if (operation_at(program, i, OP_IF)) depth++;
if (operation_at(program, i, OP_END) && --depth == 0) {
*end_index = i;
return true;
}
if (operation_at(program, i, OP_ELSE) && depth == 1) *else_index = i;
}
error->line = program->items[start].line;
snprintf(error->message, sizeof(error->message), "if without matching end");
return false;
}
static bool find_loop(const Program* program, size_t start, size_t limit,
size_t* while_index, size_t* repeat_index, Error* error) {
unsigned depth = 1;
*while_index = SIZE_MAX;
for (size_t i = start + 1; i < limit; i++) {
if (operation_at(program, i, OP_BEGIN)) depth++;
if (operation_at(program, i, OP_REPEAT) && --depth == 0) {
*repeat_index = i;
return *while_index != SIZE_MAX;
}
if (operation_at(program, i, OP_WHILE) && depth == 1) *while_index = i;
}
error->line = program->items[start].line;
snprintf(error->message, sizeof(error->message), "begin without matching repeat");
return false;
}
static bool execute_range(Compiler* compiler, const Program* program, size_t start, size_t end) {
for (size_t i = start; i < end; i++) {
Instruction instruction = program->items[i];
if (instruction.kind == INST_SOURCE) {
if (!compile_source(compiler, instruction)) {
return false;
}
} else if (instruction.kind == INST_DURATION || instruction.kind == INST_SCALAR ||
instruction.kind == INST_BOOL) {
Value value = {
.type = instruction.kind == INST_DURATION ? TYPE_DURATION
: instruction.kind == INST_SCALAR ? TYPE_SCALAR : TYPE_BOOL,
.number = instruction.as.number,
.subtitle_input = -1,
};
if (!push_value(compiler, value, instruction.line)) {
return false;
}
} else if (instruction.as.op == OP_IF) {
Value condition = pop_value(compiler);
size_t else_index, end_index;
if (!find_if(program, i, end, &else_index, &end_index, compiler->error)) return false;
if (condition.number != 0) {
size_t true_end = else_index == SIZE_MAX ? end_index : else_index;
if (!execute_range(compiler, program, i + 1, true_end)) return false;
} else if (else_index != SIZE_MAX &&
!execute_range(compiler, program, else_index + 1, end_index)) {
return false;
}
i = end_index;
} else if (instruction.as.op == OP_BEGIN) {
size_t while_index, repeat_index;
if (!find_loop(program, i, end, &while_index, &repeat_index, compiler->error)) return false;
size_t iterations = 0;
for (;;) {
if (!execute_range(compiler, program, i + 1, while_index)) return false;
Value condition = pop_value(compiler);
if (condition.number == 0) break;
if (iterations++ == 10000) {
return compile_error(compiler, instruction.line, "loop exceeded 10000 iterations");
}
if (!execute_range(compiler, program, while_index + 1, repeat_index)) return false;
}
i = repeat_index;
} else if (instruction.as.op == OP_ELSE || instruction.as.op == OP_END ||
instruction.as.op == OP_WHILE || instruction.as.op == OP_REPEAT) {
return compile_error(compiler, instruction.line, "unmatched control-flow marker");
} else if (!compile_operation(compiler, instruction)) {
return false;
}
}
return true;
}
static bool append_separator(StringBuilder* graph) {
return graph->length == 0 || builder_append(graph, ";");
}
static bool node_label(StringBuilder* label, Node* node) {
if (node->kind == NODE_SOURCE && node->consumers == 1) {
return builder_printf(label, "%zu:%c:0", node->source_index,
node->type == TYPE_VIDEO ? 'v' : 'a');
}
unsigned use = node->next_consumer++;
if (node->consumers > 1) {
return builder_printf(label, "n%zu_%u", node->id, use);
}
return builder_printf(label, "n%zu", node->id);
}
static bool append_atempo(StringBuilder* graph, double speed) {
bool first = true;
while (speed > 2.0 + 1e-12) {
if (!first && !builder_append(graph, ",")) return false;
if (!builder_append(graph, "atempo=2")) return false;
speed /= 2.0;
first = false;
}
while (speed < 0.5 - 1e-12) {
if (!first && !builder_append(graph, ",")) return false;
if (!builder_append(graph, "atempo=0.5")) return false;
speed /= 0.5;
first = false;
}
if (!first && !builder_append(graph, ",")) return false;
return builder_printf(graph, "atempo=%.9g", speed);
}
static bool emit_filter(Compiler* compiler, StringBuilder* graph, Node* node) {
StringBuilder left = {0};
StringBuilder right = {0};
bool ok = true;
if (node->input[0] != NULL) ok = node_label(&left, node->input[0]);
if (ok && node->input[1] != NULL) ok = node_label(&right, node->input[1]);
if (!ok || !append_separator(graph)) goto done;
if (node->input[0] != NULL && !builder_printf(graph, "[%s]", left.data)) goto done;
if (node->input[1] != NULL && !builder_printf(graph, "[%s]", right.data)) goto done;
switch (node->kind) {
case NODE_SLICE:
ok = builder_printf(graph, "%s=start=%.9g:duration=%.9g,%s=PTS-STARTPTS",
node->type == TYPE_VIDEO ? "trim" : "atrim", node->first, node->second,
node->type == TYPE_VIDEO ? "setpts" : "asetpts");
break;
case NODE_TAKE:
ok = builder_printf(graph, "%s=duration=%.9g,%s=PTS-STARTPTS",
node->type == TYPE_VIDEO ? "trim" : "atrim", node->first,
node->type == TYPE_VIDEO ? "setpts" : "asetpts");
break;
case NODE_CONCAT:
ok = builder_printf(graph, "concat=n=2:v=%d:a=%d", node->type == TYPE_VIDEO,
node->type == TYPE_AUDIO);
break;
case NODE_DELAY:
ok = node->type == TYPE_VIDEO
? builder_printf(graph, "setpts=PTS+%.9g/TB", node->first)
: builder_printf(graph, "adelay=delays=%.0f:all=1", node->first * 1000.0);
break;
case NODE_VOLUME: ok = builder_printf(graph, "volume=%.9g", node->first); break;
case NODE_MIX: ok = builder_append(graph, "amix=inputs=2:duration=longest"); break;
case NODE_SILENCE:
ok = builder_printf(graph, "anullsrc=r=48000:cl=stereo,atrim=duration=%.9g", node->first);
break;
case NODE_SPEED:
ok = node->type == TYPE_VIDEO ? builder_printf(graph, "setpts=PTS/%.9g", node->first)
: append_atempo(graph, node->first);
break;
case NODE_FADE_IN:
ok = builder_printf(graph, "%s=t=in:st=0:d=%.9g",
node->type == TYPE_VIDEO ? "fade" : "afade", node->first);
break;
case NODE_FADE_OUT:
ok = builder_printf(graph, "%s=t=out:st=%.9g:d=%.9g",
node->type == TYPE_VIDEO ? "fade" : "afade", node->second, node->first);
break;
default: ok = false; break;
}
if (!ok) goto done;
if (node->consumers > 1) {
if (!builder_printf(graph, "[b%zu];[b%zu]%s=%u", node->id, node->id,
node->type == TYPE_VIDEO ? "split" : "asplit", node->consumers)) goto done;
for (unsigned i = 0; i < node->consumers; i++) {
if (!builder_printf(graph, "[n%zu_%u]", node->id, i)) goto done;
}
} else if (!builder_printf(graph, "[n%zu]", node->id)) {
goto done;
}
ok = true;
done:
free(left.data);
free(right.data);
if (!ok) return compile_error(compiler, 0, "out of memory while building filter graph");
return true;
}
static bool emit_source_split(Compiler* compiler, StringBuilder* graph, Node* node) {
if (node->consumers <= 1) return true;
if (!append_separator(graph) ||
!builder_printf(graph, "[%zu:%c:0]%s=%u", node->source_index,
node->type == TYPE_VIDEO ? 'v' : 'a',
node->type == TYPE_VIDEO ? "split" : "asplit", node->consumers)) {
return compile_error(compiler, 0, "out of memory while building filter graph");
}
for (unsigned i = 0; i < node->consumers; i++) {
if (!builder_printf(graph, "[n%zu_%u]", node->id, i)) {
return compile_error(compiler, 0, "out of memory while building filter graph");
}
}
return true;
}
static bool emit_final(Compiler* compiler, StringBuilder* graph, Node* node, const char* label) {
StringBuilder input = {0};
bool ok = node_label(&input, node) && append_separator(graph) &&
builder_printf(graph, "[%s]%s[%s]", input.data,
node->type == TYPE_VIDEO ? "null" : "anull", label);
free(input.data);
if (!ok) return compile_error(compiler, 0, "out of memory while building final mapping");
return true;
}
static void mark_reachable(Node* node) {
if (node == NULL || node->reachable) {
return;
}
node->reachable = true;
for (size_t i = 0; i < 2; i++) {
if (node->input[i] != NULL) {
node->input[i]->consumers++;
mark_reachable(node->input[i]);
}
}
}
static bool compact_inputs(Compiler* compiler, Value* final) {
bool* used = arena_alloc(compiler->arena, compiler->input_count * sizeof(*used));
size_t* remap = arena_alloc(compiler->arena, compiler->input_count * sizeof(*remap));
if ((used == NULL || remap == NULL) && compiler->input_count != 0) {
return compile_error(compiler, 0, "out of memory while compacting inputs");
}
memset(used, 0, compiler->input_count * sizeof(*used));
for (size_t i = 0; i < compiler->node_count; i++) {
Node* node = compiler->nodes[i];
if (node->reachable && node->kind == NODE_SOURCE) {
used[node->source_index] = true;
}
}
if (final->subtitle_input >= 0) {
used[final->subtitle_input] = true;
}
size_t retained = 0;
for (size_t i = 0; i < compiler->input_count; i++) {
if (used[i]) {
remap[i] = retained;
compiler->inputs[retained] = compiler->inputs[i];
compiler->inputs[retained].input_index = retained;
retained++;
}
}
for (size_t i = 0; i < compiler->node_count; i++) {
Node* node = compiler->nodes[i];
if (node->reachable && node->kind == NODE_SOURCE) {
node->source_index = remap[node->source_index];
}
}
if (final->subtitle_input >= 0) {
final->subtitle_input = (ssize_t)remap[final->subtitle_input];
}
compiler->input_count = retained;
return true;
}
static bool build_graph(Compiler* compiler, Value* final, StringBuilder* graph) {
if (final->video != NULL) {
final->video->consumers++;
mark_reachable(final->video);
}
if (final->audio != NULL) {
final->audio->consumers++;
mark_reachable(final->audio);
}
if (!compact_inputs(compiler, final)) {
return false;
}
for (size_t i = 0; i < compiler->node_count; i++) {
Node* node = compiler->nodes[i];
if (!node->reachable) continue;
if (node->kind == NODE_SOURCE) {
if (!emit_source_split(compiler, graph, node)) return false;
} else if (!emit_filter(compiler, graph, node)) {
return false;
}
}
if (final->video != NULL && !emit_final(compiler, graph, final->video, "vout")) return false;
if (final->audio != NULL && !emit_final(compiler, graph, final->audio, "aout")) return false;
return true;
}
static bool add_argument(CompiledCommand* command, size_t* capacity, const char* argument) {
if (command->argc + 1 >= *capacity) {
size_t new_capacity = *capacity == 0 ? 16 : *capacity * 2;
char** argv = arena_alloc_aligned(&command->arena, new_capacity * sizeof(*argv), _Alignof(char* ));
if (argv == NULL) return false;
if (command->argv != NULL) memcpy(argv, command->argv, command->argc * sizeof(*argv));
command->argv = argv;
*capacity = new_capacity;
}
command->argv[command->argc++] = arena_string(&command->arena, argument);
command->argv[command->argc] = NULL;
return command->argv[command->argc - 1] != NULL;
}
static bool shell_safe_char(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
c == '_' || c == '-' || c == '.' || c == '/' || c == ':' || c == '=';
}
static bool append_shell_argument(StringBuilder* builder, const char* argument) {
bool safe = *argument != '\0';
for (const char* p = argument; *p != '\0'; p++) safe = safe && shell_safe_char(*p);
if (safe) return builder_append(builder, argument);
if (!builder_append(builder, "'")) return false;
for (const char* p = argument; *p != '\0'; p++) {
if (*p == '\'') {
if (!builder_append(builder, "'\\''")) return false;
} else {
char character[2] = {*p, '\0'};
if (!builder_append(builder, character)) return false;
}
}
return builder_append(builder, "'");
}
bool compile_program(const Program* program, const TypedProgram* typed, ProbeCache* probes,
const char* output_path, CompiledCommand* out, Error* error) {
(void)typed;
*out = (CompiledCommand){.arena = arena_create()};
Compiler compiler = {.arena = &out->arena, .probes = probes, .error = error};
if (!execute_range(&compiler, program, 0, program->count)) goto failure;
Value final = compiler.stack[0];
StringBuilder graph = {0};
if (!build_graph(&compiler, &final, &graph)) {
free(graph.data);
goto failure;
}
out->filter_complex = arena_string(&out->arena, graph.data == NULL ? "" : graph.data);
free(graph.data);
if (out->filter_complex == NULL) goto memory_failure;
size_t argv_capacity = 0;
#define ARG(text) do { if (!add_argument(out, &argv_capacity, (text))) goto memory_failure; } while (0)
ARG("ffmpeg");
ARG("-y");
for (size_t i = 0; i < compiler.input_count; i++) {
ARG("-i");
ARG(compiler.inputs[i].path);
}
ARG("-filter_complex");
ARG(out->filter_complex);
if (final.video != NULL) { ARG("-map"); ARG("[vout]"); }
if (final.audio != NULL) { ARG("-map"); ARG("[aout]"); }
if (final.subtitle_input >= 0) {
char subtitle_map[64];
snprintf(subtitle_map, sizeof(subtitle_map), "%zd:s:0", final.subtitle_input);
ARG("-map"); ARG(subtitle_map); ARG("-c:s"); ARG("mov_text");
}
if (final.video != NULL) { ARG("-c:v"); ARG("libx264"); }
if (final.audio != NULL) { ARG("-c:a"); ARG("aac"); }
ARG(output_path);
#undef ARG
StringBuilder shell = {0};
for (size_t i = 0; i < out->argc; i++) {
if (i != 0 && !builder_append(&shell, " ")) goto shell_failure;
if (!append_shell_argument(&shell, out->argv[i])) goto shell_failure;
}
out->shell_command = arena_string(&out->arena, shell.data);
free(shell.data);
if (out->shell_command == NULL) goto memory_failure;
return true;
shell_failure:
free(shell.data);
memory_failure:
error->line = 0;
snprintf(error->message, sizeof(error->message), "out of memory");
failure:
compiled_command_destroy(out);
return false;
}
void compiled_command_destroy(CompiledCommand* command) {
arena_destroy(&command->arena);
*command = (CompiledCommand){0};
}
bool execute_command(const CompiledCommand* command, int* exit_code, Error* error) {
pid_t child = fork();
if (child < 0) {
error->line = 0;
snprintf(error->message, sizeof(error->message), "cannot start ffmpeg");
return false;
}
if (child == 0) {
execvp(command->argv[0], command->argv);
_exit(127);
}
int status = 0;
while (waitpid(child, &status, 0) < 0) {
if (errno != EINTR) {
error->line = 0;
snprintf(error->message, sizeof(error->message), "could not wait for ffmpeg");
return false;
}
}
*exit_code = WIFEXITED(status) ? WEXITSTATUS(status) : 128 + WTERMSIG(status);
return true;
}