From 2030684b16004a4f6c60f499584366ae5ad57bc9 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Tue, 15 Feb 2022 21:52:40 +0100 Subject: Finish video player --- video_player/README.md | 20 ++- video_player/include/Args.hpp | 136 ++++++++++++++++++++ video_player/include/Utils.hpp | 30 +++++ video_player/project.conf | 3 + video_player/src/main.cpp | 281 ++++++++++++++++------------------------- 5 files changed, 294 insertions(+), 176 deletions(-) create mode 100644 video_player/include/Args.hpp create mode 100644 video_player/include/Utils.hpp (limited to 'video_player') diff --git a/video_player/README.md b/video_player/README.md index 68697d9..9a74c9c 100644 --- a/video_player/README.md +++ b/video_player/README.md @@ -7,21 +7,24 @@ Return seeking position in file in seconds ### request ``` { - "command": "time-pos" + "command": "time-pos", + "request_id": 232 // Optional } ``` ### response on success ``` { "status": "success", - "data": 112.432 + "data": 112.432, + "request_id": 232, // Optional. Its provided if request_id was provided in the request } ``` ### response on error ``` { "status": "error", - "message": "error message" + "message": "error message", + "request_id": 233 // Optional. Its provided if request_id was provided in the request } ``` ## sub-add @@ -34,25 +37,28 @@ Add a subtitle file/url that is loaded asynchronously "file": "path/to/file/or/url", "title": "title", // Optional "language": "en_us" // Optional - } + }, + "request_id": 233 // Optional } ``` ### response on success ``` { - "status": "success" + "status": "success", + "request_id": 233 // Optional. Its provided if request_id was provided in the request } ``` ### response on error ``` { "status": "error", - "message": "error message" + "message": "error message", + "request_id": 233 // Optional. Its provided if request_id was provided in the request } ``` # IPC event ``` { - "name": "file-loaded" + "event": "file-loaded" } ``` \ No newline at end of file diff --git a/video_player/include/Args.hpp b/video_player/include/Args.hpp new file mode 100644 index 0000000..610f0b0 --- /dev/null +++ b/video_player/include/Args.hpp @@ -0,0 +1,136 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +struct MpvProperty { + std::string key; + std::string value; +}; + +struct Args { + long wid_num = 0; + long ipc_fd_num = 0; + + const char *ipc_fd = nullptr; + const char *audio_file = nullptr; + const char *file_to_play = nullptr; + + std::vector mpv_properties; +}; + +static void usage() { + fprintf(stderr, "usage: quickmedia-video-player [--ipc-fd ] [--audio-file ] [--key=value...] \n"); + fprintf(stderr, " --ipc-fd A bi-directional (socketpair) file descriptor to receive commands from. Optional\n"); + fprintf(stderr, " --audio-file Load the given audio file. Optional\n"); + fprintf(stderr, " --key=value Additional options in the format --key=value are passed directly to mpv as string properties. Optional\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 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 string_to_double(const char *str, double &result) { + errno = 0; + char *endptr = NULL; + result = strtod(str, &endptr); + return endptr != str && errno == 0; +} + +static bool fd_is_valid(int fd) { + errno = 0; + return fcntl(fd, F_GETFD) != -1 && errno != EBADF; +} + +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, "--audio-file") == 0) { + if(args.audio_file) { + fprintf(stderr, "Error: option --audio-file was specified multiple times\n"); + usage(); + } + + if(i + 1 == argc) { + fprintf(stderr, "Error: missing audio file after option --audio-file\n"); + usage(); + } + + args.audio_file = 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, "--") == 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) { + const char *equal_p = strchr(arg, '='); + if(!equal_p) { + fprintf(stderr, "Error: mpv option %s is missing \"=\"\n", arg); + usage(); + } + + args.mpv_properties.push_back({ std::string(arg + 2, equal_p - (arg + 2)), equal_p + 1 }); + } 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.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; +} \ No newline at end of file diff --git a/video_player/include/Utils.hpp b/video_player/include/Utils.hpp new file mode 100644 index 0000000..a5219f4 --- /dev/null +++ b/video_player/include/Utils.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +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; +} \ No newline at end of file diff --git a/video_player/project.conf b/video_player/project.conf index b71b204..41d6bd3 100644 --- a/video_player/project.conf +++ b/video_player/project.conf @@ -4,5 +4,8 @@ type = "executable" version = "0.1.0" platforms = ["posix"] +[lang.cpp] +version = "c++17" + [dependencies] mpv = "2" diff --git a/video_player/src/main.cpp b/video_player/src/main.cpp index 42ff936..7489873 100644 --- a/video_player/src/main.cpp +++ b/video_player/src/main.cpp @@ -1,69 +1,22 @@ +#include "../include/Args.hpp" +#include "../include/Utils.hpp" #include #include #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) { @@ -148,29 +101,65 @@ static Json::Value handle_json_command_sub_add(mpv_handle *mpv_ctx, const Json:: 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()) { - fprintf(stderr, "Error: expected command json root to be an object\n"); + 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()) { - fprintf(stderr, "Error: command json is missing field \"command\" or it's not a string\n"); + 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 { - Json::Value response_json(Json::objectValue); + response_json = Json::Value(Json::objectValue); response_json["status"] = "error"; response_json["message"] = "invalid command " + command_json.asString() + ", expected time-pos or sub-add"; } + if(request_id) + response_json["request_id"] = *request_id; + Json::StreamWriterBuilder builder; builder["commentStyle"] = "None"; builder["indentation"] = ""; @@ -197,7 +186,7 @@ static void handle_request_commands_line_by_line(mpv_handle *mpv_ctx, int fd, ch 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()); + send_error("failed to parse command as json, error: " + json_errors, std::nullopt, fd); } command_offset = ((const char*)space_p + 1) - command_buffer; } @@ -206,7 +195,7 @@ static void handle_request_commands_line_by_line(mpv_handle *mpv_ctx, int fd, ch static void send_event(const char *event_name, int fd) { Json::Value json_root(Json::objectValue); - json_root["name"] = event_name; + json_root["event"] = event_name; Json::StreamWriterBuilder builder; builder["commentStyle"] = "None"; @@ -221,106 +210,58 @@ static void send_event(const char *event_name, int fd) { } } -static inline void check_error(int status) { +static inline void check_error(int status, const char *prefix) { if (status < 0) { - fprintf(stderr, "Error: mpv error: %s\n", mpv_error_string(status)); + fprintf(stderr, "Error: %s mpv error: %s\n", prefix, 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(); - } +static bool is_num(char c) { + return c >= '0' && c <= '9'; +} - args.file_to_play = arg; - } +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; +} - if(!args.file_to_play) { - fprintf(stderr, "Error: missing file option\n"); - usage(); +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; +} - 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(); - } - } +static void mpv_set_before_init_options(mpv_handle *mpv_ctx, const Args &args) { + long value_long = 0; + double value_double = 0; - 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(); - } + std::set known_string_properties = { "start", "force-media-title" }; - 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(); - } + 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()); } +} - return args; +static void mpv_set_after_load_options(mpv_handle *mpv_ctx, const Args &args) { + if(args.audio_file) { + const char* cmd_args[] = { "audio-add", args.audio_file, "select", "Default track", nullptr }; + check_error(mpv_command_async(mpv_ctx, 0, cmd_args), "audio-add"); + } } int main(int argc, char **argv) { @@ -335,23 +276,13 @@ int main(int argc, char **argv) { 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)); + 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)); + check_error(mpv_command(mpv_ctx, cmd), "loadfile"); + + check_error(mpv_observe_property(mpv_ctx, 0, "idle-active", MPV_FORMAT_FLAG), "observe idle-active"); char command_buffer[COMMAND_BUFFER_MAX_SIZE]; size_t command_buffer_size = 0; @@ -372,27 +303,39 @@ int main(int argc, char **argv) { 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 (event->event_id == MPV_EVENT_SHUTDOWN) { - running = false; - break; - } - if(event->event_id != MPV_EVENT_NONE) + if(args.ipc_fd && 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"); + if(event->event_id == MPV_EVENT_START_FILE && !file_started) { + file_started = true; + mpv_set_after_load_options(mpv_ctx, args); + } else if(event->event_id == MPV_EVENT_SHUTDOWN) { running = false; break; + } else if(event->event_id == MPV_EVENT_PROPERTY_CHANGE && file_started) { + // End of file (idle) + mpv_event_property *property = (mpv_event_property*)event->data; + if(strcmp(property->name, "idle-active") == 0 && *(int*)property->data == 1) { + running = false; + break; + } } - handle_request_commands_line_by_line(mpv_ctx, args.ipc_fd_num, command_buffer, command_buffer_size, json_root, json_errors); + 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); -- cgit v1.2.3