#define _POSIX_C_SOURCE 200809L
#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_BURN_SUBTITLES,
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;
size_t source_stream_index;
const char* source_path;
bool subtitle_is_text;
const char* subtitle_path;
const char* burn_style;
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 {
Node** items;
size_t count;
} TrackList;
typedef struct {
ValueType type;
double number;
const char* text;
TrackList videos;
TrackList audios;
TrackList subtitles;
} 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 text_subtitle_codec(const char* codec) {
return codec[0] == '\0' ||
(strcmp(codec, "dvd_subtitle") != 0 && strcmp(codec, "dvb_subtitle") != 0 &&
strcmp(codec, "hdmv_pgs_subtitle") != 0 && strcmp(codec, "xsub") != 0);
}
static TrackList list_single(Compiler* compiler, Node* node) {
Node** items = arena_alloc_aligned(compiler->arena, sizeof(*items), _Alignof(Node*));
if (items != NULL) items[0] = node;
return (TrackList){.items = items, .count = items == NULL ? 0 : 1};
}
static TrackList* value_list(Value* value, ValueType type) {
if (type == TYPE_VIDEO) return &value->videos;
if (type == TYPE_AUDIO) return &value->audios;
return &value->subtitles;
}
static const TrackList* value_const_list(const Value* value, ValueType type) {
if (type == TYPE_VIDEO) return &value->videos;
if (type == TYPE_AUDIO) return &value->audios;
return &value->subtitles;
}
static bool list_copy_with(Compiler* compiler, const TrackList* source, size_t count,
Node* append, TrackList* out) {
size_t total = count + (append != NULL);
Node** items = total == 0 ? NULL : arena_alloc_aligned(
compiler->arena, total * sizeof(*items), _Alignof(Node*));
if (items == NULL && total != 0) return false;
if (count != 0) memcpy(items, source->items, count * sizeof(*items));
if (append != NULL) items[count] = append;
*out = (TrackList){.items = items, .count = total};
return true;
}
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) {
Duration result = {0};
bool any = false;
const TrackList* lists[] = {&value.videos, &value.audios, &value.subtitles};
for (size_t kind = 0; kind < 3; kind++) {
for (size_t i = 0; i < lists[kind]->count; i++) {
Duration current = lists[kind]->items[i]->duration;
if (!any) { result = current; any = true; }
else result = duration_max(result, current);
}
}
return any ? result : (Duration){0};
}
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");
}
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 && !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 && !probe.has_audio) {
return compile_error(compiler, instruction.line, "source '%s' has no audio stream",
instruction.as.source.path);
}
Value value = {.type = (ValueType)instruction.as.source.kind};
ProbeStream legacy[3];
ProbeStream* streams = probe.streams;
size_t stream_count = probe.stream_count;
if (stream_count == 0) {
if (probe.has_video) legacy[stream_count++] = (ProbeStream){
.type = TYPE_VIDEO, .stream_index = 0,
.duration_known = probe.video_duration_known, .duration = probe.video_duration};
if (probe.has_audio) legacy[stream_count++] = (ProbeStream){
.type = TYPE_AUDIO, .stream_index = probe.has_video ? 1 : 0,
.duration_known = probe.audio_duration_known, .duration = probe.audio_duration};
if (instruction.as.source.kind == SRC_SUBTITLES) legacy[stream_count++] = (ProbeStream){
.type = TYPE_SUBTITLES, .stream_index = 0,
.duration_known = probe.subtitle_duration_known, .duration = probe.subtitle_duration};
streams = legacy;
}
for (size_t i = 0; i < stream_count; i++) {
ValueType type = streams[i].type;
bool wanted = instruction.as.source.kind == SRC_COMBINED ||
(ValueType)instruction.as.source.kind == type;
TrackList* list = value_list(&value, type);
if (!wanted || (instruction.as.source.kind != SRC_COMBINED && list->count != 0)) continue;
Node* node = source_node(compiler, type, input_index,
(Duration){streams[i].duration_known, streams[i].duration});
if (node == NULL) return compile_error(compiler, instruction.line, "out of memory");
node->source_stream_index = streams[i].stream_index;
node->source_path = instruction.as.source.path;
node->subtitle_is_text = type != TYPE_SUBTITLES || text_subtitle_codec(streams[i].codec_name);
TrackList grown;
if (!list_copy_with(compiler, list, list->count, node, &grown)) {
return compile_error(compiler, instruction.line, "out of memory");
}
*list = grown;
}
if (instruction.as.source.kind == SRC_SUBTITLES && value.subtitles.count == 0) {
return compile_error(compiler, instruction.line, "source '%s' has no subtitle stream",
instruction.as.source.path);
}
if (instruction.as.source.kind == SRC_COMBINED && value.videos.count == 0 &&
value.audios.count == 0 && value.subtitles.count == 0) {
return compile_error(compiler, instruction.line, "source '%s' has no supported media streams",
instruction.as.source.path);
}
return push_value(compiler, value, instruction.line);
}
static Node* stream_node(Value value) {
const TrackList* list = value_const_list(&value, value.type);
return list->count == 0 ? NULL : list->items[0];
}
static Value stream_value(Compiler* compiler, ValueType type, Node* node) {
Value value = {.type = type};
*value_list(&value, type) = list_single(compiler, 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 ValueType collection_operation_type(Operation operation) {
switch (operation) {
case OP_VIDEO_AT: case OP_TAKE_VIDEO: case OP_DROP_VIDEO: case OP_ADD_VIDEO:
case OP_REPLACE_VIDEO: case OP_MOVE_VIDEO: return TYPE_VIDEO;
case OP_AUDIO_AT: case OP_TAKE_AUDIO: case OP_DROP_AUDIO: case OP_ADD_AUDIO:
case OP_REPLACE_AUDIO: case OP_MOVE_AUDIO: return TYPE_AUDIO;
default: return TYPE_SUBTITLES;
}
}
static bool list_remove(Compiler* compiler, const TrackList* source, size_t index, TrackList* out) {
if (index >= source->count) return false;
Node** items = source->count == 1 ? NULL : arena_alloc_aligned(
compiler->arena, (source->count - 1) * sizeof(*items), _Alignof(Node*));
if (items == NULL && source->count != 1) return false;
if (index != 0) memcpy(items, source->items, index * sizeof(*items));
if (index + 1 < source->count) {
memcpy(items + index, source->items + index + 1,
(source->count - index - 1) * sizeof(*items));
}
*out = (TrackList){items, source->count - 1};
return true;
}
static bool list_replace(Compiler* compiler, const TrackList* source, size_t index,
Node* node, TrackList* out) {
if (index >= source->count || !list_copy_with(compiler, source, source->count, NULL, out)) return false;
out->items[index] = node;
return true;
}
static bool list_move(Compiler* compiler, const TrackList* source, size_t from, size_t to,
TrackList* out) {
if (from >= source->count || to >= source->count ||
!list_copy_with(compiler, source, source->count, NULL, out)) return false;
Node* moved = out->items[from];
if (from < to) memmove(out->items + from, out->items + from + 1, (to - from) * sizeof(Node*));
if (from > to) memmove(out->items + to + 1, out->items + to, (from - to) * sizeof(Node*));
out->items[to] = moved;
return true;
}
static Node* edit_node(Compiler* compiler, NodeKind kind, ValueType type, Node* left, Node* right,
double first, double second, int line) {
Node* node = new_node(compiler, kind, type, left, right);
if (node == NULL) { compile_error(compiler, line, "out of memory"); return NULL; }
node->first = first;
node->second = second;
node->duration = left->duration;
if (type == TYPE_SUBTITLES) {
node->subtitle_is_text = left->subtitle_is_text &&
(right == NULL || right->subtitle_is_text);
}
if (kind == NODE_SLICE) {
if (left->duration.known) {
double remaining = fmax(0, left->duration.seconds - first);
node->duration = (Duration){true, fmin(second, remaining)};
} else node->duration = (Duration){true, second};
} else if (kind == NODE_TAKE) {
node->duration = left->duration.known
? (Duration){true, fmin(left->duration.seconds, first)} : (Duration){true, first};
} else if (kind == NODE_CONCAT) {
node->duration = duration_sum(left->duration, right->duration);
} else if (kind == NODE_DELAY) {
node->duration = left->duration.known
? (Duration){true, left->duration.seconds + first} : (Duration){0};
} else if (kind == NODE_MIX) {
node->duration = duration_max(left->duration, right->duration);
} else if (kind == NODE_SPEED) {
node->duration = left->duration.known
? (Duration){true, left->duration.seconds / first} : (Duration){0};
} else if (kind == NODE_FADE_OUT) {
if (!left->duration.known) {
compile_error(compiler, line, "fade-out requires a known stream duration");
return NULL;
}
node->second = fmax(0, left->duration.seconds - first);
}
return node;
}
static bool transform_list(Compiler* compiler, const TrackList* input, ValueType type,
NodeKind kind, double first, double second, TrackList* output, int line) {
Node** items = input->count == 0 ? NULL : arena_alloc_aligned(
compiler->arena, input->count * sizeof(*items), _Alignof(Node*));
if (items == NULL && input->count != 0) return compile_error(compiler, line, "out of memory");
for (size_t i = 0; i < input->count; i++) {
items[i] = edit_node(compiler, kind, type, input->items[i], NULL, first, second, line);
if (items[i] == NULL) return false;
}
*output = (TrackList){items, input->count};
return true;
}
static bool concat_lists(Compiler* compiler, const TrackList* left, const TrackList* right,
ValueType type, TrackList* output, int line) {
if (left->count != right->count) {
return compile_error(compiler, line, "combined concat requires matching track counts");
}
Node** items = left->count == 0 ? NULL : arena_alloc_aligned(
compiler->arena, left->count * sizeof(*items), _Alignof(Node*));
if (items == NULL && left->count != 0) return compile_error(compiler, line, "out of memory");
for (size_t i = 0; i < left->count; i++) {
items[i] = edit_node(compiler, NODE_CONCAT, type, left->items[i], right->items[i], 0, 0, line);
if (items[i] == NULL) return false;
}
*output = (TrackList){items, left->count};
return true;
}
static char* trim_text(char* text) {
while (*text == ' ' || *text == '\t') text++;
char* end = text + strlen(text);
while (end > text && (end[-1] == ' ' || end[-1] == '\t')) *--end = '\0';
return text;
}
static bool style_number(const char* text, double* value) {
char* end = NULL;
errno = 0;
*value = strtod(text, &end);
return errno == 0 && end != text && *end == '\0' && isfinite(*value);
}
static bool style_color(const char* text, char output[16]) {
size_t length = strlen(text);
unsigned red, green, blue, alpha = 255;
if (length == 4 && text[0] == '#' && sscanf(text + 1, "%1x%1x%1x", &red, &green, &blue) == 3) {
red *= 17; green *= 17; blue *= 17;
} else if (length == 7 && text[0] == '#' &&
sscanf(text + 1, "%2x%2x%2x", &red, &green, &blue) == 3) {
} else if (length == 9 && text[0] == '#' &&
sscanf(text + 1, "%2x%2x%2x%2x", &red, &green, &blue, &alpha) == 4) {
} else return false;
snprintf(output, 16, "&H%02X%02X%02X%02X", 255 - alpha, blue, green, red);
return true;
}
static bool append_style_property(StringBuilder* style, const char* key, const char* value) {
return (style->length == 0 || builder_append(style, ",")) &&
builder_printf(style, "%s=%s", key, value);
}
static const char* compile_burn_style(Compiler* compiler, const char* source, int line) {
char* editable = arena_string(compiler->arena, source);
if (editable == NULL) { compile_error(compiler, line, "out of memory"); return NULL; }
StringBuilder style = {0};
char* state = NULL;
char* bad_item = editable;
for (char* item = strtok_r(editable, ";", &state); item != NULL;
item = strtok_r(NULL, ";", &state)) {
bad_item = item;
item = trim_text(item);
if (*item == '\0') continue;
char* equals = strchr(item, '=');
if (equals == NULL) goto malformed;
*equals = '\0';
char* key = trim_text(item);
char* value = trim_text(equals + 1);
const char* ass_key = NULL;
char converted[64];
if (strcmp(key, "font") == 0) {
if (*value == '\0' || strpbrk(value, ",:'\\") != NULL) goto malformed;
ass_key = "FontName";
} else if (strcmp(key, "size") == 0 || strcmp(key, "outline-width") == 0 ||
strcmp(key, "shadow") == 0 || strcmp(key, "margin") == 0) {
double number;
if (!style_number(value, &number) || number < 0 ||
(strcmp(key, "size") == 0 && number == 0)) goto malformed;
snprintf(converted, sizeof(converted), "%.9g", number);
value = converted;
ass_key = strcmp(key, "size") == 0 ? "FontSize"
: strcmp(key, "outline-width") == 0 ? "Outline"
: strcmp(key, "shadow") == 0 ? "Shadow" : "MarginV";
} else if (strcmp(key, "color") == 0 || strcmp(key, "outline-color") == 0) {
if (!style_color(value, converted)) goto malformed;
value = converted;
ass_key = strcmp(key, "color") == 0 ? "PrimaryColour" : "OutlineColour";
} else if (strcmp(key, "align") == 0) {
struct { const char* name; const char* value; } alignments[] = {
{"bottom-left", "1"}, {"bottom-center", "2"}, {"bottom-right", "3"},
{"center-left", "4"}, {"center", "5"}, {"center-right", "6"},
{"top-left", "7"}, {"top-center", "8"}, {"top-right", "9"},
};
const char* alignment = NULL;
for (size_t i = 0; i < sizeof(alignments) / sizeof(alignments[0]); i++) {
if (strcmp(value, alignments[i].name) == 0) alignment = alignments[i].value;
}
if (alignment == NULL) goto malformed;
value = (char*)alignment;
ass_key = "Alignment";
} else goto malformed;
if (!append_style_property(&style, ass_key, value)) {
free(style.data); compile_error(compiler, line, "out of memory"); return NULL;
}
}
const char* result = arena_string(compiler->arena, style.data == NULL ? "" : style.data);
free(style.data);
if (result == NULL) compile_error(compiler, line, "out of memory");
return result;
malformed:
free(style.data);
compile_error(compiler, line, "invalid burn style near '%s'", bad_item);
return NULL;
}
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_GET_SUBTITLES || operation == OP_SPLIT) {
Value combined = pop_value(compiler);
ValueType type = operation == OP_GET_AUDIO ? TYPE_AUDIO
: operation == OP_GET_SUBTITLES ? TYPE_SUBTITLES : TYPE_VIDEO;
const TrackList* list = value_const_list(&combined, type);
if (list->count == 0) return compile_error(compiler, line, "combined value has no %s track", type_name(type));
if (!push_value(compiler, stream_value(compiler, type, list->items[0]), line)) return false;
if (operation == OP_SPLIT) {
if (combined.audios.count == 0) return compile_error(compiler, line, "combined value has no AUDIO track");
if (!push_value(compiler, stream_value(compiler, TYPE_AUDIO, combined.audios.items[0]), line)) return false;
}
return true;
}
if ((operation >= OP_VIDEO_AT && operation <= OP_TAKE_SUBTITLES) ||
(operation >= OP_DROP_VIDEO && operation <= OP_MOVE_SUBTITLES)) {
ValueType type = collection_operation_type(operation);
if (operation >= OP_VIDEO_AT && operation <= OP_DROP_SUBTITLES) {
size_t index = (size_t)pop_value(compiler).number;
Value combined = pop_value(compiler);
TrackList* list = value_list(&combined, type);
if (index >= list->count) return compile_error(compiler, line, "%s index %zu is out of range", type_name(type), index);
Node* selected = list->items[index];
bool removes = operation >= OP_TAKE_VIDEO && operation <= OP_DROP_SUBTITLES;
if (removes) {
TrackList changed;
if (!list_remove(compiler, list, index, &changed)) return compile_error(compiler, line, "out of memory");
*list = changed;
}
if (!push_value(compiler, combined, line)) return false;
if (operation <= OP_TAKE_SUBTITLES && !push_value(
compiler, stream_value(compiler, type, selected), line)) return false;
return true;
}
if (operation >= OP_ADD_VIDEO && operation <= OP_ADD_SUBTITLES) {
Value track = pop_value(compiler);
Value combined = pop_value(compiler);
TrackList* list = value_list(&combined, type);
TrackList changed;
if (!list_copy_with(compiler, list, list->count, stream_node(track), &changed)) {
return compile_error(compiler, line, "out of memory");
}
*list = changed;
return push_value(compiler, combined, line);
}
if (operation >= OP_REPLACE_VIDEO && operation <= OP_REPLACE_SUBTITLES) {
Value track = pop_value(compiler);
size_t index = (size_t)pop_value(compiler).number;
Value combined = pop_value(compiler);
TrackList* list = value_list(&combined, type);
TrackList changed;
if (!list_replace(compiler, list, index, stream_node(track), &changed)) {
return compile_error(compiler, line, "%s index %zu is out of range", type_name(type), index);
}
*list = changed;
return push_value(compiler, combined, line);
}
size_t to = (size_t)pop_value(compiler).number;
size_t from = (size_t)pop_value(compiler).number;
Value combined = pop_value(compiler);
TrackList* list = value_list(&combined, type);
TrackList changed;
if (!list_move(compiler, list, from, to, &changed)) {
return compile_error(compiler, line, "%s move indexes are out of range", type_name(type));
}
*list = changed;
return push_value(compiler, combined, line);
}
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(compiler, TYPE_AUDIO, node), line);
}
if (operation == OP_MUX) {
Value audio = pop_value(compiler);
Value video = pop_value(compiler);
Value combined = {.type = TYPE_COMBINED, .videos = video.videos, .audios = audio.audios};
return push_value(compiler, combined, line);
}
if (operation == OP_ATTACH_SUBTITLES) {
Value subtitles = pop_value(compiler);
Value combined = pop_value(compiler);
TrackList changed;
if (!list_copy_with(compiler, &combined.subtitles, combined.subtitles.count,
stream_node(subtitles), &changed)) return compile_error(compiler, line, "out of memory");
combined.subtitles = changed;
return push_value(compiler, combined, line);
}
if (operation == OP_VIDEO_TO_COMBINED) {
Value video = pop_value(compiler);
video.type = TYPE_COMBINED;
return push_value(compiler, video, line);
}
if (operation == OP_BURN_SUBTITLES) {
Value style = {0};
bool styled = compiler->stack[compiler->stack_count - 1].type == TYPE_BURN_STYLE;
if (styled) style = pop_value(compiler);
Value subtitles = pop_value(compiler);
Value video = pop_value(compiler);
Node* subtitle_node = stream_node(subtitles);
if (!subtitle_node->subtitle_is_text) {
return compile_error(compiler, line, "burn-subtitles supports text subtitles only");
}
Node* node = new_node(compiler, NODE_BURN_SUBTITLES, TYPE_VIDEO,
stream_node(video), subtitle_node);
if (node == NULL) return compile_error(compiler, line, "out of memory");
node->duration = stream_node(video)->duration;
if (styled) {
node->burn_style = compile_burn_style(compiler, style.text, line);
if (node->burn_style == NULL) return false;
}
return push_value(compiler, stream_value(compiler, TYPE_VIDEO, node), 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;
if (operation == OP_CONCAT && left.type == TYPE_COMBINED) {
Value combined = {.type = TYPE_COMBINED};
if (!concat_lists(compiler, &left.videos, &right.videos, TYPE_VIDEO, &combined.videos, line) ||
!concat_lists(compiler, &left.audios, &right.audios, TYPE_AUDIO, &combined.audios, line) ||
!concat_lists(compiler, &left.subtitles, &right.subtitles, TYPE_SUBTITLES,
&combined.subtitles, line)) return false;
return push_value(compiler, combined, line);
}
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");
}
}
if (left.type == TYPE_COMBINED) {
Value combined = {.type = TYPE_COMBINED};
if (!transform_list(compiler, &left.videos, TYPE_VIDEO, kind,
operation == OP_SLICE ? right.number : parameter.number,
operation == OP_SLICE ? parameter.number : 0, &combined.videos, line) ||
!transform_list(compiler, &left.audios, TYPE_AUDIO, kind,
operation == OP_SLICE ? right.number : parameter.number,
operation == OP_SLICE ? parameter.number : 0, &combined.audios, line) ||
!transform_list(compiler, &left.subtitles, TYPE_SUBTITLES, kind,
operation == OP_SLICE ? right.number : parameter.number,
operation == OP_SLICE ? parameter.number : 0, &combined.subtitles, line)) return false;
return push_value(compiler, combined, line);
}
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(compiler, 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 || instruction.kind == INST_INDEX) {
Value value = {
.type = instruction.kind == INST_DURATION ? TYPE_DURATION
: instruction.kind == INST_SCALAR ? TYPE_SCALAR
: instruction.kind == INST_INDEX ? TYPE_INDEX : TYPE_BOOL,
.number = instruction.as.number,
};
if (!push_value(compiler, value, instruction.line)) {
return false;
}
} else if (instruction.kind == INST_BURN_STYLE) {
if (!push_value(compiler, (Value){.type = TYPE_BURN_STYLE, .text = instruction.as.text},
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:%zu", node->source_index, node->source_stream_index);
}
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 && node->kind != NODE_BURN_SUBTITLES) {
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 && node->kind != NODE_BURN_SUBTITLES &&
!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_BURN_SUBTITLES:
ok = builder_printf(graph, "subtitles=filename='%s'", node->subtitle_path);
if (ok && node->burn_style != NULL && node->burn_style[0] != '\0') {
ok = builder_printf(graph, ":force_style='%s'", node->burn_style);
}
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:%zu]%s=%u", node->source_index, node->source_stream_index,
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) {
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 && node->type != TYPE_SUBTITLES) {
used[node->source_index] = 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->type != TYPE_SUBTITLES) {
node->source_index = remap[node->source_index];
}
}
compiler->input_count = retained;
return true;
}
static bool build_graph(Compiler* compiler, Value* final, StringBuilder* graph) {
TrackList* lists[] = {&final->videos, &final->audios, &final->subtitles};
for (size_t kind = 0; kind < 3; kind++) {
for (size_t i = 0; i < lists[kind]->count; i++) {
lists[kind]->items[i]->consumers++;
mark_reachable(lists[kind]->items[i]);
}
}
if (!compact_inputs(compiler)) {
return false;
}
for (size_t i = 0; i < compiler->node_count; i++) {
Node* node = compiler->nodes[i];
if (!node->reachable) continue;
if (node->type == TYPE_SUBTITLES) {
continue;
} else if (node->kind == NODE_SOURCE) {
if (!emit_source_split(compiler, graph, node)) return false;
} else if (!emit_filter(compiler, graph, node)) {
return false;
}
}
for (size_t i = 0; i < final->videos.count; i++) {
char label[64];
snprintf(label, sizeof(label), i == 0 ? "vout" : "vout%zu", i);
if (!emit_final(compiler, graph, final->videos.items[i], label)) return false;
}
for (size_t i = 0; i < final->audios.count; i++) {
char label[64];
snprintf(label, sizeof(label), i == 0 ? "aout" : "aout%zu", i);
if (!emit_final(compiler, graph, final->audios.items[i], label)) 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 StagedCommand* add_stage(CompiledCommand* command) {
if (command->stage_count == command->stage_capacity) {
size_t capacity = command->stage_capacity == 0 ? 8 : command->stage_capacity * 2;
StagedCommand* stages = arena_alloc_aligned(&command->arena, capacity * sizeof(*stages),
_Alignof(StagedCommand));
if (stages == NULL) return NULL;
if (command->stages != NULL) {
memcpy(stages, command->stages, command->stage_count * sizeof(*stages));
}
command->stages = stages;
command->stage_capacity = capacity;
}
StagedCommand* stage = &command->stages[command->stage_count++];
*stage = (StagedCommand){0};
return stage;
}
static bool add_stage_argument(CompiledCommand* command, StagedCommand* stage,
size_t* capacity, const char* argument) {
if (stage->argc + 1 >= *capacity) {
size_t grown_capacity = *capacity == 0 ? 16 : *capacity * 2;
char** argv = arena_alloc_aligned(&command->arena, grown_capacity * sizeof(*argv),
_Alignof(char*));
if (argv == NULL) return false;
if (stage->argv != NULL) memcpy(argv, stage->argv, stage->argc * sizeof(*argv));
stage->argv = argv;
*capacity = grown_capacity;
}
stage->argv[stage->argc++] = arena_string(&command->arena, argument);
stage->argv[stage->argc] = NULL;
return stage->argv[stage->argc - 1] != NULL;
}
static const char* temporary_path(CompiledCommand* command, Error* error) {
char pattern[] = "/tmp/vedit-subtitle-XXXXXX";
int fd = mkstemp(pattern);
if (fd < 0) {
error->line = 0;
snprintf(error->message, sizeof(error->message), "cannot create subtitle temporary file");
return NULL;
}
close(fd);
if (command->temporary_count == command->temporary_capacity) {
size_t capacity = command->temporary_capacity == 0 ? 8 : command->temporary_capacity * 2;
const char** paths = arena_alloc_aligned(&command->arena, capacity * sizeof(*paths),
_Alignof(const char*));
if (paths == NULL) { unlink(pattern); return NULL; }
if (command->temporary_paths != NULL) {
memcpy(paths, command->temporary_paths, command->temporary_count * sizeof(*paths));
}
command->temporary_paths = paths;
command->temporary_capacity = capacity;
}
const char* path = arena_string(&command->arena, pattern);
if (path == NULL) { unlink(pattern); return NULL; }
command->temporary_paths[command->temporary_count++] = path;
return path;
}
static bool add_subtitle_stage(CompiledCommand* command, Node* node, Compiler* compiler,
const char** output, Error* error) {
const char* left = NULL;
const char* right = NULL;
if (node->kind != NODE_SOURCE &&
!add_subtitle_stage(command, node->input[0], compiler, &left, error)) return false;
if (node->kind == NODE_CONCAT &&
!add_subtitle_stage(command, node->input[1], compiler, &right, error)) return false;
const char* path = temporary_path(command, error);
if (path == NULL) return false;
StagedCommand* stage = add_stage(command);
if (stage == NULL) return false;
stage->subtitle_output = path;
if (node->kind == NODE_SLICE || node->kind == NODE_TAKE) {
stage->clip_subtitles = true;
stage->clip_duration = node->duration.seconds;
}
size_t capacity = 0;
#define SARG(text) do { if (!add_stage_argument(command, stage, &capacity, (text))) return false; } while (0)
SARG("ffmpeg"); SARG("-y");
char number[64];
if (node->kind == NODE_SOURCE) {
SARG("-i"); SARG(node->source_path);
snprintf(number, sizeof(number), "0:%zu", node->source_stream_index);
SARG("-map"); SARG(number);
} else if (node->kind == NODE_SLICE) {
snprintf(number, sizeof(number), "%.9g", node->first);
SARG("-ss"); SARG(number);
snprintf(number, sizeof(number), "%.9g", node->second);
SARG("-t"); SARG(number); SARG("-i"); SARG(left); SARG("-map"); SARG("0:s:0");
} else if (node->kind == NODE_TAKE) {
snprintf(number, sizeof(number), "%.9g", node->first);
SARG("-t"); SARG(number); SARG("-i"); SARG(left); SARG("-map"); SARG("0:s:0");
} else if (node->kind == NODE_DELAY) {
snprintf(number, sizeof(number), "%.9g", node->first);
SARG("-itsoffset"); SARG(number); SARG("-i"); SARG(left); SARG("-map"); SARG("0:s:0");
} else if (node->kind == NODE_SPEED) {
snprintf(number, sizeof(number), "%.9g", 1.0 / node->first);
SARG("-itsscale"); SARG(number); SARG("-i"); SARG(left); SARG("-map"); SARG("0:s:0");
} else if (node->kind == NODE_CONCAT) {
const char* list = temporary_path(command, error);
if (list == NULL) return false;
FILE* file = fopen(list, "wb");
if (file == NULL) return compile_error(compiler, 0, "cannot write subtitle concat list");
bool wrote = fprintf(file, "file '%s'\nfile '%s'\n", left, right) >= 0;
bool closed = fclose(file) == 0;
if (!wrote || !closed) {
return compile_error(compiler, 0, "cannot write subtitle concat list");
}
SARG("-f"); SARG("concat"); SARG("-safe"); SARG("0"); SARG("-i"); SARG(list);
SARG("-map"); SARG("0:s:0");
} else {
return compile_error(compiler, 0, "unsupported subtitle operation");
}
SARG("-f"); SARG("srt"); SARG(path);
#undef SARG
*output = path;
return true;
}
static bool prepare_burn_subtitles(CompiledCommand* command, Node* node, Compiler* compiler,
Error* error) {
if (node == NULL || node->type == TYPE_SUBTITLES) return true;
if (node->kind == NODE_BURN_SUBTITLES && node->subtitle_path == NULL) {
if (!add_subtitle_stage(command, node->input[1], compiler, &node->subtitle_path, error)) {
return false;
}
}
if (!prepare_burn_subtitles(command, node->input[0], compiler, error)) return false;
return node->kind == NODE_BURN_SUBTITLES ||
prepare_burn_subtitles(command, node->input[1], compiler, error);
}
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 const char* output_subtitle_codec(const char* path) {
const char* extension = strrchr(path, '.');
if (extension != NULL &&
(strcmp(extension, ".mp4") == 0 || strcmp(extension, ".mov") == 0 ||
strcmp(extension, ".m4v") == 0)) return "mov_text";
return "srt";
}
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];
if (final.videos.count == 0 && final.audios.count == 0 && final.subtitles.count == 0) {
compile_error(&compiler, 0, "final media value contains no tracks");
goto failure;
}
for (size_t i = 0; i < final.videos.count; i++) {
if (!prepare_burn_subtitles(out, final.videos.items[i], &compiler, error)) goto failure;
}
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;
const char** subtitle_paths = final.subtitles.count == 0 ? NULL : arena_alloc_aligned(
&out->arena, final.subtitles.count * sizeof(*subtitle_paths), _Alignof(const char*));
if (subtitle_paths == NULL && final.subtitles.count != 0) goto memory_failure;
for (size_t i = 0; i < final.subtitles.count; i++) {
if (!add_subtitle_stage(out, final.subtitles.items[i], &compiler, &subtitle_paths[i], error)) {
goto 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);
}
for (size_t i = 0; i < final.subtitles.count; i++) { ARG("-i"); ARG(subtitle_paths[i]); }
if (out->filter_complex[0] != '\0') { ARG("-filter_complex"); ARG(out->filter_complex); }
for (size_t i = 0; i < final.videos.count; i++) {
char label[64];
snprintf(label, sizeof(label), i == 0 ? "[vout]" : "[vout%zu]", i);
ARG("-map"); ARG(label);
}
for (size_t i = 0; i < final.audios.count; i++) {
char label[64];
snprintf(label, sizeof(label), i == 0 ? "[aout]" : "[aout%zu]", i);
ARG("-map"); ARG(label);
}
for (size_t i = 0; i < final.subtitles.count; i++) {
char subtitle_map[64];
snprintf(subtitle_map, sizeof(subtitle_map), "%zu:s:0", compiler.input_count + i);
ARG("-map"); ARG(subtitle_map);
}
if (final.subtitles.count != 0) { ARG("-c:s"); ARG(output_subtitle_codec(output_path)); }
if (final.videos.count != 0) { ARG("-c:v"); ARG("libx264"); }
if (final.audios.count != 0) { ARG("-c:a"); ARG("aac"); }
ARG(output_path);
#undef ARG
StringBuilder shell = {0};
for (size_t stage_index = 0; stage_index < out->stage_count; stage_index++) {
StagedCommand* stage = &out->stages[stage_index];
if (shell.length != 0 && !builder_append(&shell, " && ")) goto shell_failure;
for (size_t i = 0; i < stage->argc; i++) {
if (i != 0 && !builder_append(&shell, " ")) goto shell_failure;
if (!append_shell_argument(&shell, stage->argv[i])) goto shell_failure;
}
}
if (shell.length != 0 && !builder_append(&shell, " && ")) goto shell_failure;
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) {
for (size_t i = 0; i < command->temporary_count; i++) unlink(command->temporary_paths[i]);
arena_destroy(&command->arena);
*command = (CompiledCommand){0};
}
static bool subtitle_timestamp(const char* line, long long* start, long long* end) {
int sh, sm, ss, sms, eh, em, es, ems;
if (sscanf(line, "%d:%d:%d,%d --> %d:%d:%d,%d",
&sh, &sm, &ss, &sms, &eh, &em, &es, &ems) != 8) return false;
*start = (((long long)sh * 60 + sm) * 60 + ss) * 1000 + sms;
*end = (((long long)eh * 60 + em) * 60 + es) * 1000 + ems;
return true;
}
static void write_subtitle_timestamp(FILE* file, long long start, long long end) {
fprintf(file, "%02lld:%02lld:%02lld,%03lld --> %02lld:%02lld:%02lld,%03lld\n",
start / 3600000, start / 60000 % 60, start / 1000 % 60, start % 1000,
end / 3600000, end / 60000 % 60, end / 1000 % 60, end % 1000);
}
static bool clip_subtitle_file(const char* path, double duration, Error* error) {
FILE* source = fopen(path, "rb");
FILE* clipped = tmpfile();
if (source == NULL || clipped == NULL) {
if (source != NULL) fclose(source);
if (clipped != NULL) fclose(clipped);
error->line = 0;
snprintf(error->message, sizeof(error->message), "cannot post-process subtitles");
return false;
}
char* line = NULL;
size_t capacity = 0;
unsigned number = 1;
long long limit = llround(duration * 1000.0);
while (getline(&line, &capacity, source) >= 0) {
if (line[0] == '\n' || line[0] == '\r') continue;
if (getline(&line, &capacity, source) < 0) break;
long long start, end;
if (!subtitle_timestamp(line, &start, &end)) {
free(line); fclose(source); fclose(clipped);
error->line = 0;
snprintf(error->message, sizeof(error->message), "cannot parse generated subtitles");
return false;
}
bool keep = start < limit && end > 0;
if (end > limit) end = limit;
if (keep) {
fprintf(clipped, "%u\n", number++);
write_subtitle_timestamp(clipped, start < 0 ? 0 : start, end);
}
while (getline(&line, &capacity, source) >= 0 && line[0] != '\n' && line[0] != '\r') {
if (keep) fputs(line, clipped);
}
if (keep) fputc('\n', clipped);
}
free(line);
fclose(source);
rewind(clipped);
FILE* destination = fopen(path, "wb");
if (destination == NULL) {
fclose(clipped);
error->line = 0;
snprintf(error->message, sizeof(error->message), "cannot rewrite generated subtitles");
return false;
}
char buffer[4096];
size_t count;
bool ok = true;
while ((count = fread(buffer, 1, sizeof(buffer), clipped)) != 0) {
if (fwrite(buffer, 1, count, destination) != count) { ok = false; break; }
}
ok = ok && !ferror(clipped);
if (fclose(destination) != 0) ok = false;
fclose(clipped);
if (!ok) {
error->line = 0;
snprintf(error->message, sizeof(error->message), "cannot rewrite generated subtitles");
}
return ok;
}
bool execute_command(const CompiledCommand* command, int* exit_code, Error* error) {
for (size_t i = 0; i < command->stage_count; i++) {
pid_t stage_child = fork();
if (stage_child < 0) {
error->line = 0;
snprintf(error->message, sizeof(error->message), "cannot start subtitle processing stage");
return false;
}
if (stage_child == 0) {
execvp(command->stages[i].argv[0], command->stages[i].argv);
_exit(127);
}
int stage_status = 0;
pid_t waited;
do { waited = waitpid(stage_child, &stage_status, 0); } while (waited < 0 && errno == EINTR);
if (waited < 0) {
error->line = 0;
snprintf(error->message, sizeof(error->message), "could not wait for subtitle stage");
return false;
}
if (!WIFEXITED(stage_status) || WEXITSTATUS(stage_status) != 0) {
*exit_code = WIFEXITED(stage_status) ? WEXITSTATUS(stage_status)
: 128 + WTERMSIG(stage_status);
return true;
}
if (command->stages[i].clip_subtitles &&
!clip_subtitle_file(command->stages[i].subtitle_output,
command->stages[i].clip_duration, error)) return false;
}
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;
}