#ifndef PLEZY_SHARED_MPV_PLAYER_COMMON_H_ #define PLEZY_SHARED_MPV_PLAYER_COMMON_H_ #include #include #include #include #include #include #include #include #include #include #include namespace plezy { namespace mpv_common { using StatusCallback = std::function; using GetPropertyCallback = std::function; static constexpr char kSetPropertyFailedCode[] = "SET_PROPERTY_FAILED"; static constexpr char kSetPropertyNotInitializedCode[] = "NOT_INITIALIZED"; static constexpr size_t kSetPropertyErrorDescriptionLimit = 160; inline bool SetPropertyStatusSucceeded(int status) { return status >= 0; } inline const char* SetPropertyErrorCode(int status) { return status == MPV_ERROR_UNINITIALIZED ? kSetPropertyNotInitializedCode : kSetPropertyFailedCode; } inline std::string SetPropertyErrorDescription(int status) { const char* description = mpv_error_string(status); if (!description || description[0] == '\0') { return "MPV property write failed"; } size_t length = 0; while (length < kSetPropertyErrorDescriptionLimit && description[length] != '\0') { ++length; } return std::string(description, length); } struct CancelledRequests { std::vector status; std::vector properties; }; class AsyncRequestRegistry { public: uint64_t RegisterStatus(StatusCallback callback) { std::lock_guard lock(mutex_); const uint64_t request_id = next_id_++; status_[request_id] = std::move(callback); return request_id; } StatusCallback TakeStatus(uint64_t request_id) { std::lock_guard lock(mutex_); auto it = status_.find(request_id); if (it == status_.end()) return nullptr; auto callback = std::move(it->second); status_.erase(it); return callback; } uint64_t RegisterProperty(GetPropertyCallback callback) { std::lock_guard lock(mutex_); const uint64_t request_id = next_id_++; properties_[request_id] = std::move(callback); return request_id; } GetPropertyCallback TakeProperty(uint64_t request_id) { std::lock_guard lock(mutex_); auto it = properties_.find(request_id); if (it == properties_.end()) return nullptr; auto callback = std::move(it->second); properties_.erase(it); return callback; } CancelledRequests CancelAll() { CancelledRequests cancelled; std::lock_guard lock(mutex_); cancelled.status.reserve(status_.size()); for (auto& request : status_) { if (request.second) { cancelled.status.push_back(std::move(request.second)); } } cancelled.properties.reserve(properties_.size()); for (auto& request : properties_) { if (request.second) { cancelled.properties.push_back(std::move(request.second)); } } status_.clear(); properties_.clear(); return cancelled; } private: uint64_t next_id_ = 1; std::map status_; std::map properties_; std::mutex mutex_; }; inline mpv_format ParsePropertyFormat(const std::string& format) { if (format == "string") return MPV_FORMAT_STRING; if (format == "flag" || format == "bool") return MPV_FORMAT_FLAG; if (format == "int64") return MPV_FORMAT_INT64; if (format == "double") return MPV_FORMAT_DOUBLE; if (format == "node") return MPV_FORMAT_NODE; return MPV_FORMAT_NONE; } struct ObservationRequest { bool added; uint64_t userdata; mpv_format format; }; class PropertyObservationRegistry { public: ObservationRequest Register(const std::string& name, const std::string& format, int id) { const mpv_format parsed_format = ParsePropertyFormat(format); std::lock_guard lock(mutex_); if (userdata_by_name_.find(name) != userdata_by_name_.end()) { return {false, 0, MPV_FORMAT_NONE}; } const uint64_t userdata = next_userdata_++; userdata_by_name_[name] = userdata; id_by_name_[name] = id; return {true, userdata, parsed_format}; } bool LookupId(const std::string& name, int* id) const { std::lock_guard lock(mutex_); const auto it = id_by_name_.find(name); if (it == id_by_name_.end()) return false; *id = it->second; return true; } void Clear() { std::lock_guard lock(mutex_); userdata_by_name_.clear(); id_by_name_.clear(); } private: uint64_t next_userdata_ = 1; std::map userdata_by_name_; std::map id_by_name_; mutable std::mutex mutex_; }; inline bool ParseEnabledFlag(const std::string& value) { return value == "yes" || value == "true" || value == "1"; } inline const char* TargetColorspaceHint(bool hdr_enabled) { return hdr_enabled ? "auto" : "no"; } enum class AudioReloadReason { kNone, kResume, kNullFallback }; struct AudioReloadAction { AudioReloadReason reason = AudioReloadReason::kNone; int attempt = 0; bool exhausted = false; uint64_t request_generation = 0; }; enum class AudioOutputTransition { kNone, kFellBackToNull, kRecovered }; class AudioRecoveryState { public: using Clock = std::chrono::steady_clock; void SetFileLoaded(bool loaded, Clock::time_point now = Clock::now()) { std::lock_guard lock(mutex_); const bool was_loaded = file_loaded_; file_loaded_ = loaded; if (!loaded) { resume_requested_ = false; resume_attempts_left_ = 0; null_attempts_left_ = 0; reload_pending_ = false; pending_request_generation_ = 0; return; } if (!was_loaded && current_ao_is_null_) { null_attempts_left_ = kNullRetryBudget; null_backoff_ = NullFirstDelay(); null_next_attempt_ = now + NullFirstDelay(); } } void RequestResume() { std::lock_guard lock(mutex_); if (!file_loaded_) { resume_requested_ = false; resume_attempts_left_ = 0; return; } resume_requested_ = true; } AudioOutputTransition SetCurrentAudioOutputNull(bool is_null, Clock::time_point now) { std::lock_guard lock(mutex_); if (is_null == current_ao_is_null_) return AudioOutputTransition::kNone; current_ao_is_null_ = is_null; if (is_null) { if (file_loaded_) { null_attempts_left_ = kNullRetryBudget; null_backoff_ = NullFirstDelay(); null_next_attempt_ = now + NullFirstDelay(); } return AudioOutputTransition::kFellBackToNull; } null_attempts_left_ = 0; return AudioOutputTransition::kRecovered; } bool OnAudioDeviceListChanged(Clock::time_point now) { std::lock_guard lock(mutex_); if (!file_loaded_ || !current_ao_is_null_) return false; const auto candidate = now + DeviceListDebounce(); if (null_attempts_left_ <= 0 || candidate < null_next_attempt_) { null_next_attempt_ = candidate; } null_attempts_left_ = kNullRetryBudget; null_backoff_ = NullFirstDelay(); return true; } AudioReloadAction NextReload(Clock::time_point now) { std::lock_guard lock(mutex_); if (resume_requested_ && file_loaded_) { resume_requested_ = false; resume_attempts_left_ = kResumeReloadAttempts; resume_next_attempt_ = now + ResumeFirstDelay(); } if (reload_pending_) return {}; if (resume_attempts_left_ > 0 && now >= resume_next_attempt_) { const int attempt = kResumeReloadAttempts - resume_attempts_left_ + 1; --resume_attempts_left_; resume_next_attempt_ = now + ResumeRetryDelay(); reload_pending_ = true; pending_request_generation_ = ++next_request_generation_; return {AudioReloadReason::kResume, attempt, false, pending_request_generation_}; } if (null_attempts_left_ > 0 && now >= null_next_attempt_) { if (!current_ao_is_null_) { null_attempts_left_ = 0; return {}; } const int attempt = kNullRetryBudget - null_attempts_left_ + 1; --null_attempts_left_; null_next_attempt_ = now + null_backoff_; null_backoff_ = std::min(null_backoff_ * 2, NullBackoffCap()); reload_pending_ = true; pending_request_generation_ = ++next_request_generation_; return {AudioReloadReason::kNullFallback, attempt, null_attempts_left_ == 0, pending_request_generation_}; } return {}; } bool CompleteReload(uint64_t request_generation) { std::lock_guard lock(mutex_); if (!reload_pending_ || pending_request_generation_ != request_generation) { return false; } reload_pending_ = false; pending_request_generation_ = 0; return true; } bool HasPendingWork() const { std::lock_guard lock(mutex_); return file_loaded_ && (resume_requested_ || resume_attempts_left_ > 0 || null_attempts_left_ > 0 || reload_pending_); } private: static constexpr int kResumeReloadAttempts = 2; static constexpr int kNullRetryBudget = 5; static std::chrono::milliseconds ResumeFirstDelay() { return std::chrono::milliseconds(1500); } static std::chrono::milliseconds ResumeRetryDelay() { return std::chrono::milliseconds(4500); } static std::chrono::milliseconds NullFirstDelay() { return std::chrono::milliseconds(500); } static std::chrono::milliseconds NullBackoffCap() { return std::chrono::milliseconds(8000); } static std::chrono::milliseconds DeviceListDebounce() { return std::chrono::milliseconds(250); } bool resume_requested_ = false; bool file_loaded_ = false; bool current_ao_is_null_ = false; bool reload_pending_ = false; uint64_t next_request_generation_ = 0; uint64_t pending_request_generation_ = 0; int resume_attempts_left_ = 0; Clock::time_point resume_next_attempt_{}; int null_attempts_left_ = 0; Clock::time_point null_next_attempt_{}; std::chrono::milliseconds null_backoff_{0}; mutable std::mutex mutex_; }; } // namespace mpv_common } // namespace plezy #endif // PLEZY_SHARED_MPV_PLAYER_COMMON_H_