#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 <strings.h>
#include <sys/types.h>
#include <sys/stat.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,
NODE_METADATA,
} NodeKind;
typedef struct {
const char* key;
const char* value;
} Tag;
typedef struct { Tag* items; size_t count; } TagList;
typedef struct {
double start;
double end;
const char* title;
} Chapter;
typedef struct { Chapter* items; size_t count; } ChapterList;
typedef struct {
bool external;
size_t source_index;
size_t stream_index;
const char* source_path;
const char* filename;
const char* mimetype;
} Attachment;
typedef struct { Attachment* items; size_t count; } AttachmentList;
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;
SubtitleKind subtitle_kind;
const char* codec_name;
const char* language;
const char* title;
unsigned dispositions;
const char* subtitle_path;
const char* burn_style;
const char* fonts_directory;
AttachmentList font_attachments;
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;
TagList tags;
ChapterList chapters;
AttachmentList attachments;
Chapter chapter;
Attachment attachment;
} 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 copy_probe_tags(Compiler* compiler, const ProbeResult* probe, TagList* out) {
if (probe->tag_count == 0) return true;
Tag* items = arena_alloc_aligned(compiler->arena, probe->tag_count * sizeof(*items), _Alignof(Tag));
if (items == NULL) return false;
for (size_t i = 0; i < probe->tag_count; i++) {
items[i] = (Tag){arena_string(compiler->arena, probe->tags[i].key),
arena_string(compiler->arena, probe->tags[i].value)};
if (items[i].key == NULL || items[i].value == NULL) return false;
}
*out = (TagList){items, probe->tag_count};
return true;
}
static bool copy_probe_chapters(Compiler* compiler, const ProbeResult* probe, ChapterList* out) {
if (probe->chapter_count == 0) return true;
Chapter* items = arena_alloc_aligned(compiler->arena, probe->chapter_count * sizeof(*items),
_Alignof(Chapter));
if (items == NULL) return false;
for (size_t i = 0; i < probe->chapter_count; i++) {
items[i] = (Chapter){probe->chapters[i].start, probe->chapters[i].end,
arena_string(compiler->arena, probe->chapters[i].title)};
if (items[i].title == NULL) return false;
}
*out = (ChapterList){items, probe->chapter_count};
return true;
}
static bool copy_probe_attachments(Compiler* compiler, const ProbeResult* probe,
size_t input_index, const char* path, AttachmentList* out) {
if (probe->attachment_count == 0) return true;
Attachment* items = arena_alloc_aligned(compiler->arena,
probe->attachment_count * sizeof(*items), _Alignof(Attachment));
if (items == NULL) return false;
for (size_t i = 0; i < probe->attachment_count; i++) {
items[i] = (Attachment){
.source_index = input_index, .stream_index = probe->attachments[i].stream_index,
.source_path = path,
.filename = arena_string(compiler->arena, probe->attachments[i].filename),
.mimetype = arena_string(compiler->arena, probe->attachments[i].mimetype),
};
if (items[i].filename == NULL || items[i].mimetype == NULL) return false;
}
*out = (AttachmentList){items, probe->attachment_count};
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};
if (!copy_probe_tags(compiler, &probe, &value.tags) ||
!copy_probe_chapters(compiler, &probe, &value.chapters) ||
!copy_probe_attachments(compiler, &probe, input_index, instruction.as.source.path,
&value.attachments)) {
return compile_error(compiler, instruction.line, "out of memory");
}
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);
node->subtitle_kind = streams[i].subtitle_kind;
node->codec_name = arena_string(compiler->arena, streams[i].codec_name);
node->language = arena_string(compiler->arena, streams[i].language);
node->title = arena_string(compiler->arena, streams[i].title);
node->dispositions = streams[i].dispositions;
if (node->codec_name == NULL || node->language == NULL || node->title == NULL) {
return compile_error(compiler, instruction.line, "out of memory");
}
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;
node->subtitle_kind = left->subtitle_kind;
node->codec_name = left->codec_name;
node->language = left->language;
node->title = left->title;
node->dispositions = left->dispositions;
if (type == TYPE_SUBTITLES) {
node->subtitle_is_text = left->subtitle_is_text &&
(right == NULL || right->subtitle_is_text);
if (right != NULL && (left->subtitle_kind != right->subtitle_kind ||
(left->codec_name[0] != '\0' && right->codec_name[0] != '\0' &&
strcmp(left->codec_name, right->codec_name) != 0))) {
compile_error(compiler, line, "subtitle concat requires matching codecs");
return NULL;
}
}
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 Node* metadata_node(Compiler* compiler, Node* source, int line) {
Node* node = new_node(compiler, NODE_METADATA, source->type, source, NULL);
if (node == NULL) { compile_error(compiler, line, "out of memory"); return NULL; }
node->duration = source->duration;
node->subtitle_is_text = source->subtitle_is_text;
node->subtitle_kind = source->subtitle_kind;
node->codec_name = source->codec_name;
node->language = source->language;
node->title = source->title;
node->dispositions = source->dispositions;
return node;
}
static bool chapters_append(Compiler* compiler, ChapterList source, Chapter item, ChapterList* out) {
Chapter* items = arena_alloc_aligned(compiler->arena, (source.count + 1) * sizeof(*items),
_Alignof(Chapter));
if (items == NULL) return false;
if (source.count != 0) memcpy(items, source.items, source.count * sizeof(*items));
items[source.count] = item;
*out = (ChapterList){items, source.count + 1};
return true;
}
static bool chapters_remove(Compiler* compiler, ChapterList source, size_t index, ChapterList* out) {
if (index >= source.count) return false;
Chapter* items = source.count == 1 ? NULL : arena_alloc_aligned(
compiler->arena, (source.count - 1) * sizeof(*items), _Alignof(Chapter));
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 = (ChapterList){items, source.count - 1};
return true;
}
static bool chapters_move(Compiler* compiler, ChapterList source, size_t from, size_t to,
ChapterList* out) {
if (from >= source.count || to >= source.count) return false;
Chapter* items = arena_alloc_aligned(compiler->arena, source.count * sizeof(*items),
_Alignof(Chapter));
if (items == NULL) return false;
memcpy(items, source.items, source.count * sizeof(*items));
Chapter moved = items[from];
if (from < to) memmove(items + from, items + from + 1, (to - from) * sizeof(*items));
if (from > to) memmove(items + to + 1, items + to, (from - to) * sizeof(*items));
items[to] = moved;
*out = (ChapterList){items, source.count};
return true;
}
static bool attachments_append(Compiler* compiler, AttachmentList source, Attachment item,
AttachmentList* out) {
Attachment* items = arena_alloc_aligned(compiler->arena, (source.count + 1) * sizeof(*items),
_Alignof(Attachment));
if (items == NULL) return false;
if (source.count != 0) memcpy(items, source.items, source.count * sizeof(*items));
items[source.count] = item;
*out = (AttachmentList){items, source.count + 1};
return true;
}
static bool attachments_remove(Compiler* compiler, AttachmentList source, size_t index,
AttachmentList* out) {
if (index >= source.count) return false;
Attachment* items = source.count == 1 ? NULL : arena_alloc_aligned(
compiler->arena, (source.count - 1) * sizeof(*items), _Alignof(Attachment));
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 = (AttachmentList){items, source.count - 1};
return true;
}
static bool attachments_move(Compiler* compiler, AttachmentList source, size_t from, size_t to,
AttachmentList* out) {
if (from >= source.count || to >= source.count) return false;
Attachment* items = arena_alloc_aligned(compiler->arena, source.count * sizeof(*items),
_Alignof(Attachment));
if (items == NULL) return false;
memcpy(items, source.items, source.count * sizeof(*items));
Attachment moved = items[from];
if (from < to) memmove(items + from, items + from + 1, (to - from) * sizeof(*items));
if (from > to) memmove(items + to + 1, items + to, (from - to) * sizeof(*items));
items[to] = moved;
*out = (AttachmentList){items, source.count};
return true;
}
static bool tags_set(Compiler* compiler, TagList source, const char* key, const char* value,
TagList* out) {
size_t index = source.count;
for (size_t i = 0; i < source.count; i++) if (strcmp(source.items[i].key, key) == 0) index = i;
size_t count = source.count + (index == source.count);
Tag* items = arena_alloc_aligned(compiler->arena, count * sizeof(*items), _Alignof(Tag));
if (items == NULL) return false;
if (source.count != 0) memcpy(items, source.items, source.count * sizeof(*items));
items[index] = (Tag){key, value};
*out = (TagList){items, count};
return true;
}
static bool transform_chapters(Compiler* compiler, ChapterList source, NodeKind kind,
double first, double second, ChapterList* out) {
Chapter* items = source.count == 0 ? NULL : arena_alloc_aligned(
compiler->arena, source.count * sizeof(*items), _Alignof(Chapter));
if (items == NULL && source.count != 0) return false;
size_t count = 0;
for (size_t i = 0; i < source.count; i++) {
Chapter chapter = source.items[i];
if (kind == NODE_SLICE || kind == NODE_TAKE) {
double start = kind == NODE_SLICE ? first : 0;
double end = start + (kind == NODE_SLICE ? second : first);
if (chapter.end <= start || chapter.start >= end) continue;
chapter.start = fmax(chapter.start, start) - start;
chapter.end = fmin(chapter.end, end) - start;
} else if (kind == NODE_DELAY) {
chapter.start += first; chapter.end += first;
} else if (kind == NODE_SPEED) {
chapter.start /= first; chapter.end /= first;
}
items[count++] = chapter;
}
*out = (ChapterList){items, count};
return true;
}
static bool concat_chapters(Compiler* compiler, ChapterList left, ChapterList right,
double offset, ChapterList* out) {
size_t count = left.count + right.count;
Chapter* items = count == 0 ? NULL : arena_alloc_aligned(
compiler->arena, count * sizeof(*items), _Alignof(Chapter));
if (items == NULL && count != 0) return false;
if (left.count != 0) memcpy(items, left.items, left.count * sizeof(*items));
for (size_t i = 0; i < right.count; i++) {
items[left.count + i] = right.items[i];
items[left.count + i].start += offset;
items[left.count + i].end += offset;
}
*out = (ChapterList){items, count};
return true;
}
static bool union_attachments(Compiler* compiler, AttachmentList left, AttachmentList right,
AttachmentList* out) {
Attachment* items = arena_alloc_aligned(compiler->arena,
(left.count + right.count) * sizeof(*items), _Alignof(Attachment));
if (items == NULL && left.count + right.count != 0) return false;
size_t count = 0;
for (size_t side = 0; side < 2; side++) {
AttachmentList list = side == 0 ? left : right;
for (size_t i = 0; i < list.count; i++) {
bool duplicate = false;
for (size_t j = 0; j < count; j++) {
duplicate = duplicate || (items[j].external == list.items[i].external &&
strcmp(items[j].source_path, list.items[i].source_path) == 0 &&
(items[j].external || items[j].stream_index == list.items[i].stream_index));
}
if (!duplicate) items[count++] = list.items[i];
}
}
*out = (AttachmentList){items, count};
return true;
}
static bool font_attachment(const Attachment* attachment) {
const char* extension = strrchr(attachment->filename, '.');
return strstr(attachment->mimetype, "font") != NULL ||
strstr(attachment->mimetype, "opentype") != NULL ||
(extension != NULL && (strcasecmp(extension, ".ttf") == 0 ||
strcasecmp(extension, ".ttc") == 0 || strcasecmp(extension, ".otf") == 0 ||
strcasecmp(extension, ".otc") == 0 || strcasecmp(extension, ".woff") == 0 ||
strcasecmp(extension, ".woff2") == 0));
}
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));
Value selected = stream_value(compiler, type, list->items[0]);
selected.attachments = combined.attachments;
selected.tags = combined.tags;
selected.chapters = combined.chapters;
if (!push_value(compiler, selected, 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) {
Value selected_value = stream_value(compiler, type, selected);
selected_value.attachments = combined.attachments;
selected_value.tags = combined.tags;
selected_value.chapters = combined.chapters;
if (!push_value(compiler, selected_value, 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,
.tags = video.tags, .chapters = video.chapters};
if (!union_attachments(compiler, video.attachments, audio.attachments,
&combined.attachments)) return compile_error(compiler, line, "out of memory");
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;
node->font_attachments = subtitles.attachments;
if (styled) {
node->burn_style = compile_burn_style(compiler, style.text, line);
if (node->burn_style == NULL) return false;
}
Value result = stream_value(compiler, TYPE_VIDEO, node);
result.tags = video.tags; result.chapters = video.chapters; result.attachments = video.attachments;
return push_value(compiler, result, line);
}
if (operation == OP_SET_LANGUAGE || operation == OP_SET_TITLE ||
operation == OP_SET_DEFAULT || operation == OP_SET_FORCED) {
Value parameter = pop_value(compiler);
Value track = pop_value(compiler);
Node* node = metadata_node(compiler, stream_node(track), line);
if (node == NULL) return false;
if (operation == OP_SET_LANGUAGE) node->language = parameter.text;
if (operation == OP_SET_TITLE) node->title = parameter.text;
if (operation == OP_SET_DEFAULT) {
if (parameter.number != 0) node->dispositions |= DISPOSITION_DEFAULT;
else node->dispositions &= ~DISPOSITION_DEFAULT;
}
if (operation == OP_SET_FORCED) {
if (parameter.number != 0) node->dispositions |= DISPOSITION_FORCED;
else node->dispositions &= ~DISPOSITION_FORCED;
}
Value result = stream_value(compiler, track.type, node);
result.attachments = track.attachments;
result.tags = track.tags;
result.chapters = track.chapters;
return push_value(compiler, result, line);
}
if (operation == OP_CHAPTER) {
Value title = pop_value(compiler);
Value end = pop_value(compiler);
Value start = pop_value(compiler);
if (end.number <= start.number) return compile_error(compiler, line, "chapter end must be after start");
return push_value(compiler, (Value){.type = TYPE_CHAPTER,
.chapter = {start.number, end.number, title.text}}, line);
}
if (operation == OP_ADD_CHAPTER) {
Value chapter = pop_value(compiler);
Value combined = pop_value(compiler);
ChapterList changed;
if (!chapters_append(compiler, combined.chapters, chapter.chapter, &changed))
return compile_error(compiler, line, "out of memory");
combined.chapters = changed;
return push_value(compiler, combined, line);
}
if (operation == OP_DROP_CHAPTER) {
size_t index = (size_t)pop_value(compiler).number;
Value combined = pop_value(compiler);
ChapterList changed;
if (!chapters_remove(compiler, combined.chapters, index, &changed))
return compile_error(compiler, line, "chapter index %zu is out of range", index);
combined.chapters = changed;
return push_value(compiler, combined, line);
}
if (operation == OP_MOVE_CHAPTER) {
size_t to = (size_t)pop_value(compiler).number;
size_t from = (size_t)pop_value(compiler).number;
Value combined = pop_value(compiler);
ChapterList changed;
if (!chapters_move(compiler, combined.chapters, from, to, &changed))
return compile_error(compiler, line, "chapter move indexes are out of range");
combined.chapters = changed;
return push_value(compiler, combined, line);
}
if (operation == OP_ADD_ATTACHMENT) {
Value attachment = pop_value(compiler);
Value combined = pop_value(compiler);
AttachmentList changed;
if (!attachments_append(compiler, combined.attachments, attachment.attachment, &changed))
return compile_error(compiler, line, "out of memory");
combined.attachments = changed;
return push_value(compiler, combined, line);
}
if (operation == OP_DROP_ATTACHMENT) {
size_t index = (size_t)pop_value(compiler).number;
Value combined = pop_value(compiler);
AttachmentList changed;
if (!attachments_remove(compiler, combined.attachments, index, &changed))
return compile_error(compiler, line, "attachment index %zu is out of range", index);
combined.attachments = changed;
return push_value(compiler, combined, line);
}
if (operation == OP_MOVE_ATTACHMENT) {
size_t to = (size_t)pop_value(compiler).number;
size_t from = (size_t)pop_value(compiler).number;
Value combined = pop_value(compiler);
AttachmentList changed;
if (!attachments_move(compiler, combined.attachments, from, to, &changed))
return compile_error(compiler, line, "attachment move indexes are out of range");
combined.attachments = changed;
return push_value(compiler, combined, line);
}
if (operation == OP_SET_TAG) {
Value value = pop_value(compiler);
Value key = pop_value(compiler);
Value combined = pop_value(compiler);
TagList changed;
if (!tags_set(compiler, combined.tags, key.text, value.text, &changed))
return compile_error(compiler, line, "out of memory");
combined.tags = changed;
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;
if (operation == OP_CONCAT && left.type == TYPE_COMBINED) {
Value combined = {.type = TYPE_COMBINED, .tags = left.tags};
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;
Duration left_duration = value_duration(left);
if (!left_duration.known && right.chapters.count != 0)
return compile_error(compiler, line, "combined concat needs known left duration for chapters");
if (!concat_chapters(compiler, left.chapters, right.chapters,
left_duration.known ? left_duration.seconds : 0, &combined.chapters) ||
!union_attachments(compiler, left.attachments, right.attachments,
&combined.attachments)) return compile_error(compiler, line, "out of memory");
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, .tags = left.tags,
.attachments = left.attachments};
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) ||
!transform_chapters(compiler, left.chapters, kind,
operation == OP_SLICE ? right.number : parameter.number,
operation == OP_SLICE ? parameter.number : 0,
&combined.chapters)) 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;
node->subtitle_is_text = left_node->subtitle_is_text &&
(right_node == NULL || right_node->subtitle_is_text);
node->subtitle_kind = left_node->subtitle_kind;
node->codec_name = left_node->codec_name;
node->language = left_node->language;
node->title = left_node->title;
node->dispositions = left_node->dispositions;
if (result_type == TYPE_SUBTITLES && right_node != NULL &&
(left_node->subtitle_kind != right_node->subtitle_kind ||
(left_node->codec_name[0] != '\0' && right_node->codec_name[0] != '\0' &&
strcmp(left_node->codec_name, right_node->codec_name) != 0))) {
return compile_error(compiler, line, "subtitle concat requires matching codecs");
}
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);
}
Value result = stream_value(compiler, result_type, node);
result.tags = left.tags;
result.attachments = left.attachments;
if (operation == OP_CONCAT) {
Duration offset = left_node->duration;
if (!offset.known && right.chapters.count != 0)
return compile_error(compiler, line, "concat needs known left duration for chapters");
if (!concat_chapters(compiler, left.chapters, right.chapters,
offset.known ? offset.seconds : 0, &result.chapters) ||
!union_attachments(compiler, left.attachments, right.attachments, &result.attachments))
return compile_error(compiler, line, "out of memory");
} else if (operation == OP_SLICE || operation == OP_TAKE || operation == OP_DELAY ||
operation == OP_SPEED) {
if (!transform_chapters(compiler, left.chapters, kind,
operation == OP_SLICE ? right.number : parameter.number,
operation == OP_SLICE ? parameter.number : 0, &result.chapters))
return compile_error(compiler, line, "out of memory");
} else result.chapters = left.chapters;
return push_value(compiler, result, 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 || instruction.kind == INST_STRING ||
instruction.kind == INST_ATTACHMENT) {
Value value = {.type = instruction.kind == INST_BURN_STYLE ? TYPE_BURN_STYLE
: instruction.kind == INST_STRING ? TYPE_STRING : TYPE_ATTACHMENT,
.text = instruction.as.text};
if (instruction.kind == INST_ATTACHMENT) {
const char* extension = strrchr(instruction.as.text, '.');
const char* filename = strrchr(instruction.as.text, '/');
filename = filename == NULL ? instruction.as.text : filename + 1;
const char* mimetype = "application/octet-stream";
if (extension != NULL && (strcasecmp(extension, ".ttf") == 0 ||
strcasecmp(extension, ".ttc") == 0))
mimetype = "application/x-truetype-font";
else if (extension != NULL && (strcasecmp(extension, ".otf") == 0 ||
strcasecmp(extension, ".otc") == 0))
mimetype = "application/vnd.ms-opentype";
else if (extension != NULL && strcasecmp(extension, ".woff") == 0) mimetype = "font/woff";
else if (extension != NULL && strcasecmp(extension, ".woff2") == 0) mimetype = "font/woff2";
else if (extension != NULL && (strcasecmp(extension, ".jpg") == 0 ||
strcasecmp(extension, ".jpeg") == 0)) mimetype = "image/jpeg";
else if (extension != NULL && strcasecmp(extension, ".png") == 0) mimetype = "image/png";
value.attachment = (Attachment){.external = true,
.source_path = instruction.as.text, .filename = filename, .mimetype = mimetype};
}
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:%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->fonts_directory != NULL) {
ok = builder_printf(graph, ":fontsdir='%s'", node->fonts_directory);
}
if (ok && node->burn_style != NULL && node->burn_style[0] != '\0') {
ok = builder_printf(graph, ":force_style='%s'", node->burn_style);
}
break;
case NODE_METADATA:
ok = builder_append(graph, node->type == TYPE_VIDEO ? "null" : "anull");
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, 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;
}
}
for (size_t i = 0; i < final->attachments.count; i++) {
if (!final->attachments.items[i].external) used[final->attachments.items[i].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->source_index = remap[node->source_index];
}
}
for (size_t i = 0; i < final->attachments.count; i++) {
if (!final->attachments.items[i].external)
final->attachments.items[i].source_index = remap[final->attachments.items[i].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, final)) {
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 const char* temporary_directory(CompiledCommand* command, Error* error) {
char pattern[] = "/tmp/vedit-fonts-XXXXXX";
if (mkdtemp(pattern) == NULL) {
error->line = 0;
snprintf(error->message, sizeof(error->message), "cannot create font temporary directory");
return NULL;
}
if (command->temporary_directory_count == command->temporary_directory_capacity) {
size_t capacity = command->temporary_directory_capacity == 0 ? 4
: command->temporary_directory_capacity * 2;
const char** paths = arena_alloc_aligned(&command->arena, capacity * sizeof(*paths),
_Alignof(const char*));
if (paths == NULL) { rmdir(pattern); return NULL; }
if (command->temporary_directories != NULL) memcpy(paths, command->temporary_directories,
command->temporary_directory_count * sizeof(*paths));
command->temporary_directories = paths;
command->temporary_directory_capacity = capacity;
}
const char* path = arena_string(&command->arena, pattern);
if (path == NULL) { rmdir(pattern); return NULL; }
command->temporary_directories[command->temporary_directory_count++] = path;
return path;
}
static const char* font_output_path(CompiledCommand* command, const char* directory,
const Attachment* attachment, size_t index) {
const char* filename = strrchr(attachment->filename, '/');
filename = filename == NULL ? attachment->filename : filename + 1;
if (*filename == '\0' || strstr(filename, "..") != NULL) filename = NULL;
char path[4096];
snprintf(path, sizeof(path), "%s/%zu-%s", directory, index,
filename == NULL ? "font.ttf" : filename);
const char* result = arena_string(&command->arena, path);
if (result == NULL) return NULL;
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) 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;
}
command->temporary_paths[command->temporary_count++] = result;
return result;
}
static bool add_subtitle_stage(CompiledCommand* command, Node* node, Compiler* compiler,
const char** output, Error* error) {
if (node->kind == NODE_METADATA) {
return add_subtitle_stage(command, node->input[0], compiler, output, 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) &&
node->subtitle_kind != SUBTITLE_BITMAP) {
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;
stage->concat_list = list;
stage->concat_left = left;
stage->concat_right = right;
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");
}
if (node->subtitle_kind == SUBTITLE_ASS) {
SARG("-c:s"); SARG("ass"); SARG("-f"); SARG("ass");
} else if (node->subtitle_kind == SUBTITLE_BITMAP) {
SARG("-c:s"); SARG("copy"); SARG("-f"); SARG("matroska");
} else {
SARG("-c:s"); SARG("srt"); 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;
}
size_t font_count = 0;
for (size_t i = 0; i < node->font_attachments.count; i++)
font_count += font_attachment(&node->font_attachments.items[i]);
if (font_count != 0) {
node->fonts_directory = temporary_directory(command, error);
if (node->fonts_directory == NULL) return false;
StagedCommand* directory_stage = add_stage(command);
size_t directory_capacity = 0;
if (directory_stage == NULL ||
!add_stage_argument(command, directory_stage, &directory_capacity,
command->executable_path) ||
!add_stage_argument(command, directory_stage, &directory_capacity,
"--internal-mkdir") ||
!add_stage_argument(command, directory_stage, &directory_capacity,
node->fonts_directory)) return false;
for (size_t i = 0; i < node->font_attachments.count; i++) {
Attachment* attachment = &node->font_attachments.items[i];
if (!font_attachment(attachment)) continue;
const char* destination = font_output_path(command, node->fonts_directory, attachment, i);
StagedCommand* stage = add_stage(command);
if (destination == NULL || stage == NULL) return false;
size_t capacity = 0;
#define FARG(text) do { if (!add_stage_argument(command, stage, &capacity, (text))) return false; } while (0)
if (attachment->external) {
FARG(command->executable_path); FARG("--internal-copy-file");
FARG(attachment->source_path); FARG(destination);
} else {
FARG("ffmpeg"); FARG("-y");
char option[64];
snprintf(option, sizeof(option), "-dump_attachment:%zu", attachment->stream_index);
FARG(option); FARG(destination); FARG("-i"); FARG(attachment->source_path);
FARG("-map"); FARG("0:v?"); FARG("-map"); FARG("0:a?");
FARG("-t"); FARG("0"); FARG("-f"); FARG("null"); FARG("-");
}
#undef FARG
}
}
}
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 matroska_path(const char* path) {
const char* extension = strrchr(path, '.');
return extension != NULL && (strcasecmp(extension, ".mkv") == 0 || strcasecmp(extension, ".mka") == 0 ||
strcasecmp(extension, ".mks") == 0);
}
static bool mp4_path(const char* path) {
const char* extension = strrchr(path, '.');
return extension != NULL && (strcasecmp(extension, ".mp4") == 0 || strcasecmp(extension, ".mov") == 0 ||
strcasecmp(extension, ".m4v") == 0);
}
static Node* direct_subtitle_source(Node* node) {
while (node != NULL && node->kind == NODE_METADATA) node = node->input[0];
return node != NULL && node->kind == NODE_SOURCE ? node : NULL;
}
static bool direct_subtitle_compatible(Node* source, const char* output_path) {
if (matroska_path(output_path)) return true;
if (mp4_path(output_path)) return strcmp(source->codec_name, "mov_text") == 0;
const char* extension = strrchr(output_path, '.');
if (extension != NULL && strcasecmp(extension, ".ass") == 0) return source->subtitle_kind == SUBTITLE_ASS;
if (extension != NULL && strcasecmp(extension, ".srt") == 0) return source->subtitle_kind == SUBTITLE_TEXT;
return false;
}
static const char* disposition_text(Arena* arena, unsigned flags) {
if (flags == 0) return "0";
StringBuilder text = {0};
struct { unsigned flag; const char* name; } values[] = {
{DISPOSITION_DEFAULT, "default"}, {DISPOSITION_FORCED, "forced"},
{DISPOSITION_HEARING_IMPAIRED, "hearing_impaired"},
{DISPOSITION_VISUAL_IMPAIRED, "visual_impaired"}, {DISPOSITION_COMMENT, "comment"},
{DISPOSITION_ORIGINAL, "original"},
{DISPOSITION_DUB, "dub"}, {DISPOSITION_LYRICS, "lyrics"},
{DISPOSITION_KARAOKE, "karaoke"}, {DISPOSITION_CLEAN_EFFECTS, "clean_effects"},
{DISPOSITION_ATTACHED_PIC, "attached_pic"},
{DISPOSITION_TIMED_THUMBNAILS, "timed_thumbnails"},
{DISPOSITION_NON_DIEGETIC, "non_diegetic"}, {DISPOSITION_CAPTIONS, "captions"},
{DISPOSITION_DESCRIPTIONS, "descriptions"}, {DISPOSITION_METADATA, "metadata"},
{DISPOSITION_DEPENDENT, "dependent"}, {DISPOSITION_STILL_IMAGE, "still_image"},
{DISPOSITION_MULTILAYER, "multilayer"},
};
for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); i++) if (flags & values[i].flag) {
if (text.length != 0 && !builder_append(&text, "+")) { free(text.data); return NULL; }
if (!builder_append(&text, values[i].name)) { free(text.data); return NULL; }
}
const char* result = arena_string(arena, text.data == NULL ? "0" : text.data);
free(text.data);
return result;
}
static bool append_ffmetadata_text(StringBuilder* builder, const char* text) {
for (const char* p = text; *p != '\0'; p++) {
if (strchr("\\=;#", *p) != NULL && !builder_append(builder, "\\")) return false;
char character[2] = {*p == '\n' || *p == '\r' ? ' ' : *p, '\0'};
if (!builder_append(builder, character)) return false;
}
return true;
}
static const char* chapter_metadata(Arena* arena, ChapterList chapters) {
StringBuilder text = {0};
if (!builder_append(&text, ";FFMETADATA1\n")) return NULL;
for (size_t i = 0; i < chapters.count; i++) {
long long start = llround(chapters.items[i].start * 1000.0);
long long end = llround(chapters.items[i].end * 1000.0);
if (!builder_printf(&text, "[CHAPTER]\nTIMEBASE=1/1000\nSTART=%lld\nEND=%lld\ntitle=", start, end) ||
!append_ffmetadata_text(&text, chapters.items[i].title) || !builder_append(&text, "\n")) {
free(text.data); return NULL;
}
}
const char* result = arena_string(arena, text.data);
free(text.data);
return result;
}
static const char* current_executable(Arena* arena) {
char path[4096];
ssize_t length = readlink("/proc/self/exe", path, sizeof(path) - 1);
if (length < 0 || (size_t)length >= sizeof(path) - 1) return arena_string(arena, "vedit");
path[length] = '\0';
return arena_string(arena, path);
}
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_with_ffmpeg_args(const Program* program, const TypedProgram* typed,
ProbeCache* probes, const char* output_path,
size_t ffmpeg_argument_count,
const char* const* ffmpeg_arguments,
CompiledCommand* out, Error* error) {
(void)typed;
*out = (CompiledCommand){.arena = arena_create()};
out->executable_path = current_executable(&out->arena);
if (out->executable_path == NULL) goto memory_failure;
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;
}
if (final.type != TYPE_COMBINED) final.attachments = (AttachmentList){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;
if (final.attachments.count != 0 && !matroska_path(output_path)) {
compile_error(&compiler, 0, "attachments require a Matroska output");
goto failure;
}
const char** subtitle_paths = final.subtitles.count == 0 ? NULL : arena_alloc_aligned(
&out->arena, final.subtitles.count * sizeof(*subtitle_paths), _Alignof(const char*));
Node** direct_subtitles = final.subtitles.count == 0 ? NULL : arena_alloc_aligned(
&out->arena, final.subtitles.count * sizeof(*direct_subtitles), _Alignof(Node*));
size_t* subtitle_inputs = final.subtitles.count == 0 ? NULL : arena_alloc_aligned(
&out->arena, final.subtitles.count * sizeof(*subtitle_inputs), _Alignof(size_t));
if ((subtitle_paths == NULL || direct_subtitles == NULL || subtitle_inputs == NULL) &&
final.subtitles.count != 0) goto memory_failure;
if (final.subtitles.count != 0) {
memset(subtitle_paths, 0, final.subtitles.count * sizeof(*subtitle_paths));
memset(direct_subtitles, 0, final.subtitles.count * sizeof(*direct_subtitles));
}
for (size_t i = 0; i < final.subtitles.count; i++) {
Node* subtitle = final.subtitles.items[i];
if (subtitle->subtitle_kind == SUBTITLE_BITMAP && !matroska_path(output_path)) {
compile_error(&compiler, 0, "bitmap subtitles require a Matroska output"); goto failure;
}
if (subtitle->subtitle_kind == SUBTITLE_ASS && !matroska_path(output_path) &&
!(strrchr(output_path, '.') != NULL && strcasecmp(strrchr(output_path, '.'), ".ass") == 0)) {
compile_error(&compiler, 0, "ASS/SSA subtitles require Matroska or ASS output"); goto failure;
}
Node* direct = direct_subtitle_source(subtitle);
if (direct != NULL && direct_subtitle_compatible(direct, output_path)) {
direct_subtitles[i] = direct;
} else if (!add_subtitle_stage(out, subtitle, &compiler, &subtitle_paths[i], error)) {
goto failure;
}
}
const char* chapter_path = NULL;
if (final.chapters.count != 0) {
chapter_path = temporary_path(out, error);
const char* contents = chapter_metadata(&out->arena, final.chapters);
StagedCommand* stage = add_stage(out);
if (chapter_path == NULL || contents == NULL || stage == NULL) goto memory_failure;
size_t capacity = 0;
if (!add_stage_argument(out, stage, &capacity, out->executable_path) ||
!add_stage_argument(out, stage, &capacity, "--internal-write-file") ||
!add_stage_argument(out, stage, &capacity, chapter_path) ||
!add_stage_argument(out, stage, &capacity, contents)) 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);
}
size_t next_input = compiler.input_count;
size_t chapter_input = SIZE_MAX;
if (chapter_path != NULL) {
chapter_input = next_input++;
ARG("-f"); ARG("ffmetadata"); ARG("-i"); ARG(chapter_path);
}
for (size_t i = 0; i < final.subtitles.count; i++) if (subtitle_paths[i] != NULL) {
subtitle_inputs[i] = next_input++;
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];
if (direct_subtitles[i] != NULL)
snprintf(subtitle_map, sizeof(subtitle_map), "%zu:%zu", direct_subtitles[i]->source_index,
direct_subtitles[i]->source_stream_index);
else snprintf(subtitle_map, sizeof(subtitle_map), "%zu:s:0", subtitle_inputs[i]);
ARG("-map"); ARG(subtitle_map);
}
for (size_t i = 0; i < final.attachments.count; i++) {
Attachment* attachment = &final.attachments.items[i];
if (attachment->external) { ARG("-attach"); ARG(attachment->source_path); }
else {
char map[64]; snprintf(map, sizeof(map), "%zu:%zu", attachment->source_index,
attachment->stream_index);
ARG("-map"); ARG(map);
}
}
if (final.attachments.count != 0) { ARG("-c:t"); ARG("copy"); }
for (size_t i = 0; i < final.subtitles.count; i++) {
char option[64]; snprintf(option, sizeof(option), "-c:s:%zu", i); ARG(option);
if (direct_subtitles[i] != NULL) ARG("copy");
else if (mp4_path(output_path)) ARG("mov_text");
else if (final.subtitles.items[i]->subtitle_kind == SUBTITLE_ASS) ARG("ass");
else if (final.subtitles.items[i]->subtitle_kind == SUBTITLE_BITMAP) ARG("copy");
else ARG("srt");
}
if (ffmpeg_argument_count == 0) {
if (final.videos.count != 0) { ARG("-c:v"); ARG("libx264"); }
if (final.audios.count != 0) { ARG("-c:a"); ARG("aac"); }
} else {
for (size_t i = 0; i < ffmpeg_argument_count; i++) ARG(ffmpeg_arguments[i]);
}
ARG("-map_metadata"); ARG("-1");
if (chapter_input != SIZE_MAX) {
char input[64]; snprintf(input, sizeof(input), "%zu", chapter_input);
ARG("-map_chapters"); ARG(input);
for (size_t i = 0; i < final.chapters.count; i++) {
char option[64], title[384];
snprintf(option, sizeof(option), "-metadata:c:%zu", i);
snprintf(title, sizeof(title), "title=%s", final.chapters.items[i].title);
ARG(option); ARG(title);
}
} else { ARG("-map_chapters"); ARG("-1"); }
for (size_t i = 0; i < final.tags.count; i++) {
char metadata[384]; snprintf(metadata, sizeof(metadata), "%s=%s",
final.tags.items[i].key, final.tags.items[i].value);
ARG("-metadata"); ARG(metadata);
}
TrackList* metadata_lists[] = {&final.videos, &final.audios, &final.subtitles};
const char* specifiers[] = {"v", "a", "s"};
for (size_t type = 0; type < 3; type++) for (size_t i = 0; i < metadata_lists[type]->count; i++) {
Node* node = metadata_lists[type]->items[i];
char option[64], value[384];
if (node->language != NULL && node->language[0] != '\0') {
snprintf(option, sizeof(option), "-metadata:s:%s:%zu", specifiers[type], i);
snprintf(value, sizeof(value), "language=%s", node->language); ARG(option); ARG(value);
}
if (node->title != NULL && node->title[0] != '\0') {
snprintf(option, sizeof(option), "-metadata:s:%s:%zu", specifiers[type], i);
snprintf(value, sizeof(value), "title=%s", node->title); ARG(option); ARG(value);
}
snprintf(option, sizeof(option), "-disposition:%s:%zu", specifiers[type], i);
const char* disposition = disposition_text(&out->arena, node->dispositions);
if (disposition == NULL) goto memory_failure;
ARG(option); ARG(disposition);
}
for (size_t i = 0; i < final.attachments.count; i++) {
char option[64], value[384];
snprintf(option, sizeof(option), "-metadata:s:t:%zu", i);
snprintf(value, sizeof(value), "filename=%s", final.attachments.items[i].filename);
ARG(option); ARG(value);
snprintf(value, sizeof(value), "mimetype=%s", final.attachments.items[i].mimetype);
ARG(option); ARG(value);
}
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 (stage->concat_list != NULL) {
if (shell.length != 0 && !builder_append(&shell, " && ")) goto shell_failure;
if (!append_shell_argument(&shell, out->executable_path) ||
!builder_append(&shell, " --internal-subtitle-concat-list ") ||
!append_shell_argument(&shell, stage->concat_list) || !builder_append(&shell, " ") ||
!append_shell_argument(&shell, stage->concat_left) || !builder_append(&shell, " ") ||
!append_shell_argument(&shell, stage->concat_right)) goto shell_failure;
}
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 (stage->clip_subtitles) {
char duration[64];
snprintf(duration, sizeof(duration), "%.9g", stage->clip_duration);
if (!builder_append(&shell, " && ") ||
!append_shell_argument(&shell, out->executable_path) ||
!builder_append(&shell, " --internal-subtitle-clip ") ||
!append_shell_argument(&shell, stage->subtitle_output) || !builder_append(&shell, " ") ||
!append_shell_argument(&shell, duration)) 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;
}
bool compile_program(const Program* program, const TypedProgram* typed, ProbeCache* probes,
const char* output_path, CompiledCommand* out, Error* error) {
return compile_program_with_ffmpeg_args(program, typed, probes, output_path, 0, NULL,
out, error);
}
void compiled_command_destroy(CompiledCommand* command) {
for (size_t i = 0; i < command->temporary_count; i++) unlink(command->temporary_paths[i]);
for (size_t i = 0; i < command->temporary_directory_count; i++)
rmdir(command->temporary_directories[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 ass_timestamp(const char* text, long long* value) {
int hours, minutes, seconds, centiseconds;
if (sscanf(text, "%d:%d:%d.%d", &hours, &minutes, &seconds, ¢iseconds) != 4) return false;
*value = (((long long)hours * 60 + minutes) * 60 + seconds) * 1000 + centiseconds * 10;
return true;
}
static void write_ass_timestamp(FILE* file, long long value) {
fprintf(file, "%lld:%02lld:%02lld.%02lld", value / 3600000, value / 60000 % 60,
value / 1000 % 60, value / 10 % 100);
}
static bool ass_clip_stream(FILE* source, FILE* clipped, double duration) {
char* line = NULL;
size_t capacity = 0;
long long limit = llround(duration * 1000.0);
bool ok = true;
while (getline(&line, &capacity, source) >= 0) {
if (strncmp(line, "Dialogue:", 9) != 0) { fputs(line, clipped); continue; }
char* first = strchr(line, ',');
char* second = first == NULL ? NULL : strchr(first + 1, ',');
char* third = second == NULL ? NULL : strchr(second + 1, ',');
if (first == NULL || second == NULL || third == NULL) { ok = false; break; }
*second = '\0'; *third = '\0';
long long start, end;
if (!ass_timestamp(first + 1, &start) || !ass_timestamp(second + 1, &end)) {
ok = false; break;
}
if (start >= limit || end <= 0) continue;
if (start < 0) start = 0;
if (end > limit) end = limit;
fwrite(line, 1, (size_t)(first + 1 - line), clipped);
write_ass_timestamp(clipped, start); fputc(',', clipped);
write_ass_timestamp(clipped, end); fputc(',', clipped);
fputs(third + 1, clipped);
}
free(line);
return ok && !ferror(source) && !ferror(clipped);
}
bool subtitle_clip_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 signature[64] = {0};
size_t signature_count = fread(signature, 1, sizeof(signature) - 1, source);
rewind(source);
if (signature_count != 0 && strstr(signature, "[Script Info]") != NULL) {
bool ok = ass_clip_stream(source, clipped, duration);
fclose(source);
if (!ok) {
fclose(clipped);
error->line = 0;
snprintf(error->message, sizeof(error->message), "cannot parse generated ASS subtitles");
return false;
}
rewind(clipped);
FILE* destination = fopen(path, "wb");
if (destination == NULL) { fclose(clipped); return false; }
char buffer[4096]; size_t count;
while ((count = fread(buffer, 1, sizeof(buffer), clipped)) != 0)
if (fwrite(buffer, 1, count, destination) != count) { fclose(destination); fclose(clipped); return false; }
bool copied = fclose(destination) == 0 && !ferror(clipped);
fclose(clipped);
return copied;
}
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 subtitle_write_concat_list(const char* path, const char* left, const char* right, Error* error) {
FILE* file = fopen(path, "wb");
if (file == NULL) goto failure;
bool wrote = fprintf(file, "file '%s'\nfile '%s'\n", left, right) >= 0;
bool closed = fclose(file) == 0;
if (wrote && closed) return true;
failure:
error->line = 0;
snprintf(error->message, sizeof(error->message), "cannot write subtitle concat list");
return false;
}
bool write_generated_file(const char* path, const char* contents, Error* error) {
FILE* file = fopen(path, "wb");
if (file != NULL) {
size_t length = strlen(contents);
bool wrote = fwrite(contents, 1, length, file) == length;
bool ok = fclose(file) == 0 && wrote;
if (ok) return true;
}
error->line = 0;
snprintf(error->message, sizeof(error->message), "cannot write generated file");
return false;
}
bool copy_generated_file(const char* source, const char* destination, Error* error) {
FILE* input = fopen(source, "rb");
FILE* output = input == NULL ? NULL : fopen(destination, "wb");
bool ok = input != NULL && output != NULL;
char buffer[8192];
while (ok) {
size_t count = fread(buffer, 1, sizeof(buffer), input);
if (count != 0 && fwrite(buffer, 1, count, output) != count) ok = false;
if (count < sizeof(buffer)) { if (ferror(input)) ok = false; break; }
}
if (input != NULL) fclose(input);
if (output != NULL && fclose(output) != 0) ok = false;
if (!ok) {
error->line = 0;
snprintf(error->message, sizeof(error->message), "cannot copy font attachment");
}
return ok;
}
bool ensure_generated_directory(const char* path, Error* error) {
if (mkdir(path, 0700) == 0 || errno == EEXIST) return true;
error->line = 0;
snprintf(error->message, sizeof(error->message), "cannot create generated directory");
return false;
}
bool execute_command(const CompiledCommand* command, int* exit_code, Error* error) {
for (size_t i = 0; i < command->stage_count; i++) {
if (command->stages[i].concat_list != NULL &&
!subtitle_write_concat_list(command->stages[i].concat_list,
command->stages[i].concat_left,
command->stages[i].concat_right, error)) return false;
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 &&
!subtitle_clip_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;
}