#include "capture.h" #include #include #include #include #include #include #include #include #include #include namespace crossdesk::harmony { bool Capture::Start(int width, int height, uint64_t display, Frame frame, State state, Audio audio, int fps, bool persistent_permission, bool show_cursor) { Stop(); frame_ = std::move(frame); state_ = std::move(state); audio_ = std::move(audio); show_cursor_ = show_cursor; first_video_ = true; first_data_ = true; max_frame_rate_ = std::clamp(fps, 1, 60); capture_ = OH_AVScreenCapture_Create(); if (!capture_) return false; window_ = gpu_surface_.Start(width, height, [this](const uint8_t* pixels, int width, int height, int stride) { if (active_) ConvertRgba(pixels, width, height, stride); }); const bool surface_mode = window_ != nullptr || PrepareSurface(width, height); const auto state_result = OH_AVScreenCapture_SetStateCallback(capture_, [](OH_AVScreenCapture* capture, OH_AVScreenCaptureStateCode code, void* user) { auto* self = static_cast(user); if (code == OH_SCREEN_CAPTURE_STATE_STARTED) { // Both operations require a running capture. StartScreenCapture may // return while the system is still awaiting the user's authorization. const auto rate = OH_AVScreenCapture_SetMaxVideoFrameRate(capture, self->max_frame_rate_); if (rate != AV_SCREEN_CAPTURE_ERR_OK) OH_LOG_Print(LOG_APP, LOG_WARN, 0, "SlintDesktop", "Capture frame rate unavailable: %{public}d", int(rate)); } if (code == OH_SCREEN_CAPTURE_STATE_STARTED || code == OH_SCREEN_CAPTURE_STATE_EXIT_PRIVATE_SCENE || code == OH_SCREEN_CAPTURE_STATE_RESUMED_BY_USER || code == OH_SCREEN_CAPTURE_STATE_RESUMED_BY_APP) { // Native viewers draw the separately transmitted cursor. Only advertise // that mode once the recorder confirms it has removed the video cursor. const auto result = OH_AVScreenCapture_ShowCursor(capture, self->show_cursor_); if (result == AV_SCREEN_CAPTURE_ERR_OK) { self->cursor_mode_ = self->show_cursor_ ? CaptureCursorMode::embedded : CaptureCursorMode::separate; } else { self->cursor_mode_ = !self->show_cursor_ && OH_AVScreenCapture_ShowCursor(capture, true) == AV_SCREEN_CAPTURE_ERR_OK ? CaptureCursorMode::embedded : CaptureCursorMode::unknown; OH_LOG_Print(LOG_APP, LOG_WARN, 0, "SlintDesktop", "Capture cursor setting failed: %{public}d", int(result)); } } self->active_ = CaptureActiveForState(code, self->active_); self->state_(static_cast(code)); }, this); const auto error_result = OH_AVScreenCapture_SetErrorCallback(capture_, [](OH_AVScreenCapture*, int32_t error, void* user) { OH_LOG_Print(LOG_APP, LOG_ERROR, 0, "SlintDesktop", "Capture error: %{public}d", error); auto* self = static_cast(user); self->active_ = false; self->state_(-1); }, this); const auto data_result = OH_AVScreenCapture_SetDataCallback(capture_, [](OH_AVScreenCapture*, OH_AVBuffer* buffer, OH_AVScreenCaptureBufferType type, int64_t, void* user) { auto* self = static_cast(user); if (self->first_data_.exchange(false)) OH_LOG_Print(LOG_APP, LOG_INFO, 0, "SlintDesktop", "Capture first data: type=%{public}d active=%{public}d", int(type), int(self->active_.load())); if (!self->active_) return; if (type == OH_SCREEN_CAPTURE_BUFFERTYPE_AUDIO_INNER && self->audio_) { OH_AVCodecBufferAttr attr{}; auto* data = OH_AVBuffer_GetAddr(buffer); if (data && OH_AVBuffer_GetBufferAttr(buffer, &attr) == AV_ERR_OK && attr.offset >= 0 && attr.size > 0 && int64_t(attr.offset) + attr.size <= OH_AVBuffer_GetCapacity(buffer)) self->audio_(reinterpret_cast(data + attr.offset), attr.size); return; } if (type != OH_SCREEN_CAPTURE_BUFFERTYPE_VIDEO) return; const bool first_video = self->first_video_.exchange(false); auto* native = OH_AVBuffer_GetNativeBuffer(buffer); if (!native) { if (first_video) OH_LOG_Print(LOG_APP, LOG_WARN, 0, "SlintDesktop", "Capture video has no native buffer"); return; } self->Convert(native, first_video); OH_NativeBuffer_Unreference(native); }, this); OH_AVScreenCaptureConfig config{}; OH_LOG_Print(LOG_APP, LOG_INFO, 0, "SlintDesktop", "Capture request: %{public}dx%{public}d display=%{public}llu fps=%{public}d", width, height, static_cast(display), max_frame_rate_); config.captureMode = OH_CAPTURE_SPECIFIED_SCREEN; config.dataType = OH_ORIGINAL_STREAM; config.videoInfo.videoCapInfo = {display, nullptr, 0, width, height, OH_VIDEO_SOURCE_SURFACE_RGBA}; if (audio_) config.audioInfo.innerCapInfo = {48000, 1, OH_ALL_PLAYBACK}; const auto check = [](const char* stage, OH_AVSCREEN_CAPTURE_ErrCode result) { if (result == AV_SCREEN_CAPTURE_ERR_OK) return true; OH_LOG_Print(LOG_APP, LOG_ERROR, 0, "SlintDesktop", "Capture %{public}s failed: %{public}d", stage, int(result)); return false; }; if (!check("state callback", state_result) || !check("error callback", error_result) || !check("data callback", data_result) || !check("initialization", OH_AVScreenCapture_Init(capture_, config)) || !check("microphone disable", OH_AVScreenCapture_SetMicrophoneEnabled(capture_, false))) { Stop(); return false; } if (persistent_permission) { // With CUSTOM_SCREEN_RECORDING granted, the system can reuse consent for // this specified display. Disabling the picker alone never grants access. auto* strategy = OH_AVScreenCapture_CreateCaptureStrategy(); bool configured = false; if (strategy) { configured = OH_AVScreenCapture_StrategyForPickerPopUp(strategy, false) == AV_SCREEN_CAPTURE_ERR_OK && OH_AVScreenCapture_SetCaptureStrategy(capture_, strategy) == AV_SCREEN_CAPTURE_ERR_OK; OH_AVScreenCapture_ReleaseCaptureStrategy(strategy); } if (!configured) OH_LOG_Print(LOG_APP, LOG_WARN, 0, "SlintDesktop", "Persistent capture strategy unavailable; using system authorization"); } if (!check("start", surface_mode ? OH_AVScreenCapture_StartScreenCaptureWithSurface(capture_, window_) : OH_AVScreenCapture_StartScreenCapture(capture_))) { Stop(); return false; } return true; } bool Capture::PrepareSurface(int width, int height) { image_ = OH_ConsumerSurface_Create(); if (!image_) return false; if (OH_ConsumerSurface_SetDefaultUsage(image_, NATIVEBUFFER_USAGE_CPU_READ | NATIVEBUFFER_USAGE_CPU_READ_OFTEN | NATIVEBUFFER_USAGE_HW_RENDER) != 0 || OH_ConsumerSurface_SetDefaultSize(image_, width, height) != 0 || !(window_ = OH_NativeImage_AcquireNativeWindow(image_))) { OH_NativeImage_Destroy(&image_); return false; } surface_pending_ = false; surface_stopping_ = false; OH_OnFrameAvailableListener listener{this, [](void* context) { auto* self = static_cast(context); std::lock_guard lock(self->surface_mutex_); self->surface_pending_ = true; self->surface_ready_.notify_one(); }}; if (OH_NativeImage_SetOnFrameAvailableListener(image_, listener) != 0) { window_ = nullptr; // The native image owns its producer window. OH_NativeImage_Destroy(&image_); return false; } surface_thread_ = std::thread([this] { ConsumeSurface(); }); return true; } void Capture::ConsumeSurface() { for (;;) { { std::unique_lock lock(surface_mutex_); surface_ready_.wait(lock, [this] { return surface_stopping_ || surface_pending_; }); if (surface_stopping_) return; surface_pending_ = false; } // Notifications may coalesce while conversion is running. Drain the queue // so a static desktop cannot leave its newest frame waiting indefinitely. for (;;) { { std::lock_guard lock(surface_mutex_); if (surface_stopping_) return; } OHNativeWindowBuffer* buffer = nullptr; int fence = -1; if (OH_NativeImage_AcquireNativeWindowBuffer(image_, &buffer, &fence) != 0) break; bool ready = true; if (fence >= 0) { pollfd descriptor{fence, POLLIN, 0}; int result; do { result = poll(&descriptor, 1, 1000); } while (result < 0 && errno == EINTR); ready = result > 0 && (descriptor.revents & POLLIN); } OH_NativeBuffer* native = nullptr; if (ready && active_ && OH_NativeBuffer_FromNativeWindowBuffer(buffer, &native) == 0 && native) Convert(native, first_video_.exchange(false)); // Return an unsignaled fence to the producer on timeout; never let it // reuse memory while a GPU write is still outstanding. OH_NativeImage_ReleaseNativeWindowBuffer(image_, buffer, ready ? -1 : fence); if (ready && fence >= 0) close(fence); } } } void Capture::Convert(OH_NativeBuffer* native, bool first_video) { OH_NativeBuffer_Config config{}; OH_NativeBuffer_GetConfig(native, &config); if (first_video) OH_LOG_Print(LOG_APP, LOG_INFO, 0, "SlintDesktop", "Capture buffer: %{public}dx%{public}d format=%{public}d stride=%{public}d", config.width, config.height, config.format, config.stride); void* pixels = nullptr; if (config.format == NATIVEBUFFER_PIXEL_FMT_RGBA_8888 && config.width > 0 && config.height > 0 && config.width <= 4096 && config.height <= 4096 && !(config.width & 1) && !(config.height & 1) && config.stride >= config.width * 4 && OH_NativeBuffer_Map(native, &pixels) == 0 && pixels) { ConvertRgba(static_cast(pixels), config.width, config.height, config.stride); OH_NativeBuffer_Unmap(native); } } void Capture::ConvertRgba(const uint8_t* pixels, int width, int height, int stride) { std::lock_guard lock(data_mutex_); const size_t area = size_t(width) * height; nv12_.resize(area * 3 / 2); if (libyuv::ABGRToNV12(pixels, stride, nv12_.data(), width, nv12_.data() + area, width, width, height) == 0 && active_) { MiniRtcVideoFrame video{}; video.data = reinterpret_cast(nv12_.data()); video.size = nv12_.size(); video.width = width; video.height = height; frame_(video); } } void Capture::Stop() { cursor_mode_ = CaptureCursorMode::unknown; active_ = false; if (capture_) { OH_AVScreenCapture_StopScreenCapture(capture_); OH_AVScreenCapture_Release(capture_); capture_ = nullptr; } gpu_surface_.Stop(); if (image_) { OH_NativeImage_UnsetOnFrameAvailableListener(image_); { std::lock_guard lock(surface_mutex_); surface_stopping_ = true; surface_ready_.notify_one(); } if (surface_thread_.joinable()) surface_thread_.join(); window_ = nullptr; // OH_NativeImage_Destroy releases the producer window. OH_NativeImage_Destroy(&image_); } window_ = nullptr; } }