#include "../include/VideoPlayer.hpp" #include "../include/Storage.hpp" #include "../include/Program.hpp" #include "../include/Utils.hpp" #include "../include/StringUtils.hpp" #include "../include/Notification.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 { static std::string media_chapters_to_ffmetadata_chapters(const std::vector &chapters) { std::string result = ";FFMETADATA1\n\n"; for(size_t i = 0; i < chapters.size(); ++i) { const MediaChapter &chapter = chapters[i]; result += "[CHAPTER]\n"; result += "TIMEBASE=1/1\n"; result += "START=" + std::to_string(chapter.start_seconds) + "\n"; result += "END=" + std::to_string(i + 1 == chapters.size() ? chapter.start_seconds : chapters[i + 1].start_seconds) + "\n"; std::string title = chapter.title; string_replace_all(title, '\n', ' '); result += "title=" + std::move(title) + "\n\n"; } return result; } // If |chapters| is empty then |tmp_chapters_filepath| is removed, otherwise the file is overwritten static bool create_tmp_file_with_chapters_data(char *tmp_chapters_filepath, const std::vector &chapters) { if(chapters.empty()) { if(tmp_chapters_filepath[0] != '\0') { remove(tmp_chapters_filepath); tmp_chapters_filepath[0] = '\0'; } return true; } if(tmp_chapters_filepath[0] == '\0') { strcpy(tmp_chapters_filepath, "/tmp/qm-mpv-chapter-XXXXXX"); mktemp(tmp_chapters_filepath); if(tmp_chapters_filepath[0] == '\0') { fprintf(stderr, "Failed to create temporay file\n"); return false; } } if(file_overwrite(tmp_chapters_filepath, media_chapters_to_ffmetadata_chapters(chapters)) == 0) { return true; } else { remove(tmp_chapters_filepath); tmp_chapters_filepath[0] = '\0'; return false; } } VideoPlayer::VideoPlayer(bool no_video, bool use_system_mpv_config, bool keep_open, EventCallbackFunc _event_callback, VideoPlayerWindowCreateCallback _window_create_callback, const std::string &resource_root, int monitor_height, std::string plugin_name) : exit_status(0), plugin_name(std::move(plugin_name)), no_video(no_video), use_system_mpv_config(use_system_mpv_config), keep_open(keep_open), use_youtube_dl(true), video_process_id(-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) { tmp_chapters_filepath[0] = '\0'; display = XOpenDisplay(NULL); if (!display) { show_notification("QuickMedia", "Failed to open display to X11 server", Urgency::CRITICAL); abort(); } fprintf(stderr, "Video max height: %d\n", monitor_height); ipc_server_path[0] = '\0'; } 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 && ipc_server_path[0] != '\0') remove(ipc_server_path); if(display) XCloseDisplay(display); if(tmp_chapters_filepath[0] != '\0') remove(tmp_chapters_filepath); } VideoPlayer::Error VideoPlayer::launch_video_process(const char *path, const char *audio_path, sf::WindowHandle _parent_window, const std::string &title, const std::string &start_time) { 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; 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"; std::string cache_dir = "--cache-dir=" + std::move(get_cache_dir().join("media").data); // 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=no", "--profile=pseudo-gui", // For gui when playing audio, requires a version of mpv that isn't ancient "--no-resume-playback", // TODO: Disable hr seek on low power devices? "--hr-seek=yes", //"--cache=no", "--force-seekable=yes", "--image-display-duration=5", "--cache-pause=yes", "--cache=yes", "--cache-on-disk=yes", "--cache-secs=86400", // 24 hours "--sub-font-size=35", "--sub-margin-y=45", "--sub-border-size=1.6", "--sub-border-color=0.01", cache_dir.c_str(), input_conf.c_str(), wid_arg.c_str() }); if(is_running_wayland()) { args.push_back("--gpu-context=x11egl"); fprintf(stderr, "Wayland detected. Launching mpv in x11egl mode\n"); } if(keep_open) args.push_back("--keep-open=yes"); 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"; if(!use_youtube_dl) args.push_back("--no-ytdl"); else args.push_back(ytdl_format.c_str()); if(!use_system_mpv_config) { args.insert(args.end(), { "--no-config", "--profile=gpu-hq", "--vo=gpu,vdpau,x11", "--hwdec=auto" }); } std::string force_media_title_arg; if(!title.empty()) { force_media_title_arg = "--force-media-title=" + title; args.push_back(force_media_title_arg.c_str()); } if(no_video) args.push_back("--no-video"); std::string chapters_file_arg; if(tmp_chapters_filepath[0] != '\0') { chapters_file_arg = std::string("--chapters-file=") + tmp_chapters_filepath; args.push_back(chapters_file_arg.c_str()); } std::string audio_file_arg; if(audio_path && audio_path[0] != '\0') { audio_file_arg = std::string("--audio-file=") + audio_path; args.push_back(audio_file_arg.c_str()); } std::string start_time_arg; if(!start_time.empty()) { start_time_arg = "--start=" + start_time; args.push_back(start_time_arg.c_str()); } args.insert(args.end(), { "--", path, nullptr }); 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); if(flags != -1) // TODO: Proper error handling fcntl(ipc_socket, F_SETFL, flags | O_NONBLOCK); if(exec_program_async(args.data(), &video_process_id) != 0) { close(ipc_socket); ipc_socket = -1; return Error::FAIL_TO_LAUNCH_PROCESS; } return Error::OK; } VideoPlayer::Error VideoPlayer::load_video(const char *path, const char *audio_path, sf::WindowHandle _parent_window, bool use_youtube_dl, const std::string &title, const std::string &start_time, const std::vector &chapters) { // 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); assert(path); this->use_youtube_dl = use_youtube_dl; if(!create_tmp_file_with_chapters_data(tmp_chapters_filepath, chapters)) fprintf(stderr, "Warning: failed to create chapters file. Chapters will not be displayed\n"); fprintf(stderr, "Playing video: %s, audio: %s\n", path ? path : "", audio_path ? audio_path : ""); if(video_process_id == -1) return launch_video_process(path, audio_path, _parent_window, title, start_time); // TODO: When these are used, add audio_path, title and start_time. Also handle is_youtube 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 = nullptr; unsigned int num_children = 0; if(XQueryTree(display, window, &root_window, &parent_window, &child_window, &num_children) && child_window) { for(unsigned int i = 0; i < num_children; i++) result.push_back(child_window[i]); XFree(child_window); } return result; } VideoPlayer::Error VideoPlayer::update() { const int max_retries_find_window = 1000; 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); ipc_server_path[0] = '\0'; video_process_id = -1; ipc_socket = -1; window_handle = None; connected_to_ipc = false; return Error::EXITED; } } 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(connected_to_ipc && window_handle && 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 && err != EWOULDBLOCK) { 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::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::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"] = std::move(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"] = std::move(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::add_subtitle(const std::string &url, const std::string &title, const std::string &lang) { Json::Value command_data(Json::arrayValue); command_data.append("sub-add"); command_data.append(url); command_data.append("auto"); if(!title.empty()) { command_data.append(title); if(!lang.empty()) command_data.append(lang); } Json::Value command(Json::objectValue); command["command"] = std::move(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; } }