feat: surface OHCodec diagnostics in Playback Lab

This commit is contained in:
BoatingCloud committed 2026-08-29 12:36:20 +08:00
1 parent 3f6132637b
commit 37df87fc56
6 files changed
+213 -7

No files matched your search

+2
View File
@@ -10,3 +10,5 @@
```
`stage-mpv.sh` 只把本地 Phase 1 产物复制到未跟踪的 HAP native 库目录,不把 34 MB 的二进制提交到仓库。
面板中的 `OHCodec Probe` 只请求 `hwdec=ohcodec`,不会把当前 EGL window 注入 Direct Surface。没有独占 `ohos_direct_surface` 时,MPV 必须回退软件解码;面板会显示 requested/actual decoder、hwdec state、capability 摘要和回退原因。
@@ -1,6 +1,7 @@
#include "player.h"
#include "render.h"
#include "audio/out/ao_harmony.h"
#include "video/harmony_hwdec.h"
#include <ace/xcomponent/native_interface_xcomponent.h>
#include <hilog/log.h>
@@ -90,6 +91,20 @@ napi_value setAudioEnabled(napi_env env, napi_callback_info info)
return result;
}
napi_value setHardwareDecode(napi_env env, napi_callback_info info)
{
size_t argc = 1;
napi_value argv[1] = {nullptr};
bool enabled = false;
bool value = false;
if (napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr) == napi_ok && argc > 0 &&
napi_get_value_bool(env, argv[0], &value) == napi_ok)
enabled = value;
napi_value result = nullptr;
napi_get_boolean(env, gPlayer.setHardwareDecode(enabled), &result);
return result;
}
napi_value pause(napi_env env, napi_callback_info)
{
gPlayer.pause();
@@ -140,6 +155,34 @@ napi_value getState(napi_env env, napi_callback_info)
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, state.requestedDecoder.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, object, "requestedDecoder", value);
napi_create_string_utf8(env, state.actualDecoder.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, object, "actualDecoder", value);
napi_get_boolean(env, state.hwdecRequested, &value);
napi_set_named_property(env, object, "hwdecRequested", value);
napi_get_boolean(env, state.hwdecActive, &value);
napi_set_named_property(env, object, "hwdecActive", value);
napi_create_string_utf8(env, state.hwdecBackend.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, object, "hwdecBackend", value);
napi_create_string_utf8(env, state.hwFallbackReason.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, object, "hwdecFallbackReason", value);
napi_create_string_utf8(env, state.hwCapability.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, object, "hwCapability", value);
napi_create_int64(env, state.hardwareFrameCount, &value);
napi_set_named_property(env, object, "hardwareFrameCount", value);
napi_create_int64(env, state.softwareFrameCount, &value);
napi_set_named_property(env, object, "softwareFrameCount", value);
napi_create_int64(env, state.hwFallbackCount, &value);
napi_set_named_property(env, object, "hwFallbackCount", value);
napi_create_int64(env, state.nativeBufferImportCount, &value);
napi_set_named_property(env, object, "nativeBufferImportCount", value);
napi_create_int64(env, state.nativeBufferImportFailure, &value);
napi_set_named_property(env, object, "nativeBufferImportFailure", value);
napi_create_int64(env, state.surfacePresentCount, &value);
napi_set_named_property(env, object, "surfacePresentCount", value);
napi_create_int64(env, state.gpuInteropFrameCount, &value);
napi_set_named_property(env, object, "gpuInteropFrameCount", 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);
@@ -195,6 +238,7 @@ napi_value Init(napi_env env, napi_value exports)
{"load", nullptr, load, nullptr, nullptr, nullptr, napi_default, nullptr},
{"play", nullptr, play, nullptr, nullptr, nullptr, napi_default, nullptr},
{"setAudioEnabled", nullptr, setAudioEnabled, nullptr, nullptr, nullptr, napi_default, nullptr},
{"setHardwareDecode", nullptr, setHardwareDecode, 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},
+113 -5
View File
@@ -1,5 +1,7 @@
#include "player.h"
#include "video/harmony_hwdec.h"
#include <hilog/log.h>
#include <algorithm>
@@ -26,7 +28,10 @@ void logMpvError(const char* operation, int code)
}
}
PlaybackEngine::PlaybackEngine() = default;
PlaybackEngine::PlaybackEngine()
{
mpv_harmony_hwdec_set_requested(0);
}
PlaybackEngine::~PlaybackEngine()
{
@@ -94,6 +99,30 @@ bool PlaybackEngine::setAudioEnabled(bool enabled)
return true;
}
bool PlaybackEngine::setHardwareDecode(bool enabled)
{
if (handle_ == nullptr)
return false;
int code = mpv_set_property_string(handle_, "hwdec", enabled ? "ohcodec" : "no");
if (code < 0) {
logMpvError("hwdec", code);
setError("hardware decode toggle failed");
return false;
}
mpv_harmony_hwdec_set_requested(enabled ? 1 : 0);
std::lock_guard<std::mutex> lock(mutex_);
snapshot_.hwdecRequested = enabled;
snapshot_.requestedDecoder = enabled ? "ohcodec" : "software";
if (!enabled) {
snapshot_.hwdecActive = false;
snapshot_.actualDecoder = "software";
} else {
snapshot_.actualDecoder = "pending";
snapshot_.hwFallbackReason.clear();
}
return true;
}
void PlaybackEngine::observeProperties()
{
mpv_observe_property(handle_, kTimePos, "time-pos", MPV_FORMAT_DOUBLE);
@@ -101,6 +130,9 @@ void PlaybackEngine::observeProperties()
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);
mpv_observe_property(handle_, kHwdecCurrent, "hwdec-current", MPV_FORMAT_STRING);
mpv_observe_property(handle_, kHwdecInterop, "hwdec-interop", MPV_FORMAT_STRING);
mpv_observe_property(handle_, kDecoder, "decoder", MPV_FORMAT_STRING);
}
void PlaybackEngine::load(const std::string& url)
@@ -215,13 +247,18 @@ void PlaybackEngine::release()
mpv_terminate_destroy(handle_);
handle_ = nullptr;
}
mpv_harmony_hwdec_set_requested(0);
setState("RELEASED");
}
void PlaybackEngine::markFrameRendered()
{
bool hwdec_active = false;
std::lock_guard<std::mutex> lock(mutex_);
hwdec_active = snapshot_.hwdecActive;
++snapshot_.renderCount;
if (!hwdec_active)
mpv_harmony_hwdec_record_software_frame();
if (snapshot_.firstFrameMs < 0) {
auto elapsed = std::chrono::steady_clock::now() - startedAt_;
snapshot_.firstFrameMs = std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count();
@@ -233,9 +270,60 @@ void PlaybackEngine::markFrameRendered()
PlaybackSnapshot PlaybackEngine::snapshot() const
{
std::lock_guard<std::mutex> lock(mutex_);
const_cast<PlaybackEngine *>(this)->syncHwdecStatsLocked();
return snapshot_;
}
void PlaybackEngine::syncHwdecStatsLocked()
{
mpv_harmony_hwdec_stats stats = {};
if (!mpv_harmony_hwdec_get_stats(&stats))
return;
snapshot_.hwdecRequested = stats.hwdec_requested != 0;
snapshot_.hwdecActive = stats.hwdec_active != 0;
snapshot_.hardwareFrameCount = stats.hardware_frame_count;
snapshot_.softwareFrameCount = stats.software_frame_count;
snapshot_.hwFallbackCount = stats.hw_fallback_count;
snapshot_.nativeBufferImportCount = stats.native_buffer_import_count;
snapshot_.nativeBufferImportFailure = stats.native_buffer_import_failure;
snapshot_.surfacePresentCount = stats.surface_present_count;
snapshot_.gpuInteropFrameCount = stats.gpu_interop_frame_count;
snapshot_.hwdecBackend = stats.hwdec_backend;
snapshot_.hwFallbackReason = stats.fallback_reason;
snapshot_.hwCapability = stats.capability_summary;
if (stats.decoder_name[0] != '\0')
snapshot_.actualDecoder = stats.decoder_name;
else if (snapshot_.hwdecRequested && !snapshot_.hwdecActive && stats.hw_fallback_count > 0)
snapshot_.actualDecoder = "software";
}
void PlaybackEngine::handleLogMessage(const mpv_event_log_message* message)
{
if (!message || !message->text)
return;
const char* text = message->text;
if (std::strstr(text, "h264_ohcodec") || std::strstr(text, "hevc_ohcodec")) {
std::lock_guard<std::mutex> lock(mutex_);
snapshot_.actualDecoder = std::strstr(text, "hevc_ohcodec") ? "hevc_ohcodec" : "h264_ohcodec";
snapshot_.hwdecActive = true;
}
if (message->prefix && std::strncmp(message->prefix, "vd", 2) == 0 &&
(std::strstr(text, "fallback") || std::strstr(text, "falling back") ||
std::strstr(text, "No hardware decoding"))) {
bool requested = false;
{
std::lock_guard<std::mutex> lock(mutex_);
requested = snapshot_.hwdecRequested;
}
if (requested)
mpv_harmony_hwdec_set_fallback(text);
}
if (message->prefix && std::strncmp(message->prefix, "vd", 2) == 0) {
OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG,
"mpv[%{public}s] %{public}s", message->prefix, text);
}
}
void PlaybackEngine::setState(const char* state)
{
std::lock_guard<std::mutex> lock(mutex_);
@@ -273,6 +361,27 @@ void PlaybackEngine::updateProperty(uint64_t userdata, const mpv_event_property*
case kDropped:
if (property->format == MPV_FORMAT_INT64) snapshot_.dropped = *static_cast<int64_t*>(property->data);
break;
case kHwdecCurrent:
if (property->format == MPV_FORMAT_STRING) {
const char *value = *static_cast<char **>(property->data);
snapshot_.hwdecActive = value && std::strcmp(value, "no") != 0;
}
break;
case kHwdecInterop:
if (property->format == MPV_FORMAT_STRING) {
const char *value = *static_cast<char **>(property->data);
snapshot_.hwdecBackend = value ? value : "";
}
break;
case kDecoder:
if (property->format == MPV_FORMAT_STRING) {
const char *value = *static_cast<char **>(property->data);
if (value && value[0]) {
snapshot_.actualDecoder = value;
mpv_harmony_hwdec_set_decoder(value);
}
}
break;
default:
break;
}
@@ -289,10 +398,9 @@ void PlaybackEngine::eventLoop()
case MPV_EVENT_LOG_MESSAGE: {
auto* message = static_cast<mpv_event_log_message*>(event->data);
if (message != nullptr && message->prefix != nullptr &&
std::strncmp(message->prefix, "ao", 2) == 0) {
OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG,
"mpv[%{public}s] %{public}s", message->prefix, message->text);
}
(std::strncmp(message->prefix, "ao", 2) == 0 ||
std::strncmp(message->prefix, "vd", 2) == 0))
handleLogMessage(message);
break;
}
case MPV_EVENT_PROPERTY_CHANGE:
+20
View File
@@ -18,6 +18,20 @@ struct PlaybackSnapshot {
int64_t firstFrameMs = -1;
uint64_t renderCount = 0;
std::string lastError;
std::string requestedDecoder = "software";
std::string actualDecoder = "software";
std::string hwdecBackend;
std::string hwFallbackReason;
std::string hwCapability;
bool hwdecRequested = false;
bool hwdecActive = false;
int64_t hardwareFrameCount = 0;
int64_t softwareFrameCount = 0;
int64_t hwFallbackCount = 0;
int64_t nativeBufferImportCount = 0;
int64_t nativeBufferImportFailure = 0;
int64_t surfacePresentCount = 0;
int64_t gpuInteropFrameCount = 0;
};
class PlaybackEngine {
@@ -27,6 +41,7 @@ public:
bool initialize();
bool setAudioEnabled(bool enabled);
bool setHardwareDecode(bool enabled);
void load(const std::string& url);
void play();
void pause();
@@ -44,6 +59,8 @@ private:
void setState(const char* state);
void setError(const std::string& error);
void updateProperty(uint64_t userdata, const mpv_event_property* property);
void syncHwdecStatsLocked();
void handleLogMessage(const mpv_event_log_message* message);
mutable std::mutex mutex_;
PlaybackSnapshot snapshot_;
@@ -52,4 +69,7 @@ private:
std::atomic<bool> stopEvents_{false};
std::atomic<bool> initialized_{false};
std::chrono::steady_clock::time_point startedAt_;
static constexpr uint64_t kHwdecCurrent = 6;
static constexpr uint64_t kHwdecInterop = 7;
static constexpr uint64_t kDecoder = 8;
};
@@ -25,6 +25,20 @@ interface PlaybackState {
audioFlushCount: number;
seekAudioResetCount: number;
audioState: number;
requestedDecoder: string;
actualDecoder: string;
hwdecRequested: boolean;
hwdecActive: boolean;
hwdecBackend: string;
hwdecFallbackReason: string;
hwCapability: string;
hardwareFrameCount: number;
softwareFrameCount: number;
hwFallbackCount: number;
nativeBufferImportCount: number;
nativeBufferImportFailure: number;
surfacePresentCount: number;
gpuInteropFrameCount: number;
}
const LOCAL_TEST_URL: string = 'file:///data/storage/el2/base/haps/entry/files/sample.mp4';
@@ -41,7 +55,12 @@ struct Index {
firstFrameMs: -1, renderCount: 0, lastError: '', surface: 'WAITING', egl: 'WAITING', mpv: 'WAITING',
audioAvailable: false, audioBackend: 'disabled', sampleRate: 0, sampleFormat: 0, channels: 0,
channelLayout: 0, submittedFrames: 0, playedFrames: 0, estimatedAudioDelayMs: 0,
audioBufferStarvation: 0, audioFlushCount: 0, seekAudioResetCount: 0, audioState: 0
audioBufferStarvation: 0, audioFlushCount: 0, seekAudioResetCount: 0, audioState: 0,
requestedDecoder: 'software', actualDecoder: 'software', hwdecRequested: false,
hwdecActive: false, hwdecBackend: '', hwdecFallbackReason: '', hwCapability: '',
hardwareFrameCount: 0, softwareFrameCount: 0, hwFallbackCount: 0,
nativeBufferImportCount: 0, nativeBufferImportFailure: 0, surfacePresentCount: 0,
gpuInteropFrameCount: 0
};
private poller: number = 0;
@@ -87,6 +106,12 @@ struct Index {
Button('Stop').onClick(() => playbackLab.stop())
Button('Release').onClick(() => playbackLab.release())
}.width('100%')
Row({ space: 8 }) {
Button(this.state.hwdecRequested ? 'OHCodec On' : 'OHCodec Probe').onClick(() => {
const enabled: boolean = !this.state.hwdecRequested;
playbackLab.setHardwareDecode(enabled);
})
}.width('100%')
Row({ space: 8 }) {
Button('-10s').onClick(() => playbackLab.seek(-10))
Button('+10s').onClick(() => playbackLab.seek(10))
@@ -101,6 +126,12 @@ struct Index {
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
Text(`flushes=${this.state.audioFlushCount} seekResets=${this.state.seekAudioResetCount}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%')
Text(`decoder=${this.state.requestedDecoder} -> ${this.state.actualDecoder} requested=${this.state.hwdecRequested} active=${this.state.hwdecActive} backend=${this.state.hwdecBackend || 'none'}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
Text(`hwFrames=${this.state.hardwareFrameCount} swFrames=${this.state.softwareFrameCount} fallbacks=${this.state.hwFallbackCount} surfacePresent=${this.state.surfacePresentCount} interop=${this.state.gpuInteropFrameCount}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
Text(`fallback=${this.state.hwdecFallbackReason || 'none'} capability=${this.state.hwCapability || 'DEVICE TEST REQUIRED'}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(3)
Text(detailLabel(this.detail)).fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
}
.padding(16).width('100%').height('100%').backgroundColor($r('app.color.background'))
+2 -1
View File
@@ -17,7 +17,8 @@ for symbol in \
mpv_render_context_render \
mpv_render_context_set_update_callback \
mpv_render_context_free \
mpv_harmony_audio_get_stats; do
mpv_harmony_audio_get_stats \
mpv_harmony_hwdec_get_stats; do
printf '%s\n' "$SYMBOLS" | grep -Eq "[[:space:]]${symbol}$" || {
printf 'Missing libmpv symbol: %s\n' "$symbol" >&2
exit 1