#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 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_blocking(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 && errno != EWOULDBLOCK) return -1; } else { bytes_written += written; } } return bytes_written; } namespace QuickMedia { static const double RETRY_TIME_SEC = 0.5; static const int MAX_RETRIES_CONNECT = 1000; static const double READ_TIMEOUT_SEC = 3.0; 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(StartupArgs startup_args, EventCallbackFunc _event_callback, VideoPlayerWindowCreateCallback _window_create_callback) : exit_status(0), startup_args(std::move(startup_args)), video_process_id(-1), connect_tries(0), find_window_tries(0), event_callback(std::move(_event_callback)), window_create_callback(std::move(_window_create_callback)), window_handle(0), display(nullptr), request_id_counter(1), expected_request_id(0), request_response_data(Json::nullValue), response_data_status(ResponseDataStatus::NONE) { 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", startup_args.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(display) XCloseDisplay(display); if(tmp_chapters_filepath[0] != '\0') remove(tmp_chapters_filepath); } static Path get_config_dir_xdg() { Path path; const char *xdg_config_home_p = getenv("XDG_CONFIG_HOME"); if(xdg_config_home_p) path = xdg_config_home_p; else path = get_home_dir().join(".config"); return path; } VideoPlayer::Error VideoPlayer::launch_video_process() { int fd[2]; if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0) { perror("Failed to create socketpair for video player"); return Error::FAIL_TO_CREATE_SOCKET; } ipc_socket = fd[0]; int flags = fcntl(ipc_socket, F_GETFL, 0); if(flags != -1) fcntl(ipc_socket, F_SETFL, flags | O_NONBLOCK); const std::string ipc_fd = std::to_string(fd[1]); std::string cache_dir = "--cache-dir=" + std::move(get_cache_dir().join("media").data); std::string wid_arg = "--wid="; wid_arg += std::to_string(startup_args.parent_window); std::string video_player_filepath = startup_args.resource_root + "/video_player/sibs-build/linux_x86_64/" #ifdef NDEBUG "release/" #else "debug/" #endif "quickmedia-video-player"; if(get_file_type(video_player_filepath.c_str()) != FileType::REGULAR) video_player_filepath = "/usr/bin/quickmedia-video-player"; std::string config_dir = "--config-dir=" + startup_args.resource_root + "mpv"; std::vector args; // TODO: Resume playback if the last video played matches the first video played next time QuickMedia is launched args.insert(args.end(), { video_player_filepath.c_str(), "--cursor-autohide=no", "--profile=pseudo-gui", // For gui when playing audio, requires a version of mpv that isn't ancient // TODO: Disable hr seek on low power devices? "--hr-seek=yes", "--force-seekable=yes", "--image-display-duration=5", "--cache-pause=yes", "--sub-font-size=50", "--sub-margin-y=60", "--sub-border-size=2.0", "--sub-bold=yes", "--input-default-bindings=yes", "--input-vo-keyboard=yes", wid_arg.c_str(), "--ipc-fd", ipc_fd.c_str() }); if(startup_args.cache_on_disk) { args.insert(args.end(), { "--cache=yes", "--cache-on-disk=yes", "--cache-secs=86400", // 24 hours cache_dir.c_str() }); } if(startup_args.resume) { args.push_back("--save-position-on-quit=yes"); args.push_back("--resume-playback=yes"); } else { args.push_back("--save-position-on-quit=no"); args.push_back("--resume-playback=no"); } if(is_running_wayland()) { args.push_back("--gpu-context=x11egl"); fprintf(stderr, "Wayland detected. Launching mpv in x11egl mode\n"); } if(startup_args.keep_open) args.push_back("--keep-open=yes"); std::string ytdl_format; if(startup_args.no_video) ytdl_format = "--ytdl-format=bestaudio/best"; else ytdl_format = "--ytdl-format=bestvideo[height<=?" + std::to_string(startup_args.monitor_height) + "]+bestaudio/best"; if(!startup_args.use_youtube_dl) args.push_back("--ytdl=no"); else args.push_back(ytdl_format.c_str()); std::string mpris_arg; Path mpris_path = get_config_dir_xdg().join("mpv").join("scripts").join("mpris.so"); if(get_file_type(mpris_path) == FileType::REGULAR) mpris_arg = "--scripts=" + mpris_path.data; if(startup_args.use_system_mpv_config) { args.push_back("--config=yes"); args.push_back("--load-scripts=yes"); args.push_back("--osc=yes"); } else { args.insert(args.end(), { config_dir.c_str(), "--config=yes" }); if(!mpris_arg.empty()) args.push_back(mpris_arg.c_str()); } std::string force_media_title_arg; if(!startup_args.title.empty()) { force_media_title_arg = "--force-media-title=" + startup_args.title; args.push_back(force_media_title_arg.c_str()); } if(startup_args.no_video) args.push_back("--video=no"); 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()); } if(!startup_args.audio_path.empty()) { args.push_back("--audio-file"); args.push_back(startup_args.audio_path.c_str()); } std::string start_time_arg; if(!startup_args.start_time.empty()) { start_time_arg = "--start=" + startup_args.start_time; args.push_back(start_time_arg.c_str()); } args.insert(args.end(), { "--", startup_args.path.c_str(), nullptr }); if(exec_program_async(args.data(), &video_process_id) != 0) { close(fd[1]); close(ipc_socket); ipc_socket = -1; return Error::FAIL_TO_LAUNCH_PROCESS; } close(fd[1]); return Error::OK; } VideoPlayer::Error VideoPlayer::load_video() { // 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(!startup_args.path.empty()); if(!create_tmp_file_with_chapters_data(tmp_chapters_filepath, startup_args.chapters)) fprintf(stderr, "Warning: failed to create chapters file. Chapters will not be displayed\n"); fprintf(stderr, "Playing video: %s, audio: %s\n", startup_args.path.c_str(), startup_args.audio_path.c_str()); if(video_process_id == -1) return launch_video_process(); fprintf(stderr, "TODO: Implement VideoPlayer::load_video without restarting the video player\n"); abort(); return VideoPlayer::Error::INIT_FAILED; } 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); video_process_id = -1; ipc_socket = -1; window_handle = None; 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(window_handle == 0 && retry_timer.get_elapsed_time_seconds() >= RETRY_TIME_SEC) { retry_timer.restart(); std::vector child_windows = get_child_window(display, startup_args.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", (int)(RETRY_TIME_SEC * max_retries_find_window)); 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(window_handle && event_callback) { Error err = read_ipc_func(); if(err != Error::OK) return err; } return Error::OK; } static std::vector json_array_to_string_list(const Json::Value &json) { std::vector result; if(!json.isArray()) return result; for(const Json::Value &item : json) { if(!item.isString()) continue; result.push_back(item.asString()); } return result; } VideoPlayer::Error VideoPlayer::read_ipc_func() { 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_eintr(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 &args = json_root["args"]; if(event.isString()) { if(event_callback) event_callback(event.asCString(), json_array_to_string_list(args)); } const Json::Value &request_id_json = json_root["request_id"]; if(expected_request_id != 0 && request_id_json.isNumeric() && request_id_json.asUInt() == expected_request_id) { const Json::Value &status_json = json_root["status"]; if(!status_json.isString() || strcmp(status_json.asCString(), "error") == 0) { response_data_status = ResponseDataStatus::ERROR; const char *err_msg = "Unknown"; const Json::Value &message_json = json_root["message"]; if(message_json.isString()) err_msg = message_json.asCString(); fprintf(stderr, "VideoPlayer::send_command failed, error from video player: %s\n", err_msg); } 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 json_root(Json::objectValue); json_root["command"] = "time-pos"; Json::Value time_pos_json; Error err = send_command(json_root, &time_pos_json, Json::ValueType::realValue); if(err != Error::OK) return err; *result = time_pos_json.asDouble(); return err; } VideoPlayer::Error VideoPlayer::get_duration_in_file(double *result) { Json::Value json_root(Json::objectValue); json_root["command"] = "duration"; Json::Value duration_json; Error err = send_command(json_root, &duration_json, Json::ValueType::realValue); if(err != Error::OK) return err; *result = duration_json.asDouble(); return err; } VideoPlayer::Error VideoPlayer::add_subtitle(const std::string &url, const std::string &title, const std::string &lang) { Json::Value data_json(Json::objectValue); data_json["file"] = url; if(!title.empty()) { data_json["title"] = title; if(!lang.empty()) data_json["language"] = title; } Json::Value json_root(Json::objectValue); json_root["command"] = "sub-add"; json_root["data"] = std::move(data_json); Json::Value result; return send_command(json_root, &result, Json::ValueType::nullValue); } VideoPlayer::Error VideoPlayer::cycle_fullscreen() { Json::Value json_root(Json::objectValue); json_root["command"] = "cycle-fullscreen"; Json::Value result; return send_command(json_root, &result, Json::ValueType::nullValue); } uint32_t VideoPlayer::get_next_request_id() { unsigned int cmd_request_id = request_id_counter; ++request_id_counter; // Overflow check. 0 is defined as no request, 1 is the first valid one if(request_id_counter == 0) request_id_counter = 1; return cmd_request_id; } VideoPlayer::Error VideoPlayer::send_command(Json::Value &json_root, Json::Value *result, Json::ValueType result_type) { const uint32_t request_id = get_next_request_id(); json_root["request_id"] = request_id; Json::StreamWriterBuilder builder; builder["commentStyle"] = "None"; builder["indentation"] = ""; const std::string cmd_str = Json::writeString(builder, json_root) + "\n"; if(write_all_blocking(ipc_socket, cmd_str.data(), cmd_str.size()) == -1) { fprintf(stderr, "Failed to send to ipc socket, error: %s, command: %.*s\n", strerror(errno), (int)cmd_str.size(), cmd_str.c_str()); return Error::FAIL_TO_SEND; } VideoPlayer::Error err; mgl::Clock read_timer; expected_request_id = request_id; do { err = read_ipc_func(); if(err != Error::OK) goto cleanup; if(response_data_status != ResponseDataStatus::NONE) break; } while(read_timer.get_elapsed_time_seconds() < READ_TIMEOUT_SEC); 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; } }