diff --git a/playback_lab/entry/src/main/cpp/CMakeLists.txt b/playback_lab/entry/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000000..9399981fbd --- /dev/null +++ b/playback_lab/entry/src/main/cpp/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.5.0) +project(playback_lab) + +set(REPO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../") +set(MPV_INCLUDE_DIR "${REPO_ROOT}/build/ohos-arm64/prefix/include") +set(MPV_LIBRARY "${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/arm64-v8a/libmpv.so.2") + +if(NOT EXISTS "${MPV_LIBRARY}") + message(FATAL_ERROR "Missing ${MPV_LIBRARY}; run scripts/stage-mpv.sh first") +endif() + +include_directories(${MPV_INCLUDE_DIR}) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}) + +add_library(mpv SHARED IMPORTED) +set_target_properties(mpv PROPERTIES IMPORTED_LOCATION "${MPV_LIBRARY}") + +add_library(entry SHARED + bridge.cpp + player.cpp + render.cpp +) + +target_compile_features(entry PRIVATE cxx_std_17) +target_compile_options(entry PRIVATE -fvisibility=hidden -Wall -Wextra -Werror=return-type) +target_link_libraries(entry PUBLIC + mpv + libace_napi.z.so + libace_ndk.z.so + libEGL.so + libGLESv3.so + libnative_window.so + libhilog_ndk.z.so +) diff --git a/playback_lab/entry/src/main/cpp/bridge.cpp b/playback_lab/entry/src/main/cpp/bridge.cpp new file mode 100644 index 0000000000..64fcbfec34 --- /dev/null +++ b/playback_lab/entry/src/main/cpp/bridge.cpp @@ -0,0 +1,164 @@ +#include "player.h" +#include "render.h" + +#include +#include +#include + +#include + +#undef LOG_DOMAIN +#undef LOG_TAG +#define LOG_DOMAIN 0xD001 +#define LOG_TAG "VelixBridge" + +namespace { +PlaybackEngine gPlayer; +EglRenderer gRenderer(gPlayer); + +void onSurfaceCreated(OH_NativeXComponent* component, void* window) +{ + gRenderer.onSurfaceCreated(component, window); +} + +void onSurfaceChanged(OH_NativeXComponent* component, void* window) +{ + gRenderer.onSurfaceChanged(component, window); +} + +void onSurfaceDestroyed(OH_NativeXComponent*, void* window) +{ + gRenderer.onSurfaceDestroyed(window); +} + +void onTouch(OH_NativeXComponent*, void*) {} + +bool getStringArg(napi_env env, napi_callback_info info, std::string& value) +{ + size_t argc = 1; + napi_value argv[1] = {nullptr}; + if (napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr) != napi_ok || argc < 1) return false; + size_t size = 0; + if (napi_get_value_string_utf8(env, argv[0], nullptr, 0, &size) != napi_ok) return false; + value.resize(size + 1); + if (napi_get_value_string_utf8(env, argv[0], value.data(), value.size(), &size) != napi_ok) return false; + value.resize(size); + return true; +} + +bool getDoubleArg(napi_env env, napi_callback_info info, double& value) +{ + size_t argc = 1; + napi_value argv[1] = {nullptr}; + if (napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr) != napi_ok || argc < 1) return false; + return napi_get_value_double(env, argv[0], &value) == napi_ok; +} + +napi_value undefined(napi_env env) +{ + napi_value result = nullptr; + napi_get_undefined(env, &result); + return result; +} + +napi_value load(napi_env env, napi_callback_info info) +{ + std::string url; + if (getStringArg(env, info, url)) gPlayer.load(url); + return undefined(env); +} + +napi_value play(napi_env env, napi_callback_info) +{ + gPlayer.play(); + return undefined(env); +} + +napi_value pause(napi_env env, napi_callback_info) +{ + gPlayer.pause(); + return undefined(env); +} + +napi_value seek(napi_env env, napi_callback_info info) +{ + double delta = 0; + if (getDoubleArg(env, info, delta)) gPlayer.seek(delta); + return undefined(env); +} + +napi_value stop(napi_env env, napi_callback_info) +{ + gPlayer.stop(); + return undefined(env); +} + +napi_value release(napi_env env, napi_callback_info) +{ + gRenderer.onSurfaceDestroyed(nullptr); + gPlayer.release(); + return undefined(env); +} + +napi_value getState(napi_env env, napi_callback_info) +{ + PlaybackSnapshot state = gPlayer.snapshot(); + napi_value object = nullptr; + napi_create_object(env, &object); + napi_value value = nullptr; + napi_create_string_utf8(env, state.state.c_str(), NAPI_AUTO_LENGTH, &value); + napi_set_named_property(env, object, "state", value); + napi_create_double(env, state.position, &value); + napi_set_named_property(env, object, "position", value); + napi_create_double(env, state.duration, &value); + napi_set_named_property(env, object, "duration", value); + napi_create_double(env, state.fps, &value); + napi_set_named_property(env, object, "fps", value); + napi_create_int64(env, state.dropped, &value); + napi_set_named_property(env, object, "dropped", value); + napi_create_int64(env, state.firstFrameMs, &value); + napi_set_named_property(env, object, "firstFrameMs", value); + // Render count is a diagnostic value consumed by ArkTS as a number. + // Returning a BigInt makes the inspector unable to serialize the state. + napi_create_double(env, static_cast(state.renderCount), &value); + napi_set_named_property(env, object, "renderCount", value); + napi_create_string_utf8(env, state.lastError.c_str(), NAPI_AUTO_LENGTH, &value); + napi_set_named_property(env, object, "lastError", value); + napi_create_string_utf8(env, gRenderer.surfaceStatus(), NAPI_AUTO_LENGTH, &value); + napi_set_named_property(env, object, "surface", value); + napi_create_string_utf8(env, gRenderer.eglStatus(), NAPI_AUTO_LENGTH, &value); + napi_set_named_property(env, object, "egl", value); + napi_create_string_utf8(env, gRenderer.mpvStatus(), NAPI_AUTO_LENGTH, &value); + napi_set_named_property(env, object, "mpv", value); + return object; +} + +napi_value Init(napi_env env, napi_value exports) +{ + gPlayer.initialize(); + napi_value xcomponent = nullptr; + if (napi_get_named_property(env, exports, OH_NATIVE_XCOMPONENT_OBJ, &xcomponent) == napi_ok) { + OH_NativeXComponent* component = nullptr; + if (napi_unwrap(env, xcomponent, reinterpret_cast(&component)) == napi_ok && component != nullptr) { + static OH_NativeXComponent_Callback callbacks = { + onSurfaceCreated, onSurfaceChanged, onSurfaceDestroyed, onTouch + }; + OH_NativeXComponent_RegisterCallback(component, &callbacks); + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG, "XComponent callback registered"); + } + } + napi_property_descriptor properties[] = { + {"load", nullptr, load, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"play", nullptr, play, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"pause", nullptr, pause, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"seek", nullptr, seek, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"stop", nullptr, stop, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"release", nullptr, release, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"getState", nullptr, getState, nullptr, nullptr, nullptr, napi_default, nullptr}, + }; + napi_define_properties(env, exports, sizeof(properties) / sizeof(properties[0]), properties); + return exports; +} +} + +NAPI_MODULE(entry, Init) diff --git a/playback_lab/entry/src/main/cpp/player.cpp b/playback_lab/entry/src/main/cpp/player.cpp new file mode 100644 index 0000000000..5bd6a52964 --- /dev/null +++ b/playback_lab/entry/src/main/cpp/player.cpp @@ -0,0 +1,298 @@ +#include "player.h" + +#include + +#include +#include +#include + +#undef LOG_DOMAIN +#undef LOG_TAG +#define LOG_DOMAIN 0xD001 +#define LOG_TAG "VelixPlayback" + +namespace { +constexpr uint64_t kTimePos = 1; +constexpr uint64_t kDuration = 2; +constexpr uint64_t kPause = 3; +constexpr uint64_t kFps = 4; +constexpr uint64_t kDropped = 5; + +void logMpvError(const char* operation, int code) +{ + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "%{public}s failed: %{public}s (%{public}d)", + operation, mpv_error_string(code), code); +} +} + +PlaybackEngine::PlaybackEngine() = default; + +PlaybackEngine::~PlaybackEngine() +{ + release(); +} + +bool PlaybackEngine::initialize() +{ + if (initialized_.load()) { + return true; + } + handle_ = mpv_create(); + if (handle_ == nullptr) { + setError("mpv_create failed"); + return false; + } + const char* options[][2] = { + {"vo", "libmpv"}, + {"terminal", "no"}, + {"config", "no"}, + {"hwdec", "no"}, + {"audio", "no"}, + {"video-timing-offset", "0"}, + {nullptr, nullptr}, + }; + for (size_t i = 0; options[i][0] != nullptr; ++i) { + int code = mpv_set_option_string(handle_, options[i][0], options[i][1]); + if (code < 0) { + logMpvError(options[i][0], code); + setError(std::string("mpv option failed: ") + options[i][0]); + mpv_terminate_destroy(handle_); + handle_ = nullptr; + return false; + } + } + int code = mpv_initialize(handle_); + if (code < 0) { + logMpvError("mpv_initialize", code); + setError("mpv_initialize failed"); + mpv_terminate_destroy(handle_); + handle_ = nullptr; + return false; + } + observeProperties(); + initialized_.store(true); + stopEvents_.store(false); + startedAt_ = std::chrono::steady_clock::now(); + setState("PLAYER_CREATED"); + eventThread_ = std::thread(&PlaybackEngine::eventLoop, this); + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG, "libmpv initialized"); + return true; +} + +void PlaybackEngine::observeProperties() +{ + mpv_observe_property(handle_, kTimePos, "time-pos", MPV_FORMAT_DOUBLE); + mpv_observe_property(handle_, kDuration, "duration", MPV_FORMAT_DOUBLE); + mpv_observe_property(handle_, kPause, "pause", MPV_FORMAT_FLAG); + mpv_observe_property(handle_, kFps, "estimated-vf-fps", MPV_FORMAT_DOUBLE); + mpv_observe_property(handle_, kDropped, "vo-drop-frame-count", MPV_FORMAT_INT64); +} + +void PlaybackEngine::load(const std::string& url) +{ + if (!initialize() || url.empty()) { + setError(url.empty() ? "empty URL" : "player unavailable"); + return; + } + { + std::lock_guard lock(mutex_); + snapshot_.position = 0.0; + snapshot_.duration = 0.0; + snapshot_.firstFrameMs = -1; + snapshot_.renderCount = 0; + snapshot_.lastError.clear(); + startedAt_ = std::chrono::steady_clock::now(); + } + const char* command[] = {"loadfile", url.c_str(), "replace", nullptr}; + int code = mpv_command(handle_, command); + if (code < 0) { + logMpvError("loadfile", code); + setError(std::string("load failed: ") + mpv_error_string(code)); + return; + } + setState("PREPARING"); +} + +void PlaybackEngine::play() +{ + if (handle_ == nullptr) { + setError("player unavailable"); + return; + } + int code = mpv_set_property_string(handle_, "pause", "no"); + if (code < 0) { + logMpvError("play", code); + setError("play failed"); + return; + } + setState("PLAYING"); +} + +void PlaybackEngine::pause() +{ + if (handle_ == nullptr) { + return; + } + int code = mpv_set_property_string(handle_, "pause", "yes"); + if (code < 0) { + logMpvError("pause", code); + setError("pause failed"); + return; + } + setState("PAUSED"); +} + +void PlaybackEngine::seek(double delta) +{ + if (handle_ == nullptr) { + return; + } + if (!std::isfinite(delta)) { + setError("seek delta is not finite"); + return; + } + double target = 0.0; + double duration = 0.0; + { + std::lock_guard lock(mutex_); + target = snapshot_.position + delta; + duration = snapshot_.duration; + } + if (duration > 0.0) target = std::clamp(target, 0.0, duration); + int code = mpv_set_property(handle_, "time-pos", MPV_FORMAT_DOUBLE, &target); + if (code < 0) { + logMpvError("seek", code); + setError("seek failed"); + return; + } + std::lock_guard lock(mutex_); + snapshot_.lastError.clear(); +} + +void PlaybackEngine::stop() +{ + if (handle_ == nullptr) { + return; + } + int code = mpv_command_string(handle_, "stop"); + if (code < 0) { + logMpvError("stop", code); + setError("stop failed"); + return; + } + setState("STOPPED"); +} + +void PlaybackEngine::release() +{ + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG, "release called initialized=%{public}d", initialized_.load()); + if (!initialized_.exchange(false)) { + return; + } + stopEvents_.store(true); + if (handle_ != nullptr) { + mpv_wakeup(handle_); + } + if (eventThread_.joinable()) { + eventThread_.join(); + } + if (handle_ != nullptr) { + mpv_terminate_destroy(handle_); + handle_ = nullptr; + } + setState("RELEASED"); +} + +void PlaybackEngine::markFrameRendered() +{ + std::lock_guard lock(mutex_); + ++snapshot_.renderCount; + if (snapshot_.firstFrameMs < 0) { + auto elapsed = std::chrono::steady_clock::now() - startedAt_; + snapshot_.firstFrameMs = std::chrono::duration_cast(elapsed).count(); + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG, + "first frame: %{public}lld ms", static_cast(snapshot_.firstFrameMs)); + } +} + +PlaybackSnapshot PlaybackEngine::snapshot() const +{ + std::lock_guard lock(mutex_); + return snapshot_; +} + +void PlaybackEngine::setState(const char* state) +{ + std::lock_guard lock(mutex_); + snapshot_.state = state; +} + +void PlaybackEngine::setError(const std::string& error) +{ + std::lock_guard lock(mutex_); + snapshot_.state = "ERROR"; + snapshot_.lastError = error; + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "%{public}s", error.c_str()); +} + +void PlaybackEngine::updateProperty(uint64_t userdata, const mpv_event_property* property) +{ + if (property == nullptr || property->data == nullptr) { + return; + } + std::lock_guard lock(mutex_); + switch (userdata) { + case kTimePos: + if (property->format == MPV_FORMAT_DOUBLE) snapshot_.position = *static_cast(property->data); + break; + case kDuration: + if (property->format == MPV_FORMAT_DOUBLE) snapshot_.duration = *static_cast(property->data); + break; + case kPause: + if (property->format == MPV_FORMAT_FLAG && *static_cast(property->data) != 0 && + snapshot_.state == "PLAYING") snapshot_.state = "PAUSED"; + break; + case kFps: + if (property->format == MPV_FORMAT_DOUBLE) snapshot_.fps = *static_cast(property->data); + break; + case kDropped: + if (property->format == MPV_FORMAT_INT64) snapshot_.dropped = *static_cast(property->data); + break; + default: + break; + } +} + +void PlaybackEngine::eventLoop() +{ + while (!stopEvents_.load() && handle_ != nullptr) { + mpv_event* event = mpv_wait_event(handle_, 0.02); + if (event == nullptr) { + continue; + } + switch (event->event_id) { + case MPV_EVENT_PROPERTY_CHANGE: + updateProperty(event->reply_userdata, static_cast(event->data)); + break; + case MPV_EVENT_START_FILE: + setState("PREPARING"); + break; + case MPV_EVENT_FILE_LOADED: + setState("READY"); + break; + case MPV_EVENT_END_FILE: { + auto* end = static_cast(event->data); + if (end != nullptr && end->reason == MPV_END_FILE_REASON_ERROR) { + setError(std::string("end-file: ") + mpv_error_string(end->error)); + } else { + setState("STOPPED"); + } + break; + } + case MPV_EVENT_SHUTDOWN: + return; + default: + break; + } + } +} diff --git a/playback_lab/entry/src/main/cpp/player.h b/playback_lab/entry/src/main/cpp/player.h new file mode 100644 index 0000000000..71cb384aa4 --- /dev/null +++ b/playback_lab/entry/src/main/cpp/player.h @@ -0,0 +1,54 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +struct PlaybackSnapshot { + std::string state = "IDLE"; + double position = 0.0; + double duration = 0.0; + double fps = 0.0; + int64_t dropped = 0; + int64_t firstFrameMs = -1; + uint64_t renderCount = 0; + std::string lastError; +}; + +class PlaybackEngine { +public: + PlaybackEngine(); + ~PlaybackEngine(); + + bool initialize(); + void load(const std::string& url); + void play(); + void pause(); + void seek(double delta); + void stop(); + void release(); + PlaybackSnapshot snapshot() const; + + mpv_handle* handle() const { return handle_; } + void markFrameRendered(); + +private: + void eventLoop(); + void observeProperties(); + void setState(const char* state); + void setError(const std::string& error); + void updateProperty(uint64_t userdata, const mpv_event_property* property); + + mutable std::mutex mutex_; + PlaybackSnapshot snapshot_; + mpv_handle* handle_ = nullptr; + std::thread eventThread_; + std::atomic stopEvents_{false}; + std::atomic initialized_{false}; + std::chrono::steady_clock::time_point startedAt_; +}; diff --git a/playback_lab/entry/src/main/cpp/render.cpp b/playback_lab/entry/src/main/cpp/render.cpp new file mode 100644 index 0000000000..bad7fc17e3 --- /dev/null +++ b/playback_lab/entry/src/main/cpp/render.cpp @@ -0,0 +1,251 @@ +#include "render.h" + +#include +#include +#include +#include + +#include + +#undef LOG_DOMAIN +#undef LOG_TAG +#define LOG_DOMAIN 0xD001 +#define LOG_TAG "VelixRender" + +EglRenderer::EglRenderer(PlaybackEngine& player) : player_(player) {} + +EglRenderer::~EglRenderer() +{ + onSurfaceDestroyed(nullptr); +} + +const char* EglRenderer::surfaceStatus() const +{ + switch (surfaceStatus_.load()) { + case 1: return "READY"; + case 2: return "ERROR"; + case 3: return "RELEASED"; + default: return "WAITING"; + } +} + +const char* EglRenderer::eglStatus() const +{ + switch (eglStatus_.load()) { + case 1: return "READY"; + case 2: return "ERROR"; + case 3: return "RELEASED"; + default: return "WAITING"; + } +} + +const char* EglRenderer::mpvStatus() const +{ + switch (mpvStatus_.load()) { + case 1: return "READY"; + case 2: return "ERROR"; + case 3: return "RELEASED"; + default: return "WAITING"; + } +} + +void EglRenderer::onSurfaceCreated(OH_NativeXComponent* component, void* surface) +{ + onSurfaceDestroyed(nullptr); + OHNativeWindow* window = static_cast(surface); + if (window == nullptr) { + surfaceStatus_.store(2); + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "OHNativeWindow creation failed"); + return; + } + if (OH_NativeWindow_NativeObjectReference(window) != 0) { + surfaceStatus_.store(2); + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "OHNativeWindow reference failed"); + return; + } + uint64_t width = 1; + uint64_t height = 1; + if (component != nullptr) { + OH_NativeXComponent_GetXComponentSize(component, surface, &width, &height); + } + { + std::lock_guard lock(mutex_); + window_ = window; + width_ = width > 0 ? static_cast(width) : 1; + height_ = height > 0 ? static_cast(height) : 1; + stop_.store(false); + surfaceReady_.store(true); + redraw_ = true; + } + surfaceStatus_.store(1); + eglStatus_.store(0); + mpvStatus_.store(0); + thread_ = std::thread(&EglRenderer::renderLoop, this); + wake_.notify_one(); +} + +void EglRenderer::onSurfaceChanged(OH_NativeXComponent* component, void* surface) +{ + uint64_t width = 1; + uint64_t height = 1; + if (component != nullptr) { + OH_NativeXComponent_GetXComponentSize(component, surface, &width, &height); + } + { + std::lock_guard lock(mutex_); + width_ = width > 0 ? static_cast(width) : width_; + height_ = height > 0 ? static_cast(height) : height_; + redraw_ = true; + } + wake_.notify_one(); +} + +void EglRenderer::onSurfaceDestroyed(void*) +{ + surfaceReady_.store(false); + stop_.store(true); + wake_.notify_one(); + if (thread_.joinable()) { + thread_.join(); + } + std::lock_guard lock(mutex_); + if (window_ != nullptr) { + OH_NativeWindow_NativeObjectUnreference(window_); + window_ = nullptr; + } + surfaceStatus_.store(3); + eglStatus_.store(3); + mpvStatus_.store(3); +} + +void EglRenderer::requestRender() +{ + { + std::lock_guard lock(mutex_); + redraw_ = true; + } + wake_.notify_one(); +} + +bool EglRenderer::initializeEgl(OHNativeWindow* window) +{ + display_ = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (display_ == EGL_NO_DISPLAY || eglInitialize(display_, nullptr, nullptr) != EGL_TRUE) { + eglStatus_.store(2); + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "EGL initialize failed: 0x%{public}x", eglGetError()); + return false; + } + const EGLint configAttrs[] = { + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, + EGL_NONE + }; + EGLConfig config = nullptr; + EGLint count = 0; + if (eglChooseConfig(display_, configAttrs, &config, 1, &count) != EGL_TRUE || count == 0) { + eglStatus_.store(2); + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "EGL config selection failed: 0x%{public}x", eglGetError()); + destroyEgl(); + return false; + } + const EGLint contextAttrs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE}; + context_ = eglCreateContext(display_, config, EGL_NO_CONTEXT, contextAttrs); + surface_ = eglCreateWindowSurface(display_, config, reinterpret_cast(window), nullptr); + if (context_ == EGL_NO_CONTEXT || surface_ == EGL_NO_SURFACE || + eglMakeCurrent(display_, surface_, surface_, context_) != EGL_TRUE) { + eglStatus_.store(2); + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "EGL context/surface failed: 0x%{public}x", eglGetError()); + destroyEgl(); + return false; + } + eglSwapInterval(display_, 1); + glViewport(0, 0, width_, height_); + eglStatus_.store(1); + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG, "EGL vendor=%{public}s version=%{public}s renderer=%{public}s", + eglQueryString(display_, EGL_VENDOR), eglQueryString(display_, EGL_VERSION), + reinterpret_cast(glGetString(GL_RENDERER))); + return true; +} + +void EglRenderer::destroyEgl() +{ + if (display_ != EGL_NO_DISPLAY) { + eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + if (surface_ != EGL_NO_SURFACE) eglDestroySurface(display_, surface_); + if (context_ != EGL_NO_CONTEXT) eglDestroyContext(display_, context_); + eglTerminate(display_); + } + display_ = EGL_NO_DISPLAY; + context_ = EGL_NO_CONTEXT; + surface_ = EGL_NO_SURFACE; +} + +void* EglRenderer::getProcAddress(void*, const char* name) +{ + return reinterpret_cast(eglGetProcAddress(name)); +} + +void EglRenderer::onMpvUpdate(void* ctx) +{ + if (ctx != nullptr) static_cast(ctx)->requestRender(); +} + +void EglRenderer::renderLoop() +{ + OHNativeWindow* window = nullptr; + { + std::lock_guard lock(mutex_); + window = window_; + } + if (window == nullptr || !initializeEgl(window)) { + mpvStatus_.store(2); + return; + } + const char* api = MPV_RENDER_API_TYPE_OPENGL; + mpv_opengl_init_params glParams = {getProcAddress, this}; + mpv_render_param createParams[] = { + {MPV_RENDER_PARAM_API_TYPE, const_cast(api)}, + {MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &glParams}, + {MPV_RENDER_PARAM_INVALID, nullptr}, + }; + int code = mpv_render_context_create(&mpvRender_, player_.handle(), createParams); + if (code < 0) { + mpvStatus_.store(2); + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "mpv render context failed: %{public}s", mpv_error_string(code)); + destroyEgl(); + return; + } + mpvStatus_.store(1); + mpv_render_context_set_update_callback(mpvRender_, onMpvUpdate, this); + while (!stop_.load()) { + int width = 1; + int height = 1; + { + std::unique_lock lock(mutex_); + wake_.wait_for(lock, std::chrono::milliseconds(16), [this] { return redraw_ || stop_.load(); }); + redraw_ = false; + width = width_; + height = height_; + } + if (stop_.load()) break; + uint64_t flags = mpv_render_context_update(mpvRender_); + if ((flags & MPV_RENDER_UPDATE_FRAME) == 0) continue; + mpv_opengl_fbo fbo = {0, width, height, GL_RGBA8}; + int flip = 1; + mpv_render_param renderParams[] = { + {MPV_RENDER_PARAM_OPENGL_FBO, &fbo}, + {MPV_RENDER_PARAM_FLIP_Y, &flip}, + {MPV_RENDER_PARAM_INVALID, nullptr}, + }; + if (mpv_render_context_render(mpvRender_, renderParams) >= 0) { + eglSwapBuffers(display_, surface_); + mpv_render_context_report_swap(mpvRender_); + player_.markFrameRendered(); + } + } + mpv_render_context_set_update_callback(mpvRender_, nullptr, nullptr); + mpv_render_context_free(mpvRender_); + mpvRender_ = nullptr; + destroyEgl(); +} diff --git a/playback_lab/entry/src/main/cpp/render.h b/playback_lab/entry/src/main/cpp/render.h new file mode 100644 index 0000000000..b9f36e7f82 --- /dev/null +++ b/playback_lab/entry/src/main/cpp/render.h @@ -0,0 +1,53 @@ +#pragma once + +#include "player.h" +#include + +#include +#include +#include + +#include +#include +#include +#include + +class EglRenderer { +public: + explicit EglRenderer(PlaybackEngine& player); + ~EglRenderer(); + + void onSurfaceCreated(OH_NativeXComponent* component, void* surface); + void onSurfaceChanged(OH_NativeXComponent* component, void* surface); + void onSurfaceDestroyed(void* surface); + void requestRender(); + bool hasSurface() const { return surfaceReady_.load(); } + const char* surfaceStatus() const; + const char* eglStatus() const; + const char* mpvStatus() const; + +private: + void renderLoop(); + bool initializeEgl(OHNativeWindow* window); + void destroyEgl(); + static void onMpvUpdate(void* ctx); + static void* getProcAddress(void* ctx, const char* name); + + PlaybackEngine& player_; + std::thread thread_; + mutable std::mutex mutex_; + std::condition_variable wake_; + std::atomic stop_{false}; + std::atomic surfaceReady_{false}; + std::atomic surfaceStatus_{0}; + std::atomic eglStatus_{0}; + std::atomic mpvStatus_{0}; + bool redraw_ = false; + OHNativeWindow* window_ = nullptr; + int width_ = 1; + int height_ = 1; + EGLDisplay display_ = EGL_NO_DISPLAY; + EGLContext context_ = EGL_NO_CONTEXT; + EGLSurface surface_ = EGL_NO_SURFACE; + mpv_render_context* mpvRender_ = nullptr; +};