aboutsummaryrefslogtreecommitdiff
path: root/video_player
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2022-02-15 21:52:40 +0100
committerdec05eba <dec05eba@protonmail.com>2022-02-16 02:07:21 +0100
commit2030684b16004a4f6c60f499584366ae5ad57bc9 (patch)
treed22d30f8f41b05f75999369c05a6654fe87e5581 /video_player
parent4efce988240473a84a19dc2d378289b875d99a9e (diff)
Finish video player
Diffstat (limited to 'video_player')
-rw-r--r--video_player/README.md20
-rw-r--r--video_player/include/Args.hpp136
-rw-r--r--video_player/include/Utils.hpp30
-rw-r--r--video_player/project.conf3
-rw-r--r--video_player/src/main.cpp281
5 files changed, 294 insertions, 176 deletions
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 <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <fcntl.h>
+
+#include <string>
+#include <vector>
+
+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<MpvProperty> mpv_properties;
+};
+
+static void usage() {
+ fprintf(stderr, "usage: quickmedia-video-player [--ipc-fd <fd>] [--audio-file <audio_file>] [--key=value...] <file>\n");
+ fprintf(stderr, " --ipc-fd <fd> A bi-directional (socketpair) file descriptor to receive commands from. Optional\n");
+ fprintf(stderr, " --audio-file <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 <unistd.h>
+#include <errno.h>
+
+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 <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
-#include <errno.h>
#include <locale.h>
-#include <fcntl.h>
#include <unistd.h>
#include <thread>
#include <mutex>
+#include <optional>
+#include <set>
#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>] [--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");
- 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<int64_t> 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<int64_t> 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<std::string> 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<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");
+ 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<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(mpv_ctx);