#include "../include/Args.hpp" #include "../include/Utils.hpp" #include #include #include #include #include #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_duration(mpv_handle *mpv_ctx) { double duration = 0.0; const int res = mpv_get_property(mpv_ctx, "duration", MPV_FORMAT_DOUBLE, &duration); 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"] = duration; } 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(), "duration") == 0) { response_json = handle_json_command_duration(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, duration, 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"); } } #define READ_END 0 #define WRITE_END 1 struct ReadProgram { pid_t pid = -1; int read_fd = -1; int64_t offset = 0; int64_t content_length = -1; char *url = nullptr; }; static int exec_program_pipe(const char **args, ReadProgram *read_program) { read_program->pid = -1; read_program->read_fd = -1; /* 1 arguments */ if(args[0] == NULL) return -1; int fd[2]; if(pipe(fd) == -1) { perror("Failed to open pipe"); return -2; } pid_t pid = vfork(); if(pid == -1) { perror("Failed to vfork"); close(fd[READ_END]); close(fd[WRITE_END]); return -3; } else if(pid == 0) { /* child */ dup2(fd[WRITE_END], STDOUT_FILENO); close(fd[READ_END]); close(fd[WRITE_END]); execvp(args[0], (char* const*)args); perror("execvp"); _exit(127); } else { /* parent */ close(fd[WRITE_END]); read_program->pid = pid; read_program->read_fd = fd[READ_END]; return 0; } } static char to_upper(char c) { if(c >= 'a' && c <= 'z') return c - 32; else return c; } size_t str_find_case_insensitive(const std::string &str, size_t start_index, const char *substr, size_t substr_len) { if(substr_len == 0) return 0; auto it = std::search(str.begin() + start_index, str.end(), substr, substr + substr_len, [](char c1, char c2) { return to_upper(c1) == to_upper(c2); }); if(it == str.end()) return std::string::npos; return it - str.begin(); } static int64_t size_fn(void *cookie) { ReadProgram *program = (ReadProgram*)cookie; if(program->content_length != -1) return program->content_length; const char *args[] = { "curl", "-I", "-H", "Accept-Language: en-US,en;q=0.5", "-H", "Connection: keep-alive", "--no-buffer", "-H", "user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36", "-g", "-s", "-L", "-f", "--", program->url, nullptr }; ReadProgram header_program; int res = exec_program_pipe(args, &header_program); if(res != 0) return MPV_ERROR_UNSUPPORTED; std::string buffer; buffer.resize(8192); size_t read_offset = 0; for(;;) { ssize_t bytes_read = read_eintr(header_program.read_fd, &buffer[read_offset], buffer.size() - read_offset); if(bytes_read == 0) { buffer.resize(read_offset); break; } else if(bytes_read == -1) { res = MPV_ERROR_UNSUPPORTED; break; } read_offset += bytes_read; if(read_offset >= buffer.size()) { fprintf(stderr, "Error: youtube header too large\n"); res = MPV_ERROR_UNSUPPORTED; break; } } if(res != 0) kill(header_program.pid, SIGTERM); int status = 0; if(waitpid(header_program.pid, &status, 0) == -1) { perror("waitpid failed"); res = MPV_ERROR_UNSUPPORTED; goto cleanup; } if(!WIFEXITED(status)) { res = MPV_ERROR_UNSUPPORTED; goto cleanup; } if(WEXITSTATUS(status) != 0) res = MPV_ERROR_UNSUPPORTED; cleanup: close(header_program.read_fd); if(res == 0) { size_t header_start = str_find_case_insensitive(buffer, 0, "200 OK", 6); if(header_start == std::string::npos) goto end; header_start += 6; size_t content_length_index = str_find_case_insensitive(buffer, header_start, "content-length:", 15); if(content_length_index == std::string::npos) goto end; content_length_index += 15; size_t content_length_end = buffer.find('\r', content_length_index); if(content_length_end == std::string::npos) goto end; buffer[content_length_end] = '\0'; errno = 0; char *endptr; int64_t content_length = strtoll(&buffer[content_length_index], &endptr, 10); if(endptr == &buffer[content_length_index] || errno != 0) goto end; res = content_length; } end: program->content_length = res; if(res < 0) { fprintf(stderr, "Error: video size failed\n"); exit(2); } return res; } static int64_t seek_fn(void *cookie, int64_t offset) { ReadProgram *program = (ReadProgram*)cookie; if(program->pid != -1) { kill(program->pid, SIGTERM); int status; waitpid(program->pid, &status, 0); program->pid = -1; } if(program->read_fd != -1) { close(program->read_fd); program->read_fd = -1; } program->offset = offset; char range[64]; snprintf(range, sizeof(range), "%lld-%lld", (long long)program->offset, (long long)program->offset + 5242870LL); const char *args[] = { "curl", "-H", "Accept-Language: en-US,en;q=0.5", "-H", "Connection: keep-alive", "--no-buffer", "-H", "user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36", "-g", "-s", "-L", "-f", "-r", range, "--", program->url, nullptr }; int res = exec_program_pipe(args, program); if(res != 0) { fprintf(stderr, "Error: video seek failed\n"); exit(2); } return offset; } static int64_t read_fn(void *cookie, char *buf, uint64_t nbytes) { ReadProgram *program = (ReadProgram*)cookie; ssize_t bytes_read = read_eintr(program->read_fd, buf, nbytes); if(bytes_read > 0) { program->offset += bytes_read; } else if(bytes_read == 0) { // End of current range, progress to next range bytes_read = seek_fn(program, program->offset); if(bytes_read >= 0) { bytes_read = read_eintr(program->read_fd, buf, nbytes); if(bytes_read > 0) program->offset += bytes_read; } } if(bytes_read < 0) { fprintf(stderr, "Error: video read failed\n"); exit(2); } return bytes_read; } static void close_fn(void *cookie) { ReadProgram *program = (ReadProgram*)cookie; if(program->pid != -1) { kill(program->pid, SIGTERM); int status; waitpid(program->pid, &status, 0); program->pid = -1; } if(program->read_fd != -1) { close(program->read_fd); program->read_fd = -1; } if(program->url) { free(program->url); program->url = nullptr; } delete program; } static int open_fn(void*, char *uri, mpv_stream_cb_info *info) { ReadProgram *read_program = new ReadProgram(); read_program->read_fd = -1; read_program->pid = -1; read_program->url = strdup((const char*)uri + 8); read_program->offset = 0; int64_t res = seek_fn(read_program, read_program->offset); if(res < 0) { fprintf(stderr, "Error: video open failed\n"); exit(2); } info->cookie = read_program; info->size_fn = size_fn; info->read_fn = read_fn; info->seek_fn = seek_fn; info->close_fn = close_fn; return res >= 0 ? 0 : MPV_ERROR_LOADING_FAILED; } 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"); check_error(mpv_stream_cb_add_ro(mpv_ctx, "qm-yt", nullptr, open_fn), "mpv_stream_cb_add_ro"); 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"); const char *cmd[] = { "loadfile", args.file_to_play, NULL }; check_error(mpv_command(mpv_ctx, cmd), "loadfile"); 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; int exit_code = 0; 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_END_FILE) { mpv_event_end_file *end_file_event = (mpv_event_end_file*)event->data; if(end_file_event->error == MPV_END_FILE_REASON_ERROR) { exit_code = 2; 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 exit_code; }