#include "compiler.h" #include "platform.h" #include #include #include #include #include #include #include #ifdef _WIN32 #define strcasecmp _stricmp #else #include #endif static long portable_getline(char** line, size_t* capacity, FILE* file) { if (*line == NULL || *capacity == 0) { *capacity = 256; *line = malloc(*capacity); if (*line == NULL) return -1; } size_t length = 0; int character; while ((character = fgetc(file)) != EOF) { if (length + 1 >= *capacity) { if (*capacity > SIZE_MAX / 2) return -1; size_t grown_capacity = *capacity * 2; char* grown = realloc(*line, grown_capacity); if (grown == NULL) return -1; *line = grown; *capacity = grown_capacity; } (*line)[length++] = (char)character; if (character == '\n') break; } if (length == 0 && character == EOF) return -1; (*line)[length] = '\0'; return length > LONG_MAX ? LONG_MAX : (long)length; } 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 { const char** items; size_t count; } ArgumentList; 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; bool encoding_set; ArgumentList encoding_arguments; ArgumentList ffmpeg_arguments; 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 bool argument_space(char character) { return character == ' ' || character == '\t' || character == '\r' || character == '\n'; } static bool parse_argument_string(Compiler* compiler, const char* source, ArgumentList* target, bool append, int line) { ArgumentList parsed = {0}; size_t parsed_capacity = 0; size_t offset = 0; while (source[offset] != '\0') { while (argument_space(source[offset])) offset++; if (source[offset] == '\0') break; StringBuilder token = {0}; bool single_quoted = false; bool started = false; while (source[offset] != '\0') { char character = source[offset]; if (character == '\'') { single_quoted = !single_quoted; started = true; offset++; } else if (!single_quoted && argument_space(character)) { break; } else { char text[2] = {character, '\0'}; if (!builder_append(&token, text)) { free(token.data); return compile_error(compiler, line, "out of memory"); } started = true; offset++; } } if (single_quoted) { free(token.data); return compile_error(compiler, line, "unterminated single quote in FFmpeg arguments"); } if (started) { if (!grow_array(compiler->arena, (void**)&parsed.items, parsed.count, &parsed_capacity, sizeof(*parsed.items), _Alignof(const char*))) { free(token.data); return compile_error(compiler, line, "out of memory"); } parsed.items[parsed.count] = arena_string(compiler->arena, token.data == NULL ? "" : token.data); free(token.data); if (parsed.items[parsed.count++] == NULL) return compile_error(compiler, line, "out of memory"); } while (argument_space(source[offset])) offset++; } if (!append || target->count == 0) { *target = parsed; return true; } size_t total = target->count + parsed.count; const char** items = total == 0 ? NULL : arena_alloc_aligned( compiler->arena, total * sizeof(*items), _Alignof(const char*)); if (items == NULL && total != 0) return compile_error(compiler, line, "out of memory"); memcpy(items, target->items, target->count * sizeof(*items)); if (parsed.count != 0) memcpy(items + target->count, parsed.items, parsed.count * sizeof(*items)); *target = (ArgumentList){items, total}; 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* bad_item = editable; for (char* item = editable; item != NULL;) { char* separator = strchr(item, ';'); if (separator != NULL) *separator = '\0'; bad_item = item; item = trim_text(item); if (*item == '\0') { item = separator == NULL ? NULL : separator + 1; 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; } item = separator == NULL ? NULL : separator + 1; } 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_ENCODING) { compiler->encoding_set = true; if (!parse_argument_string(compiler, instruction.as.text, &compiler->encoding_arguments, false, instruction.line)) return false; } else if (instruction.kind == INST_FFMPEG_ARGS) { if (!parse_argument_string(compiler, instruction.as.text, &compiler->ffmpeg_arguments, true, instruction.line)) return false; } else 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[4096]; if (!platform_temporary_file(pattern, sizeof(pattern))) { error->line = 0; snprintf(error->message, sizeof(error->message), "cannot create subtitle temporary file"); 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) { platform_delete_file(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) { platform_delete_file(pattern); return NULL; } command->temporary_paths[command->temporary_count++] = path; return path; } static const char* temporary_directory(CompiledCommand* command, Error* error) { char pattern[4096]; if (!platform_temporary_directory(pattern, sizeof(pattern))) { 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) { platform_remove_directory(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) { platform_remove_directory(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* slash = strrchr(attachment->filename, '/'); const char* backslash = strrchr(attachment->filename, '\\'); const char* filename = slash == NULL || (backslash != NULL && backslash > slash) ? backslash : slash; filename = filename == NULL ? attachment->filename : filename + 1; if (*filename == '\0' || strstr(filename, "..") != NULL) filename = NULL; char path[4096]; snprintf(path, sizeof(path), "%s%c%zu-%s", directory, platform_path_separator(), 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* hex_encode(Arena* arena, const char* text) { static const char digits[] = "0123456789abcdef"; size_t length = strlen(text); if (length > (SIZE_MAX - 1) / 2) return NULL; char* encoded = arena_alloc(arena, length * 2 + 1); if (encoded == NULL) return NULL; for (size_t i = 0; i < length; i++) { unsigned char byte = (unsigned char)text[i]; encoded[i * 2] = digits[byte >> 4]; encoded[i * 2 + 1] = digits[byte & 15]; } encoded[length * 2] = '\0'; return encoded; } static const char* current_executable(Arena* arena) { char path[4096]; if (!platform_current_executable(path, sizeof(path))) { #ifdef _WIN32 return arena_string(arena, "vedit.exe"); #else return arena_string(arena, "vedit"); #endif } return arena_string(arena, path); } static bool append_shell_argument(StringBuilder* builder, const char* argument) { #ifdef _WIN32 /* cmd.exe leaves metacharacters inert inside quotes; %% prevents variable expansion. */ bool quote = *argument == '\0' || strpbrk(argument, " \t\"&|<>^()") != NULL; if (!quote && strchr(argument, '%') == NULL) return builder_append(builder, argument); if (!builder_append(builder, "\"")) return false; size_t slashes = 0; for (const char* p = argument;; p++) { if (*p == '\\') { slashes++; continue; } if (*p == '\"' || *p == '\0') { size_t count = slashes * 2 + (*p == '\"'); for (size_t i = 0; i < count; i++) if (!builder_append(builder, "\\")) return false; slashes = 0; if (*p == '\0') break; } else { for (size_t i = 0; i < slashes; i++) if (!builder_append(builder, "\\")) return false; slashes = 0; } if (*p == '%' && !builder_append(builder, "%%")) return false; if (*p != '%') { char character[2] = {*p, '\0'}; if (!builder_append(builder, character)) return false; } } return builder_append(builder, "\""); #else 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, "'"); #endif } bool compile_program(const Program* program, const TypedProgram* typed, ProbeCache* probes, const char* output_path, CompiledCommand* out, Error* error) { (void)typed; *out = (CompiledCommand){.arena = arena_create()}; 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); const char* encoded_contents = contents == NULL ? NULL : hex_encode(&out->arena, contents); StagedCommand* stage = add_stage(out); if (chapter_path == NULL || encoded_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-hex") || !add_stage_argument(out, stage, &capacity, chapter_path) || !add_stage_argument(out, stage, &capacity, encoded_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 (!compiler.encoding_set) { 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 < compiler.encoding_arguments.count; i++) ARG(compiler.encoding_arguments.items[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); } for (size_t i = 0; i < compiler.ffmpeg_arguments.count; i++) ARG(compiler.ffmpeg_arguments.items[i]); 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; } void compiled_command_destroy(CompiledCommand* command) { for (size_t i = 0; i < command->temporary_count; i++) platform_delete_file(command->temporary_paths[i]); for (size_t i = 0; i < command->temporary_directory_count; i++) platform_remove_directory(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 (portable_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 (portable_getline(&line, &capacity, source) >= 0) { if (line[0] == '\n' || line[0] == '\r') continue; if (portable_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 (portable_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 = true; const char* paths[] = {left, right}; for (size_t i = 0; wrote && i < 2; i++) { wrote = fputs("file '", file) >= 0; for (const char* p = paths[i]; wrote && *p != '\0'; p++) { if (*p == '\\') wrote = fputc('/', file) != EOF; else if (*p == '\'') wrote = fputs("'\\''", file) >= 0; else wrote = fputc((unsigned char)*p, file) != EOF; } if (wrote) wrote = fputs("'\n", file) >= 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; } static int hex_value(char character) { if (character >= '0' && character <= '9') return character - '0'; if (character >= 'a' && character <= 'f') return character - 'a' + 10; if (character >= 'A' && character <= 'F') return character - 'A' + 10; return -1; } bool write_generated_hex_file(const char* path, const char* contents, Error* error) { size_t length = strlen(contents); FILE* file = length % 2 == 0 ? fopen(path, "wb") : NULL; bool ok = file != NULL; for (size_t i = 0; ok && i < length; i += 2) { int high = hex_value(contents[i]); int low = hex_value(contents[i + 1]); if (high < 0 || low < 0 || fputc((high << 4) | low, file) == EOF) ok = false; } if (file != NULL && fclose(file) != 0) ok = false; 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 (platform_ensure_directory(path)) 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; int stage_exit_code = 0; if (!platform_run(command->stages[i].argv, &stage_exit_code, error, "subtitle processing stage")) return false; if (stage_exit_code != 0) { *exit_code = stage_exit_code; return true; } if (command->stages[i].clip_subtitles && !subtitle_clip_file(command->stages[i].subtitle_output, command->stages[i].clip_duration, error)) return false; } return platform_run(command->argv, exit_code, error, "ffmpeg"); }