diff options
author | dec05eba <dec05eba@protonmail.com> | 2022-02-14 06:59:23 +0100 |
---|---|---|
committer | dec05eba <dec05eba@protonmail.com> | 2022-02-16 02:04:34 +0100 |
commit | 4efce988240473a84a19dc2d378289b875d99a9e (patch) | |
tree | e17a2a3739c7aaddc69f423c253cb1a322125b04 /video_player | |
parent | 66312068bde937b0a5455800d1806e3f3077689c (diff) |
Finish implementing time-pos, sub-add and event ipc
Diffstat (limited to 'video_player')
-rw-r--r-- | video_player/README.md | 58 | ||||
m--------- | video_player/jsoncpp | 0 | ||||
-rw-r--r-- | video_player/src/main.cpp | 346 |
3 files changed, 371 insertions, 33 deletions
diff --git a/video_player/README.md b/video_player/README.md new file mode 100644 index 0000000..68697d9 --- /dev/null +++ b/video_player/README.md @@ -0,0 +1,58 @@ +# QuickMedia Video Player +The video player internally used by QuickMedia. Uses libmpv.\ +The video player window is embedded inside QuickMedia and QuickMedia and this video player communicate over a file descriptor (socketpair) using json (json without newline formatting; one command per line). +# IPC commands +## time-pos +Return seeking position in file in seconds +### request +``` +{ + "command": "time-pos" +} +``` +### response on success +``` +{ + "status": "success", + "data": 112.432 +} +``` +### response on error +``` +{ + "status": "error", + "message": "error message" +} +``` +## sub-add +Add a subtitle file/url that is loaded asynchronously +### request +``` +{ + "command": "sub-add", + "data": { + "file": "path/to/file/or/url", + "title": "title", // Optional + "language": "en_us" // Optional + } +} +``` +### response on success +``` +{ + "status": "success" +} +``` +### response on error +``` +{ + "status": "error", + "message": "error message" +} +``` +# IPC event +``` +{ + "name": "file-loaded" +} +```
\ No newline at end of file diff --git a/video_player/jsoncpp b/video_player/jsoncpp new file mode 160000 +Subproject f23fb32fd9d9c3d01fa67afa0d75f7ff227647e diff --git a/video_player/src/main.cpp b/video_player/src/main.cpp index 3a06f92..42ff936 100644 --- a/video_player/src/main.cpp +++ b/video_player/src/main.cpp @@ -3,18 +3,55 @@ #include <stdlib.h> #include <string.h> #include <errno.h> +#include <locale.h> +#include <fcntl.h> +#include <unistd.h> + +#include <thread> +#include <mutex> #include <mpv/client.h> +#include <json/json.h> + +#define COMMAND_BUFFER_MAX_SIZE 2048 static void usage() { - fprintf(stderr, "usage: quickmedia-video-player [--wid <window_id>] <file>\n"); + fprintf(stderr, "usage: quickmedia-video-player [--wid <window_id>] [--ipc-fd <fd>] [--no-config] <file>\n"); fprintf(stderr, " --wid <window_id> The window to embed the video player into. Optional\n"); + fprintf(stderr, " --ipc-fd <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"); + 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; @@ -22,22 +59,193 @@ static bool string_to_long(const char *str, long &result) { 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<const char*> 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::CharReader> 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, "mpv API error: %s\n", mpv_error_string(status)); + fprintf(stderr, "Error: mpv error: %s\n", mpv_error_string(status)); exit(2); } } -int main(int argc, char **argv) { +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(wid) { + if(args.wid) { fprintf(stderr, "Error: option --wid was specified multiple times\n"); usage(); } @@ -47,8 +255,23 @@ int main(int argc, char **argv) { usage(); } - wid = argv[i + 1]; + 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"); @@ -58,63 +281,120 @@ int main(int argc, char **argv) { usage(); } - file_to_play = argv[i + 1]; + 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(file_to_play) { + if(args.file_to_play) { fprintf(stderr, "Error: file option was specified multiple times\n"); usage(); } - file_to_play = arg; + args.file_to_play = arg; } } - if(!file_to_play) { + if(!args.file_to_play) { fprintf(stderr, "Error: missing file option\n"); usage(); } - if(wid) { - if(!string_to_long(wid, wid_num)) { - fprintf(stderr, "Error: invalid number %s was specified for option --wid\n", wid); + 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(); } } - mpv_handle *ctx = mpv_create(); - if (!ctx) { - printf("failed creating context\n"); + 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(ctx, "input-default-bindings", "yes")); - check_error(mpv_set_option_string(ctx, "input-vo-keyboard", "yes")); - check_error(mpv_set_option_string(ctx, "osc", "yes")); + 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(ctx, "profile", "gpu-hq")); - check_error(mpv_set_option_string(ctx, "vo", "gpu")); - check_error(mpv_set_option_string(ctx, "hwdec", "auto")); - check_error(mpv_set_option_string(ctx, "config", "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)); - if(wid) - check_error(mpv_set_option_string(ctx, "wid", wid)); + check_error(mpv_initialize(mpv_ctx)); - check_error(mpv_initialize(ctx)); + const char *cmd[] = { "loadfile", args.file_to_play, NULL }; + check_error(mpv_command(mpv_ctx, cmd)); - const char *cmd[] = { "loadfile", file_to_play, NULL }; - check_error(mpv_command(ctx, cmd)); + char command_buffer[COMMAND_BUFFER_MAX_SIZE]; + size_t command_buffer_size = 0; + std::mutex command_mutex; + bool ipc_disconnected = false; - while (true) { - mpv_event *event = mpv_wait_event(ctx, 10000); - printf("event: %s\n", mpv_event_name(event->event_id)); - if (event->event_id == MPV_EVENT_SHUTDOWN) + 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<std::mutex> 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<std::mutex> 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(ctx); + mpv_terminate_destroy(mpv_ctx); return 0; } |