From 15cec9fec15267f2f61094fcd6e27832a8dfbd22 Mon Sep 17 00:00:00 2001 From: xBruno1000x Date: Sun, 9 Aug 2026 02:33:25 -0300 Subject: [PATCH 1/2] feat(client): expose game screen capture to CEF --- README.md | 33 ++ src/client/core/app.cpp | 3 +- src/client/core/browser/client.cpp | 20 ++ src/client/core/browser/manager.cpp | 97 ++++++ src/client/core/browser/manager.hpp | 7 + src/client/core/rendering/screen_capture.cpp | 282 ++++++++++++++++++ src/client/core/rendering/screen_capture.hpp | 94 ++++++ src/client/renderer/main.cpp | 143 ++++++++- tests/README.md | 14 + .../omp/scriptfiles/cef/cef_demo/camera.html | 66 ++++ .../samp/scriptfiles/cef/cef_demo/camera.html | 66 ++++ tests/shared/cef_demo.inc | 29 +- tests/webviews/react/src/vite-env.d.ts | 18 ++ 13 files changed, 868 insertions(+), 4 deletions(-) create mode 100644 src/client/core/rendering/screen_capture.cpp create mode 100644 src/client/core/rendering/screen_capture.hpp create mode 100644 tests/omp/scriptfiles/cef/cef_demo/camera.html create mode 100644 tests/samp/scriptfiles/cef/cef_demo/camera.html diff --git a/README.md b/README.md index 3966b243..6e82c512 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Client/server CEF plugin for **open.mp** and **SA-MP**. - Pawn natives and callbacks - JavaScript <-> Pawn/C# event bridge - Focus, cursor, input and audio controls +- Game screen capture exposed to CEF interfaces as RGBA frames - Custom ESC menu support - Custom TAB menu (scoreboard) support - Secure client/server handshake and UDP transport @@ -83,6 +84,38 @@ Short version: - Browser lifecycle - Hidden browser navigation - Stress testing +- In-game camera preview (`/camera` in the demo gamemode) + +## Game screen capture + +CEF pages can receive the current GTA frame through `window.cef.screen`. Frames +are captured before omp-cef overlays are rendered, preventing recursive capture +when a preview is displayed inside a browser. + +```js +const canvas = document.querySelector('canvas'); +const context = canvas.getContext('2d', { alpha: false }); + +cef.screen.start((frame) => { + canvas.width = frame.width; + canvas.height = frame.height; + + const pixels = new Uint8ClampedArray(frame.data); + context.putImageData( + new ImageData(pixels, frame.width, frame.height), + 0, + 0 + ); +}, 640, 360, 15); + +addEventListener('beforeunload', () => cef.screen.stop(), { once: true }); +``` + +`cef.screen.start(callback, width, height, fps)` returns frames with `data` +(`ArrayBuffer`, RGBA), `width`, `height`, `sequence`, `timestamp`, and `format`. +Width and height are clamped to `16..1280` and `16..720`; FPS is clamped to +`1..30`. Defaults are `640x360` at `15 FPS`. High resolutions may reduce the +effective FPS, and capture stops automatically when the page or browser closes. ## Contributing diff --git a/src/client/core/app.cpp b/src/client/core/app.cpp index d9073d87..6872c499 100644 --- a/src/client/core/app.cpp +++ b/src/client/core/app.cpp @@ -284,6 +284,7 @@ void App::Tick() focus_.Update(); browser_.TickGameData(); + browser_.CaptureScreen(); browser_.RenderAll(); if (pending_clear_chat_) @@ -605,4 +606,4 @@ void App::Shutdown() audio_.Shutdown(); LOG_INFO("Good bye! See you soon."); -} \ No newline at end of file +} diff --git a/src/client/core/browser/client.cpp b/src/client/core/browser/client.cpp index 25418738..fd95c6f7 100644 --- a/src/client/core/browser/client.cpp +++ b/src/client/core/browser/client.cpp @@ -163,6 +163,26 @@ bool BrowserClient::OnProcessMessageReceived(CefRefPtr /*browser*/, const std::string msg_name = message->GetName(); + if (msg_name == "cef_screen_capture_start") + { + CefRefPtr args = message->GetArgumentList(); + const int width = args->GetSize() > 0 && args->GetType(0) == VTYPE_INT + ? args->GetInt(0) : 640; + const int height = args->GetSize() > 1 && args->GetType(1) == VTYPE_INT + ? args->GetInt(1) : 360; + const int fps = args->GetSize() > 2 && args->GetType(2) == VTYPE_INT + ? args->GetInt(2) : 15; + + manager_.StartScreenCapture(browserId_, width, height, fps); + return true; + } + + if (msg_name == "cef_screen_capture_stop") + { + manager_.StopScreenCapture(browserId_); + return true; + } + if (msg_name == "emit_event") { CefRefPtr args = message->GetArgumentList(); diff --git a/src/client/core/browser/manager.cpp b/src/client/core/browser/manager.cpp index 80b41251..0a9683fa 100644 --- a/src/client/core/browser/manager.cpp +++ b/src/client/core/browser/manager.cpp @@ -542,6 +542,8 @@ void BrowserManager::Shutdown() LOG_DEBUG("Shutting down CEF Browser manager..."); + screen_capture_.StopAll(); + screen_capture_.OnDeviceLost(); browsers_.clear(); worldRenderers_.clear(); entityToBrowserId_.clear(); @@ -1123,10 +1125,104 @@ void BrowserManager::OnBrowserCreated(int id, CefRefPtr browser) void BrowserManager::OnBrowserClosed(int id) { player_stats_poll_.erase(id); + screen_capture_.Stop(id); pending_.erase(id); browsers_.erase(id); } +void BrowserManager::StartScreenCapture(int browserId, int width, int height, int fps) +{ + if (!GetBrowserInstance(browserId)) + return; + + const auto config = screen_capture_.Start(browserId, width, height, fps); + LOG_INFO("[ScreenCapture] Started for browser {} ({}x{} @ {} FPS)", + browserId, config.width, config.height, config.fps); +} + +void BrowserManager::StopScreenCapture(int browserId) +{ + screen_capture_.Stop(browserId); + LOG_INFO("[ScreenCapture] Stopped for browser {}", browserId); +} + +void BrowserManager::CaptureScreen() +{ + if (isCefUpdatesPaused_ || is_shutting_down_ || !screen_capture_.HasSubscriptions()) + return; + + auto* device = RenderManager::Instance().GetDevice(); + if (!device) + return; + + auto frames = screen_capture_.CaptureDueFrames(device, ::GetTickCount64()); + for (auto& frame : frames) + { + const int browser_id = frame->browser_id; + const uint64_t generation = frame->generation; + if (!CefPostTask(TID_UI, base::BindOnce( + &BrowserManager::DispatchScreenFrameOnUi, + base::Unretained(this), + std::move(frame)))) + { + screen_capture_.CompleteFrame(browser_id, generation); + } + } +} + +void BrowserManager::DispatchScreenFrameOnUi(std::shared_ptr frame) +{ + CEF_REQUIRE_UI_THREAD(); + + if (!frame) + return; + + const auto complete = [this, &frame]() { + screen_capture_.CompleteFrame(frame->browser_id, frame->generation); + }; + + if (is_shutting_down_ || + !screen_capture_.IsCurrent(frame->browser_id, frame->generation) || + !frame->rgba || frame->rgba->empty()) + { + complete(); + return; + } + + auto* instance = GetBrowserInstance(frame->browser_id); + if (!instance || !instance->browser || !instance->browser->IsValid()) + { + complete(); + return; + } + + CefRefPtr main_frame = instance->browser->GetMainFrame(); + if (!main_frame || !main_frame->IsValid()) + { + complete(); + return; + } + + CefRefPtr pixels = CefBinaryValue::Create( + frame->rgba->data(), frame->rgba->size()); + if (!pixels) + { + complete(); + return; + } + + CefRefPtr message = CefProcessMessage::Create("cef_screen_capture_frame"); + CefRefPtr args = message->GetArgumentList(); + args->SetBinary(0, pixels); + args->SetInt(1, frame->width); + args->SetInt(2, frame->height); + args->SetInt(3, static_cast(frame->sequence)); + args->SetDouble(4, frame->timestamp_ms); + main_frame->SendProcessMessage(PID_RENDERER, message); + + complete(); +} + void BrowserManager::ClearPendingPaint(int id) { auto it = pending_.find(id); @@ -1798,6 +1894,7 @@ void BrowserManager::OnDeviceLost() // Stop CEF rendering during device reset // This prevents CEF from trying to update textures while they're invalid isCefUpdatesPaused_ = true; + screen_capture_.OnDeviceLost(); // Release all browser View resources (2D overlays) for (auto& [id, instance] : browsers_) diff --git a/src/client/core/browser/manager.hpp b/src/client/core/browser/manager.hpp index 58814042..4da21ceb 100644 --- a/src/client/core/browser/manager.hpp +++ b/src/client/core/browser/manager.hpp @@ -14,6 +14,7 @@ #include "include/base/cef_callback.h" #include "include/cef_app.h" #include "include/cef_browser.h" +#include "rendering/screen_capture.hpp" #include "rendering/view.hpp" #include "rendering/world_renderer.hpp" #include "shared/packet.hpp" @@ -153,6 +154,10 @@ class BrowserManager void OnGameFocusGained(); void OnGameFocusLost(); + void StartScreenCapture(int browserId, int width, int height, int fps); + void StopScreenCapture(int browserId); + void CaptureScreen(); + void ExitGame(); // Native GTA SA ESC/pause menu handling @@ -217,6 +222,7 @@ class BrowserManager void RequestVisibleBrowsersRepaint(); void SendExternalBeginFrames(); void DispatchExternalBeginFramesOnUi(); + void DispatchScreenFrameOnUi(std::shared_ptr frame); private: bool initialized_ = false; @@ -250,6 +256,7 @@ class BrowserManager std::bitset<256> key_allowed_{}; std::unordered_map player_stats_poll_; + ScreenCaptureManager screen_capture_; // Native GTA SA ESC/pause menu handling EscapeMenuController escape_menu_; diff --git a/src/client/core/rendering/screen_capture.cpp b/src/client/core/rendering/screen_capture.cpp new file mode 100644 index 00000000..5b132609 --- /dev/null +++ b/src/client/core/rendering/screen_capture.cpp @@ -0,0 +1,282 @@ +#include "screen_capture.hpp" + +#include +#include +#include +#include + +#include "system/logger.hpp" + +namespace +{ + constexpr int kDefaultWidth = 640; + constexpr int kDefaultHeight = 360; + constexpr int kDefaultFps = 15; + constexpr int kMinDimension = 16; + constexpr int kMaxWidth = 1280; + constexpr int kMaxHeight = 720; + constexpr int kMinFps = 1; + constexpr int kMaxFps = 30; + constexpr int kMaxPixelsPerSecond = 1280 * 720 * 15; +} + +ScreenCaptureManager::~ScreenCaptureManager() +{ + ReleaseResources(); +} + +ScreenCaptureManager::Configuration ScreenCaptureManager::Sanitize(int width, int height, int fps) +{ + Configuration config; + config.width = std::clamp(width > 0 ? width : kDefaultWidth, kMinDimension, kMaxWidth); + config.height = std::clamp(height > 0 ? height : kDefaultHeight, kMinDimension, kMaxHeight); + config.fps = std::clamp(fps > 0 ? fps : kDefaultFps, kMinFps, kMaxFps); + const int pixel_rate_fps = std::max( + kMinFps, + kMaxPixelsPerSecond / (config.width * config.height)); + config.fps = std::min(config.fps, pixel_rate_fps); + return config; +} + +ScreenCaptureManager::Configuration ScreenCaptureManager::Start( + int browser_id, + int width, + int height, + int fps) +{ + const Configuration config = Sanitize(width, height, fps); + + std::lock_guard lock(subscriptions_mutex_); + auto& subscription = subscriptions_[browser_id]; + subscription.config = config; + subscription.generation = next_generation_++; + subscription.next_capture_ms = 0; + subscription.sequence = 0; + subscription.frame_pending = false; + return config; +} + +void ScreenCaptureManager::Stop(int browser_id) +{ + std::lock_guard lock(subscriptions_mutex_); + subscriptions_.erase(browser_id); +} + +void ScreenCaptureManager::StopAll() +{ + std::lock_guard lock(subscriptions_mutex_); + subscriptions_.clear(); +} + +bool ScreenCaptureManager::HasSubscriptions() const +{ + std::lock_guard lock(subscriptions_mutex_); + return !subscriptions_.empty(); +} + +bool ScreenCaptureManager::IsCurrent(int browser_id, uint64_t generation) const +{ + std::lock_guard lock(subscriptions_mutex_); + const auto it = subscriptions_.find(browser_id); + return it != subscriptions_.end() && it->second.generation == generation; +} + +void ScreenCaptureManager::CompleteFrame(int browser_id, uint64_t generation) +{ + std::lock_guard lock(subscriptions_mutex_); + const auto it = subscriptions_.find(browser_id); + if (it != subscriptions_.end() && it->second.generation == generation) + it->second.frame_pending = false; +} + +uint64_t ScreenCaptureManager::ResolutionKey(int width, int height) noexcept +{ + return (static_cast(static_cast(width)) << 32) | + static_cast(height); +} + +ScreenCaptureManager::CaptureResources* ScreenCaptureManager::GetOrCreateResources( + IDirect3DDevice9* device, + int width, + int height) +{ + if (!device) + return nullptr; + + if (resource_device_ != device) + { + ReleaseResources(); + resource_device_ = device; + } + + const uint64_t key = ResolutionKey(width, height); + auto existing = resources_.find(key); + if (existing != resources_.end()) + return &existing->second; + + CaptureResources resources; + HRESULT hr = device->CreateRenderTarget( + static_cast(width), + static_cast(height), + D3DFMT_A8R8G8B8, + D3DMULTISAMPLE_NONE, + 0, + FALSE, + &resources.render_target, + nullptr); + + if (FAILED(hr) || !resources.render_target) + { + LOG_ERROR("[ScreenCapture] CreateRenderTarget failed: 0x{:08X}", static_cast(hr)); + return nullptr; + } + + hr = device->CreateOffscreenPlainSurface( + static_cast(width), + static_cast(height), + D3DFMT_A8R8G8B8, + D3DPOOL_SYSTEMMEM, + &resources.system_surface, + nullptr); + + if (FAILED(hr) || !resources.system_surface) + { + LOG_ERROR("[ScreenCapture] CreateOffscreenPlainSurface failed: 0x{:08X}", static_cast(hr)); + resources.render_target->Release(); + return nullptr; + } + + return &resources_.emplace(key, resources).first->second; +} + +bool ScreenCaptureManager::CapturePixels( + IDirect3DDevice9* device, + int width, + int height, + std::vector& rgba) +{ + auto* resources = GetOrCreateResources(device, width, height); + if (!resources) + return false; + + IDirect3DSurface9* back_buffer = nullptr; + HRESULT hr = device->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &back_buffer); + if (FAILED(hr) || !back_buffer) + return false; + + hr = device->StretchRect(back_buffer, nullptr, resources->render_target, nullptr, D3DTEXF_LINEAR); + back_buffer->Release(); + if (FAILED(hr)) + return false; + + hr = device->GetRenderTargetData(resources->render_target, resources->system_surface); + if (FAILED(hr)) + return false; + + D3DLOCKED_RECT locked{}; + hr = resources->system_surface->LockRect(&locked, nullptr, D3DLOCK_READONLY); + if (FAILED(hr) || !locked.pBits) + return false; + + const size_t row_bytes = static_cast(width) * 4; + rgba.resize(row_bytes * static_cast(height)); + + const auto* source = static_cast(locked.pBits); + auto* destination = rgba.data(); + for (int y = 0; y < height; ++y) + { + const auto* source_row = source + static_cast(y) * locked.Pitch; + auto* destination_row = destination + static_cast(y) * row_bytes; + + for (int x = 0; x < width; ++x) + { + const size_t offset = static_cast(x) * 4; + destination_row[offset + 0] = source_row[offset + 2]; + destination_row[offset + 1] = source_row[offset + 1]; + destination_row[offset + 2] = source_row[offset + 0]; + destination_row[offset + 3] = 255; + } + } + + resources->system_surface->UnlockRect(); + return true; +} + +std::vector> ScreenCaptureManager::CaptureDueFrames( + IDirect3DDevice9* device, + uint64_t now_ms) +{ + std::vector due; + { + std::lock_guard lock(subscriptions_mutex_); + due.reserve(subscriptions_.size()); + + for (auto& [browser_id, subscription] : subscriptions_) + { + if (subscription.frame_pending || now_ms < subscription.next_capture_ms) + continue; + + subscription.frame_pending = true; + subscription.next_capture_ms = now_ms + std::max(1, 1000u / subscription.config.fps); + due.push_back(DueCapture{ + browser_id, + subscription.config, + subscription.generation, + ++subscription.sequence + }); + } + } + + using Resolution = std::pair; + std::map> grouped; + for (const auto& capture : due) + grouped[{capture.config.width, capture.config.height}].push_back(capture); + + std::vector> frames; + frames.reserve(due.size()); + + for (const auto& [resolution, captures] : grouped) + { + auto pixels = std::make_shared>(); + if (!CapturePixels(device, resolution.first, resolution.second, *pixels)) + { + for (const auto& capture : captures) + CompleteFrame(capture.browser_id, capture.generation); + continue; + } + + for (const auto& capture : captures) + { + auto frame = std::make_shared(); + frame->browser_id = capture.browser_id; + frame->width = resolution.first; + frame->height = resolution.second; + frame->sequence = capture.sequence; + frame->generation = capture.generation; + frame->timestamp_ms = static_cast(now_ms); + frame->rgba = pixels; + frames.push_back(std::move(frame)); + } + } + + return frames; +} + +void ScreenCaptureManager::ReleaseResources() +{ + for (auto& [key, resources] : resources_) + { + if (resources.system_surface) + resources.system_surface->Release(); + if (resources.render_target) + resources.render_target->Release(); + } + + resources_.clear(); + resource_device_ = nullptr; +} + +void ScreenCaptureManager::OnDeviceLost() +{ + ReleaseResources(); +} diff --git a/src/client/core/rendering/screen_capture.hpp b/src/client/core/rendering/screen_capture.hpp new file mode 100644 index 00000000..3380c603 --- /dev/null +++ b/src/client/core/rendering/screen_capture.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +struct CapturedScreenFrame +{ + int browser_id = -1; + int width = 0; + int height = 0; + uint32_t sequence = 0; + uint64_t generation = 0; + double timestamp_ms = 0.0; + std::shared_ptr> rgba; +}; + +class ScreenCaptureManager +{ +public: + struct Configuration + { + int width = 640; + int height = 360; + int fps = 15; + }; + + ScreenCaptureManager() = default; + ~ScreenCaptureManager(); + + ScreenCaptureManager(const ScreenCaptureManager&) = delete; + ScreenCaptureManager& operator=(const ScreenCaptureManager&) = delete; + + Configuration Start(int browser_id, int width, int height, int fps); + void Stop(int browser_id); + void StopAll(); + + bool HasSubscriptions() const; + bool IsCurrent(int browser_id, uint64_t generation) const; + void CompleteFrame(int browser_id, uint64_t generation); + + std::vector> CaptureDueFrames( + IDirect3DDevice9* device, + uint64_t now_ms); + + void OnDeviceLost(); + +private: + struct Subscription + { + Configuration config; + uint64_t generation = 0; + uint64_t next_capture_ms = 0; + uint32_t sequence = 0; + bool frame_pending = false; + }; + + struct DueCapture + { + int browser_id = -1; + Configuration config; + uint64_t generation = 0; + uint32_t sequence = 0; + }; + + struct CaptureResources + { + IDirect3DSurface9* render_target = nullptr; + IDirect3DSurface9* system_surface = nullptr; + }; + + static Configuration Sanitize(int width, int height, int fps); + static uint64_t ResolutionKey(int width, int height) noexcept; + + CaptureResources* GetOrCreateResources(IDirect3DDevice9* device, int width, int height); + bool CapturePixels( + IDirect3DDevice9* device, + int width, + int height, + std::vector& rgba); + void ReleaseResources(); + +private: + mutable std::mutex subscriptions_mutex_; + std::unordered_map subscriptions_; + uint64_t next_generation_ = 1; + + IDirect3DDevice9* resource_device_ = nullptr; + std::unordered_map resources_; +}; diff --git a/src/client/renderer/main.cpp b/src/client/renderer/main.cpp index c654e345..ca2ecfb6 100644 --- a/src/client/renderer/main.cpp +++ b/src/client/renderer/main.cpp @@ -18,6 +18,14 @@ static std::map> pending_events_; static bool g_chat_input_open = false; +struct ScreenCaptureCallback +{ + CefRefPtr context; + CefRefPtr function; +}; + +static std::map screen_capture_callbacks_; + static CefV8ValueList BuildJsArgs(const PendingArgs& pending) { CefV8ValueList jsArgs; @@ -108,7 +116,66 @@ class V8HandlerImpl : public CefV8Handler CefRefPtr& retval, CefString& exception) override { - if (name == "emit") + if (name == "screen_start") + { + if (arguments.empty() || !arguments[0]->IsFunction()) + { + exception = "Invalid arguments to cef.screen.start(callback[, width, height, fps])"; + return true; + } + + CefRefPtr context = CefV8Context::GetCurrentContext(); + CefRefPtr browser = context ? context->GetBrowser() : nullptr; + CefRefPtr frame = context ? context->GetFrame() : nullptr; + if (!context || !browser || !frame || !frame->IsMain()) + { + exception = "cef.screen.start() is only available in the main frame"; + return true; + } + + const int width = arguments.size() > 1 && arguments[1]->IsInt() + ? arguments[1]->GetIntValue() : 640; + const int height = arguments.size() > 2 && arguments[2]->IsInt() + ? arguments[2]->GetIntValue() : 360; + const int fps = arguments.size() > 3 && arguments[3]->IsInt() + ? arguments[3]->GetIntValue() : 15; + + screen_capture_callbacks_[browser->GetIdentifier()] = ScreenCaptureCallback{ + context, + arguments[0] + }; + + CefRefPtr message = CefProcessMessage::Create("cef_screen_capture_start"); + CefRefPtr list = message->GetArgumentList(); + list->SetInt(0, width); + list->SetInt(1, height); + list->SetInt(2, fps); + frame->SendProcessMessage(PID_BROWSER, message); + + retval = CefV8Value::CreateBool(true); + return true; + } + else if (name == "screen_stop") + { + CefRefPtr context = CefV8Context::GetCurrentContext(); + CefRefPtr browser = context ? context->GetBrowser() : nullptr; + CefRefPtr frame = context ? context->GetFrame() : nullptr; + if (!frame || !frame->IsMain()) + { + exception = "cef.screen.stop() is only available in the main frame"; + return true; + } + + if (browser) + screen_capture_callbacks_.erase(browser->GetIdentifier()); + + CefRefPtr message = CefProcessMessage::Create("cef_screen_capture_stop"); + frame->SendProcessMessage(PID_BROWSER, message); + + retval = CefV8Value::CreateBool(true); + return true; + } + else if (name == "emit") { if (arguments.size() < 1 || !arguments[0]->IsString()) { @@ -272,9 +339,41 @@ class RenderProcessHandler : public CefRenderProcessHandler cefObj->SetValue("isChatInputOpen", CefV8Value::CreateFunction("isChatInputOpen", handler), V8_PROPERTY_ATTRIBUTE_NONE); cefObj->SetValue("exitGame", CefV8Value::CreateFunction("exitGame", handler), V8_PROPERTY_ATTRIBUTE_NONE); + if (frame && frame->IsMain()) + { + CefRefPtr screenObj = CefV8Value::CreateObject(nullptr, nullptr); + screenObj->SetValue("start", CefV8Value::CreateFunction("screen_start", handler), V8_PROPERTY_ATTRIBUTE_NONE); + screenObj->SetValue("stop", CefV8Value::CreateFunction("screen_stop", handler), V8_PROPERTY_ATTRIBUTE_NONE); + cefObj->SetValue("screen", screenObj, V8_PROPERTY_ATTRIBUTE_READONLY); + } + global->SetValue("cef", cefObj, V8_PROPERTY_ATTRIBUTE_NONE); } + void OnContextReleased(CefRefPtr browser, + CefRefPtr frame, + CefRefPtr context) override + { + CEF_REQUIRE_RENDERER_THREAD(); + + if (!browser) + return; + + const auto it = screen_capture_callbacks_.find(browser->GetIdentifier()); + if (it == screen_capture_callbacks_.end() || + !it->second.context || !it->second.context->IsSame(context)) + { + return; + } + + screen_capture_callbacks_.erase(it); + if (frame) + { + CefRefPtr message = CefProcessMessage::Create("cef_screen_capture_stop"); + frame->SendProcessMessage(PID_BROWSER, message); + } + } + bool OnProcessMessageReceived(CefRefPtr browser, CefRefPtr frame, CefProcessId source_process, @@ -282,6 +381,46 @@ class RenderProcessHandler : public CefRenderProcessHandler { CEF_REQUIRE_RENDERER_THREAD(); + if (message->GetName() == "cef_screen_capture_frame") + { + const auto callback_it = screen_capture_callbacks_.find(browser->GetIdentifier()); + if (callback_it == screen_capture_callbacks_.end()) + return true; + + CefRefPtr args = message->GetArgumentList(); + if (args->GetSize() < 5 || + args->GetType(0) != VTYPE_BINARY || + args->GetType(1) != VTYPE_INT || + args->GetType(2) != VTYPE_INT) + { + return true; + } + + CefRefPtr binary = args->GetBinary(0); + auto& callback = callback_it->second; + if (!binary || !binary->IsValid() || + !callback.context || !callback.function || + !callback.context->Enter()) + { + return true; + } + + CefRefPtr pixels = CefV8Value::CreateArrayBufferWithCopy( + const_cast(binary->GetRawData()), binary->GetSize()); + CefRefPtr captured_frame = CefV8Value::CreateObject(nullptr, nullptr); + captured_frame->SetValue("data", pixels, V8_PROPERTY_ATTRIBUTE_READONLY); + captured_frame->SetValue("width", CefV8Value::CreateInt(args->GetInt(1)), V8_PROPERTY_ATTRIBUTE_READONLY); + captured_frame->SetValue("height", CefV8Value::CreateInt(args->GetInt(2)), V8_PROPERTY_ATTRIBUTE_READONLY); + captured_frame->SetValue("sequence", CefV8Value::CreateInt(args->GetInt(3)), V8_PROPERTY_ATTRIBUTE_READONLY); + captured_frame->SetValue("timestamp", CefV8Value::CreateDouble(args->GetDouble(4)), V8_PROPERTY_ATTRIBUTE_READONLY); + captured_frame->SetValue("format", CefV8Value::CreateString("RGBA"), V8_PROPERTY_ATTRIBUTE_READONLY); + + CefV8ValueList callback_args{ captured_frame }; + callback.function->ExecuteFunction(nullptr, callback_args); + callback.context->Exit(); + return true; + } + if (message->GetName() == "emit_event") { CefRefPtr args = message->GetArgumentList(); @@ -361,4 +500,4 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int) CefMainArgs args(hInstance); CefRefPtr app = new RendererApp(); return CefExecuteProcess(args, app, nullptr); -} \ No newline at end of file +} diff --git a/tests/README.md b/tests/README.md index 505ca922..4cb88127 100644 --- a/tests/README.md +++ b/tests/README.md @@ -30,6 +30,7 @@ tests/ world3d.html events.html input.html + camera.html red.html blue.html empty.html @@ -51,6 +52,7 @@ tests/ world3d.html events.html input.html + camera.html red.html blue.html empty.html @@ -219,6 +221,13 @@ This keeps the sample logic consistent across both platforms. /inputdestroy ``` +### Game screen capture + +```txt +/camera +/cameradestroy +``` + ### Stress test ```txt @@ -285,6 +294,11 @@ JavaScript -> CEF browser -> Pawn Demonstrates browser focus and keyboard input handling. +### Game screen capture + +Demonstrates a phone-like CEF interface showing the live GTA frame through +`cef.screen.start()` without capturing the CEF overlay itself. + ### Stress test Creates multiple browser modes at once and tests: diff --git a/tests/omp/scriptfiles/cef/cef_demo/camera.html b/tests/omp/scriptfiles/cef/cef_demo/camera.html new file mode 100644 index 00000000..5df84ca1 --- /dev/null +++ b/tests/omp/scriptfiles/cef/cef_demo/camera.html @@ -0,0 +1,66 @@ + + + + + omp-cef Camera + + + +
+
+
CameraStarting...
+ +
PHOTOVIDEO
+
+
+ + + diff --git a/tests/samp/scriptfiles/cef/cef_demo/camera.html b/tests/samp/scriptfiles/cef/cef_demo/camera.html new file mode 100644 index 00000000..5df84ca1 --- /dev/null +++ b/tests/samp/scriptfiles/cef/cef_demo/camera.html @@ -0,0 +1,66 @@ + + + + + omp-cef Camera + + + +
+
+
CameraStarting...
+ +
PHOTOVIDEO
+
+
+ + + diff --git a/tests/shared/cef_demo.inc b/tests/shared/cef_demo.inc index 62bce68a..a86bfb6a 100644 --- a/tests/shared/cef_demo.inc +++ b/tests/shared/cef_demo.inc @@ -26,6 +26,7 @@ #define CEF_DEMO_OVERLAY_ID 9101 #define CEF_DEMO_WORLD2D_ID 9102 #define CEF_DEMO_WORLD3D_ID 9103 +#define CEF_DEMO_CAMERA_ID 9104 #define CEF_DEMO_WORLD3D_OBJECT_MODEL 14772 #define CEF_DEMO_WORLD3D_TEXTURE_NAME "CJ_TV_SCREEN" @@ -37,6 +38,7 @@ #define CEF_DEMO_URL_WORLD3D "http://cef/cef_demo/world3d.html" #define CEF_DEMO_URL_EVENTS "http://cef/cef_demo/events.html" #define CEF_DEMO_URL_INPUT "http://cef/cef_demo/input.html" +#define CEF_DEMO_URL_CAMERA "http://cef/cef_demo/camera.html" #define CEF_DEMO_URL_RED "http://cef/cef_demo/red.html" #define CEF_DEMO_URL_BLUE "http://cef/cef_demo/blue.html" #define CEF_DEMO_URL_EMPTY "http://cef/cef_demo/empty.html" @@ -186,6 +188,7 @@ public OnPlayerCommandText(playerid, cmdtext[]) CEF_EnableDevTools(playerid, CEF_DEMO_OVERLAY_ID, true); CEF_EnableDevTools(playerid, CEF_DEMO_WORLD2D_ID, true); CEF_EnableDevTools(playerid, CEF_DEMO_WORLD3D_ID, true); + CEF_EnableDevTools(playerid, CEF_DEMO_CAMERA_ID, true); SendClientMessage(playerid, COLOR_OK, "[CEF Demo] DevTools enabled for demo browsers."); return 1; } @@ -295,6 +298,28 @@ public OnPlayerCommandText(playerid, cmdtext[]) return 1; } + if (!strcmp(cmdtext, "/camera", true)) + { + CEF_CreateBrowser( + playerid, + CEF_DEMO_CAMERA_ID, + CEF_DEMO_URL_CAMERA, + false, + false, + 0.0, + 0.0 + ); + SendClientMessage(playerid, COLOR_OK, "[CEF Demo] Game camera overlay created."); + return 1; + } + + if (!strcmp(cmdtext, "/cameradestroy", true)) + { + CEF_DestroyBrowser(playerid, CEF_DEMO_CAMERA_ID); + SendClientMessage(playerid, COLOR_OK, "[CEF Demo] Game camera overlay destroyed."); + return 1; + } + // World2D if (!strcmp(cmdtext, "/world2d", true)) { @@ -718,6 +743,7 @@ static CefDemo_SendHelp(playerid) SendClientMessage(playerid, COLOR_INFO, "[CEF Demo] Main: /cefhelp /cefstatus /cefdevtools /jsnotify /jsstats"); SendClientMessage(playerid, COLOR_INFO, "[CEF Demo] Overlay2D: /overlay /overlayhide /overlayshow /overlayred /overlayblue /overlayempty /overlayanim"); SendClientMessage(playerid, COLOR_INFO, "[CEF Demo] Overlay2D: /overlayevents /overlayinput /overlayreload /overlaydestroy /overlayrecreate"); + SendClientMessage(playerid, COLOR_INFO, "[CEF Demo] Screen capture: /camera /cameradestroy"); SendClientMessage(playerid, COLOR_INFO, "[CEF Demo] World2D: /world2d /world2dmove /world2dhide /world2dshow /world2dred /world2dblue /world2dempty /world2danim"); SendClientMessage(playerid, COLOR_INFO, "[CEF Demo] World3D: /world3d /world3dmove /world3dattach /world3ddetach /world3dhide /world3dshow /world3dred /world3dblue /world3dempty /world3danim"); SendClientMessage(playerid, COLOR_INFO, "[CEF Demo] Stress: /cefstress /cefstressred /cefstressblue /cefstressempty /cefstressanim /cefstresshide /cefstressshow /cefstressdestroy"); @@ -886,6 +912,7 @@ static CefDemo_DestroyAll(playerid) { CEF_DestroyBrowser(playerid, CEF_DEMO_OVERLAY_ID); CEF_DestroyBrowser(playerid, CEF_DEMO_WORLD2D_ID); + CEF_DestroyBrowser(playerid, CEF_DEMO_CAMERA_ID); CefDemo_DestroyWorld3D(playerid); SendClientMessage(playerid, COLOR_OK, "[CEF Demo] Stress setup destroyed."); return 1; @@ -905,4 +932,4 @@ static CefDemo_SetAllVisible(playerid, bool:visible) CEF_SetBrowserVisible(playerid, CEF_DEMO_WORLD2D_ID, visible); CEF_SetBrowserVisible(playerid, CEF_DEMO_WORLD3D_ID, visible); return 1; -} \ No newline at end of file +} diff --git a/tests/webviews/react/src/vite-env.d.ts b/tests/webviews/react/src/vite-env.d.ts index 0a893daf..62e693ac 100644 --- a/tests/webviews/react/src/vite-env.d.ts +++ b/tests/webviews/react/src/vite-env.d.ts @@ -1,9 +1,27 @@ /// +interface CefScreenFrame { + data: ArrayBuffer; + width: number; + height: number; + sequence: number; + timestamp: number; + format: 'RGBA'; +} + interface CefAPI { emit: (event: string, ...args: any[]) => void; on: (event: string, callback: (...args: any[]) => void) => void; off: (event: string, callback: (...args: any[]) => void) => void; + screen: { + start: ( + callback: (frame: CefScreenFrame) => void, + width?: number, + height?: number, + fps?: number + ) => boolean; + stop: () => boolean; + }; } interface Window { From e33c892cf1f288a8e8735dda8c294b30649611bf Mon Sep 17 00:00:00 2001 From: xBruno1000x Date: Mon, 10 Aug 2026 20:10:55 -0300 Subject: [PATCH 2/2] :wrench: chore: Adiciona suporte a sampctl --- pawn.json | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 pawn.json diff --git a/pawn.json b/pawn.json new file mode 100644 index 00000000..aa97b9bc --- /dev/null +++ b/pawn.json @@ -0,0 +1,25 @@ +{ + "user": "aurora-mp", + "repo": "omp-cef", + "website": "https://github.com/aurora-mp/omp-cef", + "preset": "openmp", + "include_path": "src/server", + "resources": [ + { + "name": "^cef-omp-v(.*)-x86\\.zip$", + "platform": "windows", + "archive": true, + "plugins": [ + "components/Cef.dll" + ] + }, + { + "name": "^cef-omp-v(.*)-x86\\.zip$", + "platform": "linux", + "archive": true, + "plugins": [ + "components/Cef.so" + ] + } + ] +}