feat(playback-lab): add EGL and libmpv playback bridge
This commit is contained in:
1 parent
2356e2f67b
commit
d4c84601fa
6 files changed
+854
No files matched your search
@@ -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
|
||||
)
|
||||
@@ -0,0 +1,164 @@
|
||||
#include "player.h"
|
||||
#include "render.h"
|
||||
|
||||
#include <ace/xcomponent/native_interface_xcomponent.h>
|
||||
#include <hilog/log.h>
|
||||
#include <napi/native_api.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#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<double>(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<void**>(&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)
|
||||
@@ -0,0 +1,298 @@
|
||||
#include "player.h"
|
||||
|
||||
#include <hilog/log.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
|
||||
#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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> lock(mutex_);
|
||||
++snapshot_.renderCount;
|
||||
if (snapshot_.firstFrameMs < 0) {
|
||||
auto elapsed = std::chrono::steady_clock::now() - startedAt_;
|
||||
snapshot_.firstFrameMs = std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count();
|
||||
OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG,
|
||||
"first frame: %{public}lld ms", static_cast<long long>(snapshot_.firstFrameMs));
|
||||
}
|
||||
}
|
||||
|
||||
PlaybackSnapshot PlaybackEngine::snapshot() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return snapshot_;
|
||||
}
|
||||
|
||||
void PlaybackEngine::setState(const char* state)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
snapshot_.state = state;
|
||||
}
|
||||
|
||||
void PlaybackEngine::setError(const std::string& error)
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<std::mutex> lock(mutex_);
|
||||
switch (userdata) {
|
||||
case kTimePos:
|
||||
if (property->format == MPV_FORMAT_DOUBLE) snapshot_.position = *static_cast<double*>(property->data);
|
||||
break;
|
||||
case kDuration:
|
||||
if (property->format == MPV_FORMAT_DOUBLE) snapshot_.duration = *static_cast<double*>(property->data);
|
||||
break;
|
||||
case kPause:
|
||||
if (property->format == MPV_FORMAT_FLAG && *static_cast<int*>(property->data) != 0 &&
|
||||
snapshot_.state == "PLAYING") snapshot_.state = "PAUSED";
|
||||
break;
|
||||
case kFps:
|
||||
if (property->format == MPV_FORMAT_DOUBLE) snapshot_.fps = *static_cast<double*>(property->data);
|
||||
break;
|
||||
case kDropped:
|
||||
if (property->format == MPV_FORMAT_INT64) snapshot_.dropped = *static_cast<int64_t*>(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<mpv_event_property*>(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<mpv_event_end_file*>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <mpv/client.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
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<bool> stopEvents_{false};
|
||||
std::atomic<bool> initialized_{false};
|
||||
std::chrono::steady_clock::time_point startedAt_;
|
||||
};
|
||||
@@ -0,0 +1,251 @@
|
||||
#include "render.h"
|
||||
|
||||
#include <GLES3/gl3.h>
|
||||
#include <EGL/egl.h>
|
||||
#include <hilog/log.h>
|
||||
#include <mpv/render_gl.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#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<OHNativeWindow*>(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<std::mutex> lock(mutex_);
|
||||
window_ = window;
|
||||
width_ = width > 0 ? static_cast<int>(width) : 1;
|
||||
height_ = height > 0 ? static_cast<int>(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<std::mutex> lock(mutex_);
|
||||
width_ = width > 0 ? static_cast<int>(width) : width_;
|
||||
height_ = height > 0 ? static_cast<int>(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<std::mutex> 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<std::mutex> 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<EGLNativeWindowType>(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<const char*>(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<void*>(eglGetProcAddress(name));
|
||||
}
|
||||
|
||||
void EglRenderer::onMpvUpdate(void* ctx)
|
||||
{
|
||||
if (ctx != nullptr) static_cast<EglRenderer*>(ctx)->requestRender();
|
||||
}
|
||||
|
||||
void EglRenderer::renderLoop()
|
||||
{
|
||||
OHNativeWindow* window = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<char*>(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<std::mutex> 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();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include "player.h"
|
||||
#include <mpv/render.h>
|
||||
|
||||
#include <ace/xcomponent/native_interface_xcomponent.h>
|
||||
#include <native_window/external_window.h>
|
||||
#include <EGL/egl.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
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<bool> stop_{false};
|
||||
std::atomic<bool> surfaceReady_{false};
|
||||
std::atomic<int> surfaceStatus_{0};
|
||||
std::atomic<int> eglStatus_{0};
|
||||
std::atomic<int> 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;
|
||||
};
|
||||
Reference in new issue
Block a user