#include "../include/VideoPlayer.hpp" #include "../include/Storage.hpp" #include "../include/Program.hpp" #include #include #include #include #include #include #include #include #include #include const int RETRY_TIME_MS = 500; const int MAX_RETRIES_CONNECT = 1000; const int READ_TIMEOUT_MS = 200; namespace QuickMedia { VideoPlayer::VideoPlayer(bool use_tor, bool no_video, bool use_system_mpv_config, bool resume_playback, bool keep_open, EventCallbackFunc _event_callback, VideoPlayerWindowCreateCallback _window_create_callback, const std::string &resource_root, int monitor_height) : exit_status(0), use_tor(use_tor), no_video(no_video), use_system_mpv_config(use_system_mpv_config), resume_playback(resume_playback), keep_open(keep_open), video_process_id(-1), ipc_socket(-1), connected_to_ipc(false), connect_tries(0), find_window_tries(0), monitor_height(monitor_height), event_callback(_event_callback), window_create_callback(_window_create_callback), window_handle(0), parent_window(0), display(nullptr), request_id(1), expected_request_id(0), request_response_data(Json::nullValue), response_data_status(ResponseDataStatus::NONE), resource_root(resource_root) { display = XOpenDisplay(NULL); if (!display) throw std::runtime_error("Failed to open display to X11 server"); fprintf(stderr, "Video max height: %d\n", monitor_height); } VideoPlayer::~VideoPlayer() { if(video_process_id != -1) { kill(video_process_id, SIGTERM); wait_program(video_process_id); } if(ipc_socket != -1) close(ipc_socket); if(video_process_id != -1) remove(ipc_server_path); if(display) XCloseDisplay(display); } VideoPlayer::Error VideoPlayer::launch_video_process(const char *path, sf::WindowHandle _parent_window, const std::string&) { parent_window = _parent_window; if(!tmpnam(ipc_server_path)) { perror("Failed to generate ipc file name"); return Error::FAIL_TO_GENERATE_IPC_FILENAME; } const std::string parent_window_str = std::to_string(parent_window); std::vector args; if(use_tor) args.push_back("torsocks"); std::string input_ipc_server_arg = "--input-ipc-server="; input_ipc_server_arg += ipc_server_path; std::string wid_arg = "--wid="; wid_arg += parent_window_str; std::string input_conf = "--input-conf=" + resource_root + "input.conf"; Path video_cache_dir = get_cache_dir().join("video"); create_directory_recursive(video_cache_dir); std::string cache_dir = "--cache-dir=" + video_cache_dir.data; Path mpv_watch_later_dir = get_storage_dir().join("mpv").join("watch_later"); create_directory_recursive(mpv_watch_later_dir); std::string watch_later_dir = "--watch-later-directory=" + mpv_watch_later_dir.data; std::string ytdl_format; if(no_video) ytdl_format = "--ytdl-format=bestaudio/best"; else ytdl_format = "--ytdl-format=bestvideo[height<=?" + std::to_string(monitor_height) + "]+bestaudio/best"; // TODO: Resume playback if the last video played matches the first video played next time QuickMedia is launched args.insert(args.end(), { "mpv", input_ipc_server_arg.c_str(), "--cursor-autohide=no", /* "--no-input-default-bindings", "--input-vo-keyboard=no", "--no-input-cursor", */ "--no-terminal", "--save-position-on-quit=yes", "--profile=pseudo-gui", // For gui when playing audio, requires a version of mpv that isn't ancient cache_dir.c_str(), watch_later_dir.c_str(), "--cache-on-disk=yes", "--ytdl-raw-options=sub-lang=\"en,eng,enUS,en-US\",write-sub=", ytdl_format.c_str(), // TODO: Disable hr seek on low power devices? "--hr-seek=yes", input_conf.c_str(), wid_arg.c_str() }); if(keep_open) args.push_back("--keep-open=yes"); if(!resume_playback) args.push_back("--no-resume-playback"); if(!use_system_mpv_config) { args.insert(args.end(), { "--no-config", /*"--demuxer-max-bytes=40M", "--demuxer-max-back-bytes=20M",*/ "--profile=gpu-hq", "--vo=gpu,vdpau,x11", "--hwdec=auto" }); } /* std::string ytdl_options_arg; if(!use_tor && !plugin_name.empty()) { Path cookies_filepath; if(get_cookies_filepath(cookies_filepath, plugin_name) != 0) { fprintf(stderr, "Warning: Failed to create %s cookies file\n", plugin_name.c_str()); } else { ytdl_options_arg = "--ytdl-raw-options=cookies=" + cookies_filepath.data; args.push_back(ytdl_options_arg.c_str()); } } */ if(no_video) args.push_back("--no-video"); args.insert(args.end(), { "--", path, nullptr }); if(exec_program_async(args.data(), &video_process_id) != 0) return Error::FAIL_TO_LAUNCH_PROCESS; fprintf(stderr, "mpv input ipc server: %s\n", ipc_server_path); if((ipc_socket = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { perror("Failed to create socket for video player"); return Error::FAIL_TO_CREATE_SOCKET; } ipc_addr.sun_family = AF_UNIX; strcpy(ipc_addr.sun_path, ipc_server_path); int flags = fcntl(ipc_socket, F_GETFL, 0); fcntl(ipc_socket, F_SETFL, flags | O_NONBLOCK); return Error::OK; } VideoPlayer::Error VideoPlayer::load_video(const char *path, sf::WindowHandle _parent_window, const std::string &plugin_name) { // This check is to make sure we dont change window that the video belongs to. This is not a usecase we will have so // no need to support it for now at least. assert(parent_window == 0 || parent_window == _parent_window); if(video_process_id == -1) return launch_video_process(path, _parent_window, plugin_name); Json::Value command_data(Json::arrayValue); command_data.append("loadfile"); command_data.append(path); Json::Value command(Json::objectValue); command["command"] = command_data; Json::StreamWriterBuilder builder; builder["commentStyle"] = "None"; builder["indentation"] = ""; const std::string cmd_str = Json::writeString(builder, command) + "\n"; return send_command(cmd_str.c_str(), cmd_str.size()); } static std::vector get_child_window(Display *display, Window window) { std::vector result; Window root_window; Window parent_window; Window *child_window; unsigned int num_children; if(XQueryTree(display, window, &root_window, &parent_window, &child_window, &num_children) != 0) { for(unsigned int i = 0; i < num_children; i++) result.push_back(child_window[i]); } return result; } VideoPlayer::Error VideoPlayer::update() { const int max_retries_find_window = 1000; if(ipc_socket == -1) return Error::INIT_FAILED; if(connect_tries == MAX_RETRIES_CONNECT) return Error::FAIL_TO_CONNECT_TIMEOUT; if(find_window_tries == max_retries_find_window) return Error::FAIL_TO_FIND_WINDOW; if(!connected_to_ipc && retry_timer.getElapsedTime().asMilliseconds() >= RETRY_TIME_MS) { retry_timer.restart(); if(connect(ipc_socket, (struct sockaddr*)&ipc_addr, sizeof(ipc_addr)) == -1) { ++connect_tries; if(connect_tries == MAX_RETRIES_CONNECT) { fprintf(stderr, "Failed to connect to mpv ipc after %d seconds, last error: %s\n", (RETRY_TIME_MS * MAX_RETRIES_CONNECT)/1000, strerror(errno)); return Error::FAIL_TO_CONNECT_TIMEOUT; } } else { connected_to_ipc = true; } } if(connected_to_ipc && window_handle == 0 && retry_timer.getElapsedTime().asMilliseconds() >= RETRY_TIME_MS) { retry_timer.restart(); std::vector child_windows = get_child_window(display, parent_window); size_t num_children = child_windows.size(); if(num_children == 0) { ++find_window_tries; if(find_window_tries == max_retries_find_window) { fprintf(stderr, "Failed to find mpv window after %d seconds\n", (RETRY_TIME_MS * max_retries_find_window)/1000); return Error::FAIL_TO_FIND_WINDOW_TIMEOUT; } } else if(num_children == 1) { window_handle = child_windows[0]; if(window_create_callback) window_create_callback(window_handle); } else { fprintf(stderr, "Expected window to have one child (the video player) but it has %zu\n", num_children); return Error::UNEXPECTED_WINDOW_ERROR; } } if(video_process_id != -1) { if(wait_program_non_blocking(video_process_id, &exit_status)) { fprintf(stderr, "The video player exited!, status: %d\n", exit_status); close(ipc_socket); remove(ipc_server_path); video_process_id = -1; ipc_socket = -1; return Error::EXITED; } } if(connected_to_ipc && event_callback) { Error err = read_ipc_func(); if(err != Error::OK) return err; } return Error::OK; } VideoPlayer::Error VideoPlayer::read_ipc_func() { assert(connected_to_ipc); Json::Value json_root; Json::CharReaderBuilder json_builder; std::unique_ptr json_reader(json_builder.newCharReader()); std::string json_errors; char buffer[2048]; ssize_t bytes_read = read(ipc_socket, buffer, sizeof(buffer)); if(bytes_read == -1) { int err = errno; if(err != EAGAIN) { fprintf(stderr, "Failed to read from ipc socket, error: %s\n", strerror(err)); return Error::FAIL_TO_READ; } } else if(bytes_read > 0) { int start = 0; for(int i = 0; i < bytes_read; ++i) { if(buffer[i] != '\n') continue; if(json_reader->parse(buffer + start, buffer + i, &json_root, &json_errors) && json_root.isObject()) { const Json::Value &event = json_root["event"]; const Json::Value &request_id_json = json_root["request_id"]; if(event.isString()) { if(event_callback) event_callback(event.asCString()); } if(expected_request_id != 0 && request_id_json.isNumeric() && request_id_json.asUInt() == expected_request_id) { if(json_root["error"].isNull()) response_data_status = ResponseDataStatus::ERROR; else response_data_status = ResponseDataStatus::OK; request_response_data = json_root["data"]; } } else { fprintf(stderr, "Failed to parse json for ipc: |%.*s|, reason: %s\n", (int)bytes_read, buffer, json_errors.c_str()); } start = i + 1; } } return Error::OK; } VideoPlayer::Error VideoPlayer::toggle_pause() { const char cmd[] = "cycle pause\n"; return send_command(cmd, sizeof(cmd) - 1); } VideoPlayer::Error VideoPlayer::get_progress(double *result) { Json::Value percent_pos_json; Error err = get_property("percent-pos", &percent_pos_json, Json::realValue); if(err != Error::OK) return err; *result = percent_pos_json.asDouble() * 0.01; return err; } VideoPlayer::Error VideoPlayer::get_time_in_file(double *result) { Json::Value time_pos_json; Error err = get_property("time-pos", &time_pos_json, Json::realValue); if(err != Error::OK) return err; *result = time_pos_json.asDouble(); return err; } VideoPlayer::Error VideoPlayer::get_time_remaining(double *result) { Json::Value time_remaining_json; Error err = get_property("time-remaining", &time_remaining_json, Json::realValue); if(err != Error::OK) return err; *result = time_remaining_json.asDouble(); return err; } VideoPlayer::Error VideoPlayer::set_property(const std::string &property_name, const Json::Value &value) { Json::Value command_data(Json::arrayValue); command_data.append("set_property"); command_data.append(property_name); command_data.append(value); Json::Value command(Json::objectValue); command["command"] = command_data; Json::StreamWriterBuilder builder; builder["commentStyle"] = "None"; builder["indentation"] = ""; const std::string cmd_str = Json::writeString(builder, command) + "\n"; return send_command(cmd_str.c_str(), cmd_str.size()); } VideoPlayer::Error VideoPlayer::get_property(const std::string &property_name, Json::Value *result, Json::ValueType result_type) { unsigned int cmd_request_id = request_id; ++request_id; // Overflow check. 0 is defined as no request, 1 is the first valid one if(request_id == 0) request_id = 1; Json::Value command_data(Json::arrayValue); command_data.append("get_property"); command_data.append(property_name); Json::Value command(Json::objectValue); command["command"] = command_data; command["request_id"] = cmd_request_id; Json::StreamWriterBuilder builder; builder["commentStyle"] = "None"; builder["indentation"] = ""; const std::string cmd_str = Json::writeString(builder, command) + "\n"; Error err = send_command(cmd_str.c_str(), cmd_str.size()); if(err != Error::OK) return err; sf::Clock read_timer; expected_request_id = cmd_request_id; do { err = read_ipc_func(); if(err != Error::OK) goto cleanup; if(response_data_status != ResponseDataStatus::NONE) break; } while(read_timer.getElapsedTime().asMilliseconds() < READ_TIMEOUT_MS); if(response_data_status == ResponseDataStatus::OK) { if(request_response_data.type() == result_type) *result = request_response_data; else err = Error::READ_INCORRECT_TYPE; } else if(response_data_status == ResponseDataStatus::ERROR) { err = Error::READ_RESPONSE_ERROR; goto cleanup; } else { err = Error::READ_TIMEOUT; goto cleanup; } cleanup: expected_request_id = 0; response_data_status = ResponseDataStatus::NONE; request_response_data = Json::Value(Json::nullValue); return err; } VideoPlayer::Error VideoPlayer::set_paused(bool paused) { return set_property("pause", paused); } VideoPlayer::Error VideoPlayer::set_progress(double progress) { std::string cmd = "{ \"command\": [\"set_property\", \"percent-pos\", "; cmd += std::to_string(progress * 100.0) + "] }"; return send_command(cmd.c_str(), cmd.size()); } VideoPlayer::Error VideoPlayer::is_seekable(bool *result) { Json::Value seekable_json; Error err = get_property("seekable", &seekable_json, Json::booleanValue); if(err != Error::OK) return err; *result = seekable_json.asBool(); return err; } VideoPlayer::Error VideoPlayer::quit_and_save_watch_later() { Json::Value command_data(Json::arrayValue); command_data.append("quit-watch-later"); Json::Value command(Json::objectValue); command["command"] = command_data; Json::StreamWriterBuilder builder; builder["commentStyle"] = "None"; builder["indentation"] = ""; const std::string cmd_str = Json::writeString(builder, command) + "\n"; return send_command(cmd_str.c_str(), cmd_str.size()); } VideoPlayer::Error VideoPlayer::send_command(const char *cmd, size_t size) { if(!connected_to_ipc) return Error::FAIL_NOT_CONNECTED; if(send(ipc_socket, cmd, size, 0) == -1) { fprintf(stderr, "Failed to send to ipc socket, error: %s, command: %.*s\n", strerror(errno), (int)size, cmd); return Error::FAIL_TO_SEND; } return Error::OK; } }