#include #include #include #include #include #include #include #include #include #include #include #include #define COMMAND_BUFFER_MAX_SIZE 2048 static void usage() { fprintf(stderr, "usage: quickmedia-video-player [--wid ] [--ipc-fd ] [--no-config] \n"); fprintf(stderr, " --wid The window to embed the video player into. Optional\n"); fprintf(stderr, " --ipc-fd A bi-directional (socketpair) file descriptor to receive commands from. Optional\n"); fprintf(stderr, " --no-config Do not load the users mpv config (~/.config/mpv/mpv.conf). Optional, the users mpv config is loaded by default\n"); fprintf(stderr, "examples:\n"); fprintf(stderr, " quickmedia-video-player video.mp4\n"); fprintf(stderr, " quickmedia-video-player --wid 30481231 -- video.mp4\n"); exit(1); } static ssize_t read_eintr(int fd, void *buffer, size_t size) { while(true) { ssize_t bytes_read = read(fd, buffer, size); if(bytes_read == -1) { if(errno != EINTR) return -1; } else { return bytes_read; } } } static ssize_t write_all(int fd, const void *buffer, size_t size) { ssize_t bytes_written = 0; while((size_t)bytes_written < size) { ssize_t written = write(fd, (char*)buffer + bytes_written, size - bytes_written); if(written == -1) { if(errno != EINTR) return -1; } else { bytes_written += written; } } return bytes_written; } static bool string_to_long(const char *str, long &result) { errno = 0; char *endptr = NULL; result = strtol(str, &endptr, 0); return endptr != str && errno == 0; } static bool fd_is_valid(int fd) { errno = 0; return fcntl(fd, F_GETFD) != -1 && errno != EBADF; } 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 void handle_json_command(mpv_handle *mpv_ctx, const Json::Value &json_root, int fd) { if(!json_root.isObject()) { fprintf(stderr, "Error: expected command json root to be an object\n"); return; } const Json::Value &command_json = json_root["command"]; if(!command_json.isString()) { fprintf(stderr, "Error: command json is missing field \"command\" or it's not a string\n"); 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 { Json::Value response_json(Json::objectValue); response_json["status"] = "error"; response_json["message"] = "invalid command " + command_json.asString() + ", expected time-pos or sub-add"; } 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 { fprintf(stderr, "Error: failed to parse command as json, error: %s\n", json_errors.c_str()); } command_offset = ((const char*)space_p + 1) - command_buffer; } command_buffer_size = 0; } static void send_event(const char *event_name, int fd) { Json::Value json_root(Json::objectValue); json_root["name"] = event_name; 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) { if (status < 0) { fprintf(stderr, "Error: mpv error: %s\n", mpv_error_string(status)); exit(2); } } struct Args { long wid_num = 0; long ipc_fd_num = 0; const char *wid = nullptr; const char *ipc_fd = nullptr; const char *file_to_play = nullptr; bool no_config = false; }; static Args parse_args(int argc, char **argv) { Args args; for(int i = 1; i < argc; ++i) { const char *arg = argv[i]; if(strcmp(arg, "--wid") == 0) { if(args.wid) { fprintf(stderr, "Error: option --wid was specified multiple times\n"); usage(); } if(i + 1 == argc) { fprintf(stderr, "Error: missing window id after option --wid\n"); usage(); } args.wid = argv[i + 1]; ++i; } else if(strcmp(arg, "--ipc-fd") == 0) { if(args.ipc_fd) { fprintf(stderr, "Error: option --ipc-fd was specified multiple times\n"); usage(); } if(i + 1 == argc) { fprintf(stderr, "Error: missing fd after option --ipc-fd\n"); usage(); } args.ipc_fd = argv[i + 1]; ++i; } else if(strcmp(arg, "--no-config") == 0) { args.no_config = true; } else if(strcmp(arg, "--") == 0) { if(i + 1 == argc) { fprintf(stderr, "Error: missing file option after --\n"); usage(); } else if(i + 1 != argc - 1) { fprintf(stderr, "Error: more than one option was specified after --\n"); usage(); } args.file_to_play = argv[i + 1]; ++i; } else if(strncmp(arg, "--", 2) == 0) { fprintf(stderr, "Error: invalid option %s\n", arg); usage(); } else { if(args.file_to_play) { fprintf(stderr, "Error: file option was specified multiple times\n"); usage(); } args.file_to_play = arg; } } if(!args.file_to_play) { fprintf(stderr, "Error: missing file option\n"); usage(); } if(args.wid) { if(!string_to_long(args.wid, args.wid_num)) { fprintf(stderr, "Error: invalid number %s was specified for option --wid\n", args.wid); usage(); } } if(args.ipc_fd) { if(!string_to_long(args.ipc_fd, args.ipc_fd_num)) { fprintf(stderr, "Error: invalid number %s was specified for option --ipc-fd\n", args.ipc_fd); usage(); } if(!fd_is_valid(args.ipc_fd_num)) { fprintf(stderr, "Error: invalid fd %s was specified for option --ipc-fd\n", args.ipc_fd); usage(); } } return args; } 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; } check_error(mpv_set_option_string(mpv_ctx, "input-default-bindings", "yes")); check_error(mpv_set_option_string(mpv_ctx, "input-vo-keyboard", "yes")); check_error(mpv_set_option_string(mpv_ctx, "osc", "yes")); check_error(mpv_set_option_string(mpv_ctx, "profile", "gpu-hq")); check_error(mpv_set_option_string(mpv_ctx, "vo", "gpu")); check_error(mpv_set_option_string(mpv_ctx, "hwdec", "auto")); if(!args.no_config) check_error(mpv_set_option_string(mpv_ctx, "config", "yes")); if(args.wid) check_error(mpv_set_option_string(mpv_ctx, "wid", args.wid)); check_error(mpv_initialize(mpv_ctx)); const char *cmd[] = { "loadfile", args.file_to_play, NULL }; check_error(mpv_command(mpv_ctx, cmd)); 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; while (running) { mpv_event *event = mpv_wait_event(mpv_ctx, -1.0); if (event->event_id == MPV_EVENT_SHUTDOWN) { running = false; break; } if(event->event_id != MPV_EVENT_NONE) send_event(mpv_event_name(event->event_id), args.ipc_fd_num); // TODO: Check if we can get here without mpv_wakeup being called from ipc_handler 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; }