aboutsummaryrefslogtreecommitdiff
path: root/video_player
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2022-02-14 06:59:23 +0100
committerdec05eba <dec05eba@protonmail.com>2022-02-16 02:04:34 +0100
commit4efce988240473a84a19dc2d378289b875d99a9e (patch)
treee17a2a3739c7aaddc69f423c253cb1a322125b04 /video_player
parent66312068bde937b0a5455800d1806e3f3077689c (diff)
Finish implementing time-pos, sub-add and event ipc
Diffstat (limited to 'video_player')
-rw-r--r--video_player/README.md58
m---------video_player/jsoncpp0
-rw-r--r--video_player/src/main.cpp346
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;
}