#include "../include/Args.hpp" #include "../include/Utils.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #define COMMAND_BUFFER_MAX_SIZE 2048 static size_t ipc_handler(mpv_handle *mpv_ctx, int fd, char *buffer, size_t buffer_size, bool &disconnected) { ssize_t bytes_read = read_eintr(fd, buffer, buffer_size); if(bytes_read < 0) { fprintf(stderr, "Error: ipc read failed, error: %s\n", strerror(errno)); //exit(3); disconnected = false; return 0; } mpv_wakeup(mpv_ctx); disconnected = (bytes_read == 0); return bytes_read; } static Json::Value handle_json_command_time_pos(mpv_handle *mpv_ctx) { double time_pos = 0.0; const int res = mpv_get_property(mpv_ctx, "time-pos", MPV_FORMAT_DOUBLE, &time_pos); Json::Value response_json(Json::objectValue); if(res < 0) { response_json["status"] = "error"; response_json["message"] = mpv_error_string(res); } else { response_json["status"] = "success"; response_json["data"] = time_pos; } return response_json; } static Json::Value handle_json_command_sub_add(mpv_handle *mpv_ctx, const Json::Value &json_root) { Json::Value response_json(Json::objectValue); const Json::Value &data_json = json_root["data"]; if(!data_json.isObject()) { response_json["status"] = "error"; response_json["message"] = "expected \"data\" to be an object"; return response_json; } const Json::Value &file_json = data_json["file"]; const Json::Value &title_json = data_json["title"]; const Json::Value &language_json = data_json["language"]; if(!file_json.isString()) { response_json["status"] = "error"; response_json["message"] = "expected \"data.file\" to be a string"; return response_json; } if(!title_json.isString() && !title_json.isNull()) { response_json["status"] = "error"; response_json["message"] = "expected optional field \"data.title\" to be a string or omitted"; return response_json; } if(!language_json.isString() && !language_json.isNull()) { response_json["status"] = "error"; response_json["message"] = "expected optional field \"data.language\" to be a string or omitted"; return response_json; } std::vector args; args.push_back("sub-add"); args.push_back(file_json.asCString()); args.push_back("auto"); if(title_json.isString()) args.push_back(title_json.asCString()); if(language_json.isString()) { if(!title_json.isString()) args.push_back(language_json.asCString()); args.push_back(language_json.asCString()); } args.push_back(nullptr); const int res = mpv_command_async(mpv_ctx, 0, args.data()); if(res < 0) { response_json["status"] = "error"; response_json["message"] = mpv_error_string(res); } else { response_json["status"] = "success"; } return response_json; } static Json::Value handle_json_command_cycle_fullscreen(mpv_handle *mpv_ctx) { const char *args[] = { "cycle", "fullscreen", nullptr }; const int res = mpv_command_async(mpv_ctx, 0, args); Json::Value response_json(Json::objectValue); if(res < 0) { response_json["status"] = "error"; response_json["message"] = mpv_error_string(res); } else { response_json["status"] = "success"; } return response_json; } static void send_error(const std::string &err_msg, std::optional request_id, int fd) { fprintf(stderr, "Error: %s\n", err_msg.c_str()); Json::Value json_root(Json::objectValue); json_root["status"] = "error"; json_root["message"] = err_msg; if(request_id) json_root["request_id"] = *request_id; Json::StreamWriterBuilder builder; builder["commentStyle"] = "None"; builder["indentation"] = ""; const std::string response_str = Json::writeString(builder, json_root) + "\n"; ssize_t bytes_written = write_all(fd, response_str.data(), response_str.size()); if(bytes_written < 0) { fprintf(stderr, "Error: ipc write failed, error: %s\n", strerror(errno)); //exit(3); return; } } static void handle_json_command(mpv_handle *mpv_ctx, const Json::Value &json_root, int fd) { if(!json_root.isObject()) { send_error("expected command json root to be an object", std::nullopt, fd); return; } const Json::Value &command_json = json_root["command"]; if(!command_json.isString()) { send_error("command json is missing field \"command\" or it's not a string", std::nullopt, fd); return; } std::optional request_id = std::nullopt; const Json::Value &request_id_json = json_root["request_id"]; if(!request_id_json.isNull()) { if(request_id_json.isInt64()) { request_id = request_id_json.asInt64(); } else { send_error("request_id was provided but its not an integer", std::nullopt, fd); return; } } Json::Value response_json; if(strcmp(command_json.asCString(), "time-pos") == 0) { response_json = handle_json_command_time_pos(mpv_ctx); } else if(strcmp(command_json.asCString(), "sub-add") == 0) { response_json = handle_json_command_sub_add(mpv_ctx, json_root); } else if(strcmp(command_json.asCString(), "cycle-fullscreen") == 0) { response_json = handle_json_command_cycle_fullscreen(mpv_ctx); } else { response_json = Json::Value(Json::objectValue); response_json["status"] = "error"; response_json["message"] = "invalid command " + command_json.asString() + ", expected time-pos, sub-add or cycle-fullscreen"; } if(request_id) response_json["request_id"] = *request_id; Json::StreamWriterBuilder builder; builder["commentStyle"] = "None"; builder["indentation"] = ""; const std::string response_str = Json::writeString(builder, response_json) + "\n"; ssize_t bytes_written = write_all(fd, response_str.data(), response_str.size()); if(bytes_written < 0) { fprintf(stderr, "Error: ipc write failed, error: %s\n", strerror(errno)); //exit(3); return; } } static void handle_request_commands_line_by_line(mpv_handle *mpv_ctx, int fd, char *command_buffer, size_t &command_buffer_size, Json::Value &json_root, std::string &json_errors) { size_t command_offset = 0; while(command_offset < command_buffer_size) { const void *space_p = memchr(command_buffer + command_offset, '\n', command_buffer_size - command_offset); if(!space_p) space_p = command_buffer + command_buffer_size; Json::CharReaderBuilder json_builder; std::unique_ptr json_reader(json_builder.newCharReader()); json_errors.clear(); if(json_reader->parse(command_buffer + command_offset, (const char*)space_p, &json_root, &json_errors)) { handle_json_command(mpv_ctx, json_root, fd); } else { send_error("failed to parse command as json, error: " + json_errors, std::nullopt, fd); } command_offset = ((const char*)space_p + 1) - command_buffer; } command_buffer_size = 0; } static void send_event(const char *event_name, int fd, const std::vector &args = {}) { Json::Value json_root(Json::objectValue); json_root["event"] = event_name; if(!args.empty()) { Json::Value args_json(Json::arrayValue); for(const std::string &arg : args) { args_json.append(arg); } json_root["args"] = std::move(args_json); } Json::StreamWriterBuilder builder; builder["commentStyle"] = "None"; builder["indentation"] = ""; const std::string response_str = Json::writeString(builder, json_root) + "\n"; ssize_t bytes_written = write_all(fd, response_str.data(), response_str.size()); if(bytes_written < 0) { fprintf(stderr, "Error: ipc write failed, error: %s\n", strerror(errno)); //exit(3); return; } } static inline void check_error(int status, const char *prefix) { if (status < 0) { fprintf(stderr, "Error: %s mpv error: %s\n", prefix, mpv_error_string(status)); exit(2); } } static bool is_num(char c) { return c >= '0' && c <= '9'; } static bool string_looks_like_int(const char *str, size_t size) { for(size_t i = 0; i < size; ++i) { char c = str[i]; if(!is_num(c) && c != '-') return false; } return true; } static bool string_looks_like_double(const char *str, size_t size) { for(size_t i = 0; i < size; ++i) { char c = str[i]; if(!is_num(c) && c != '-' && c != '.') return false; } return true; } static void mpv_set_before_init_options(mpv_handle *mpv_ctx, const Args &args) { long value_long = 0; double value_double = 0; std::set known_string_properties = { "start", "force-media-title" }; for(const MpvProperty &property : args.mpv_properties) { const bool is_string_property = known_string_properties.find(property.key) != known_string_properties.end(); if(!is_string_property && string_looks_like_int(property.value.c_str(), property.value.size()) && string_to_long(property.value.c_str(), value_long)) check_error(mpv_set_option(mpv_ctx, property.key.c_str(), MPV_FORMAT_INT64, &value_long), property.key.c_str()); else if(!is_string_property && string_looks_like_double(property.value.c_str(), property.value.size()) && string_to_double(property.value.c_str(), value_double)) check_error(mpv_set_option(mpv_ctx, property.key.c_str(), MPV_FORMAT_DOUBLE, &value_double), property.key.c_str()); else check_error(mpv_set_option_string(mpv_ctx, property.key.c_str(), property.value.c_str()), property.key.c_str()); } if(args.audio_file) { mpv_node first_element; first_element.format = MPV_FORMAT_STRING; first_element.u.string = (char*)args.audio_file; mpv_node_list list; list.num = 1; list.keys = NULL; list.values = &first_element; mpv_node node; node.format = MPV_FORMAT_NODE_ARRAY; node.u.list = &list; check_error(mpv_set_option(mpv_ctx, "audio-files", MPV_FORMAT_NODE, &node), "audio-files"); } } int main(int argc, char **argv) { // This is needed for mpv_create or it will fail setlocale(LC_ALL, "C"); Args args = parse_args(argc, argv); mpv_handle *mpv_ctx = mpv_create(); if (!mpv_ctx) { fprintf(stderr, "Error: failed to create mpv context\n"); return 1; } mpv_set_before_init_options(mpv_ctx, args); check_error(mpv_initialize(mpv_ctx), "mpv_initialize"); const char *cmd[] = { "loadfile", args.file_to_play, NULL }; check_error(mpv_command(mpv_ctx, cmd), "loadfile"); check_error(mpv_observe_property(mpv_ctx, 0, "idle-active", MPV_FORMAT_FLAG), "observe idle-active"); check_error(mpv_observe_property(mpv_ctx, 0, "fullscreen", MPV_FORMAT_FLAG), "observe fullscreen"); char command_buffer[COMMAND_BUFFER_MAX_SIZE]; size_t command_buffer_size = 0; std::mutex command_mutex; bool ipc_disconnected = false; bool running = true; // TODO: Clean cleanup instead of terminating... To make that possible we need to find a way to wake up the read call that is waiting for data if(args.ipc_fd) { std::thread([&]() mutable { while(running) { command_buffer_size = ipc_handler(mpv_ctx, args.ipc_fd_num, command_buffer, COMMAND_BUFFER_MAX_SIZE, ipc_disconnected); std::lock_guard lock(command_mutex); // Wait until the command has been handled in the main loop in the main thread } }).detach(); } Json::Value json_root; std::string json_errors; bool file_started = false; while (running) { mpv_event *event = mpv_wait_event(mpv_ctx, -1.0); if(args.ipc_fd && event->event_id != MPV_EVENT_NONE) send_event(mpv_event_name(event->event_id), args.ipc_fd_num); if(event->event_id == MPV_EVENT_START_FILE && !file_started) { file_started = true; } else if(event->event_id == MPV_EVENT_SHUTDOWN) { running = false; break; } else if(event->event_id == MPV_EVENT_PROPERTY_CHANGE) { // End of file (idle) mpv_event_property *property = (mpv_event_property*)event->data; if(file_started && strcmp(property->name, "idle-active") == 0 && *(int*)property->data == 1) { running = false; break; } else if(args.ipc_fd && strcmp(property->name, "fullscreen") == 0) { send_event("fullscreen", args.ipc_fd_num, { *(bool*)property->data ? "yes" : "no" }); } } if(args.ipc_fd) { std::lock_guard lock(command_mutex); // Other end of the ipc socket has disconnected if(ipc_disconnected) { fprintf(stderr, "Warning: the other end of the ipc fd was closed, closing the video player...\n"); running = false; break; } handle_request_commands_line_by_line(mpv_ctx, args.ipc_fd_num, command_buffer, command_buffer_size, json_root, json_errors); } } mpv_terminate_destroy(mpv_ctx); return 0; }