使用NDK接口实现画中画功能开发（C/C++）
=======================

本文以视频播放为例，介绍通过NDK接口实现画中画功能的基本开发步骤。

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/62/v3/ttXqfgSpQR6KIzQz2hKQrQ/note_3.0-zh-cn.png?HW-CC-KV=V1&HW-CC-Date=20260612T073325Z&HW-CC-Expire=86400&HW-CC-Sign=57F5AF761E2D00189401A01812F55D60FCAD59DB4C6C70683EDD3F49F8AC7391)

*   从API version 20开始，支持使用NDK接口实现画中画功能开发。
    
*   支持在Phone、PC/2in1、Tablet设备使用NDK接口实现画中画功能开发。
    

#### 约束与限制

*   画中画窗口中画面的呈现不通过传入XComponent Controller实现，而是通过渲染surfaceId（在开启画中画回调中获取）对应的组件实现。
    
*   与typeNode实现方式相同，系统不缓存页面。如需进行页面操作，应用需要开启画中画生命周期监听，在对应周期内进行对应操作。
    
*   不支持设置自动拉起画中画。
    

#### 开发步骤

示例中的视频播放器简易实现逻辑如下：

1.  通过OH\_PictureInPicture\_CreatePipConfig创建画中画参数配置器，并通过OH\_PictureInPicture\_SetPipMainWindowId、OH\_PictureInPicture\_SetPipTemplateType、OH\_PictureInPicture\_SetPipRect、OH\_PictureInPicture\_SetPipControlGroup、OH\_PictureInPicture\_SetPipNapiEnv接口在画中画参数配置器中设置初始配置信息。
    
2.  创建画中画控制器，后续可根据返回的控制器标识controllerId注册生命周期事件以及控制事件回调。通过OH\_PictureInPicture\_CreatePip接口创建画中画控制器实例，并缓存对应的控制器标识。建议在创建完成后立即调用OH\_PictureInPicture\_DestroyPipConfig销毁画中画参数配置器，以免发生内存泄漏。
    
3.  通过OH\_PictureInPicture\_RegisterStartPipCallback接口注册启动画中画回调，并根据返回的surfaceId渲染视频画面。同时应用可以按需注册其他需要监听的事件回调。
    
4.  通过OH\_PictureInPicture\_StartPip启动画中画。
    
5.  通过OH\_PictureInPicture\_UpdatePipContentSize更新媒体源尺寸信息。
    
6.  通过OH\_PictureInPicture\_StopPip关闭画中画。
    
7.  通过OH\_PictureInPicture\_UnregisterStartPipCallback解注册画中画启动回调，避免内存泄漏。同时应用可以按需解注册其他已注册的事件回调。
    

以上步骤涉及的各文件及示例可见下文。

Node-API模块注册，具体使用请参考[Native API在应用工程中的使用指导](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/napi-guidelines)。

本文件仅作为参考示例，异常处理及错误码打印由开发者按需处理。

// napi\_init.cpp
#include "napi/native\_api.h"
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <string>
#include "window\_manager/oh\_window\_pip.h"
#include "hilog/log.h"
#include "js\_native\_api.h"
#include "napi/native\_api.h"
#include <cstdio>
#include "rawfile/raw\_file\_manager.h"

#define LOG\_MSG\_TAG "PiPMain"
#define LOG(format, ...) ((void)OH\_LOG\_Print(LOG\_APP, LOG\_INFO, 0xFF00, LOG\_MSG\_TAG, format, ##\_\_VA\_ARGS\_\_))
napi\_ref jsCallback;
napi\_env env\_;

napi\_ref jsLifecycleCallback;
napi\_env lifeEnv\_;
int32\_t g\_minValue = 0;
int32\_t g\_maxValue = 255;

inline bool ConvertFromJsNumber(napi\_env env, napi\_value jsValue, int32\_t& value)
{
    return napi\_get\_value\_int32(env, jsValue, &value) == napi\_ok;
}

inline bool ConvertFromJsNumber(napi\_env env, napi\_value jsValue, uint32\_t& value)
{
    return napi\_get\_value\_uint32(env, jsValue, &value) == napi\_ok;
}

inline bool ConvertFromJsNumber(napi\_env env, napi\_value jsValue, int64\_t& value)
{
    return napi\_get\_value\_int64(env, jsValue, &value) == napi\_ok;
}

inline bool ConvertFromJsNumber(napi\_env env, napi\_value jsValue, uint64\_t& value)
{
    int64\_t num;
    auto res = napi\_get\_value\_int64(env, jsValue, &num);
    if (res == napi\_ok) {
        value = static\_cast<uint64\_t>(num);
    }
    return res == napi\_ok;
}

inline bool ConvertFromJsNumber(napi\_env env, napi\_value jsValue, double& value)
{
    return napi\_get\_value\_double(env, jsValue, &value) == napi\_ok;
}

inline bool ConvertFromJsNumber(napi\_env env, napi\_value jsValue, bool& value)
{
    return napi\_get\_value\_bool(env, jsValue, &value) == napi\_ok;
}

inline bool ConvertFromJsNumber(napi\_env env, napi\_value jsValue, unsigned char& value)
{
    int32\_t num;
    if (napi\_get\_value\_int32(env, jsValue, &num) != napi\_ok) {
        return false;
    }
    if (num < g\_minValue || num > g\_maxValue) {
        return false; // 越界无效
    }
    value = static\_cast<unsigned char>(num);
    return true;
}

template<class T>
bool ConvertFromJsValue(napi\_env env, napi\_value jsValue, T& value)
{
    if (jsValue == nullptr) {
        return false;
    }
    using ValueType = std::remove\_cv\_t<std::remove\_reference\_t<T>>;
    if constexpr (std::is\_same\_v<ValueType, bool>) {
        return napi\_get\_value\_bool(env, jsValue, &value) == napi\_ok;
    } else if constexpr (std::is\_arithmetic\_v<ValueType>) {
        return ConvertFromJsNumber(env, jsValue, value);
    } else if constexpr (std::is\_same\_v<ValueType, std::string>) {
        size\_t len = 0;
        if (napi\_get\_value\_string\_utf8(env, jsValue, nullptr, 0, &len) != napi\_ok) {
            return false;
        }
        auto buffer = std::make\_unique<char\[\]>(len + 1);
        size\_t strLength = 0;
        if (napi\_get\_value\_string\_utf8(env, jsValue, buffer.get(), len + 1, &strLength) == napi\_ok) {
            value = buffer.get();
            return true;
        }
        return false;
    } else if constexpr (std::is\_enum\_v<ValueType>) {
        std::make\_signed\_t<ValueType> numberValue = 0;
        if (!ConvertFromJsNumber(env, jsValue, numberValue)) {
            return false;
        }
        value = static\_cast<ValueType>(numberValue);
        return true;
    }
    return false;
}

void PipStartPipCallback(uint32\_t controllerId, uint8\_t requestId, uint64\_t surfaceId)
{
    if (jsCallback) {
        napi\_value global = nullptr;
        napi\_get\_global(env\_, &global);
        size\_t argc = 1;
        std::string tStr = std::to\_string(surfaceId);
        const char\* cStr = tStr.c\_str();
        size\_t length = strlen(cStr);
        napi\_value str;
        napi\_status status = napi\_create\_string\_utf8(env\_, cStr, length, &str);
        napi\_value argv\[1\] = {str};
        napi\_value jsCallbackValue;
        
        napi\_value result = nullptr;
        if (!jsCallback) {
            LOG("js callback is invalid");
        }
        napi\_get\_reference\_value(env\_, jsCallback, &jsCallbackValue);
        napi\_call\_function(env\_, global, jsCallbackValue, argc, argv, &result);
    }
}

void LifecycleCallback(uint32\_t controllerId, PictureInPicture\_PipState state, int32\_t errcode)
{
    if (jsLifecycleCallback) {
        napi\_value global = nullptr;
        napi\_get\_global(lifeEnv\_, &global);
        size\_t argc = 1;
        napi\_value pipState = nullptr;
        napi\_create\_int32(lifeEnv\_, static\_cast<int32\_t> (state), &pipState);
        napi\_value argv\[1\] = {pipState};
        napi\_value jsCallbackValue;
        
        napi\_value result = nullptr;
        if (!jsCallback) {
            LOG("js callback is invalid");
        }
        napi\_get\_reference\_value(lifeEnv\_, jsLifecycleCallback, &jsCallbackValue);
        napi\_call\_function(lifeEnv\_, global, jsCallbackValue, argc, argv, &result);
    }
}

class PiPManager {
public:
    static napi\_value CreatePip(napi\_env env, napi\_callback\_info info);
    static napi\_value StartPip(napi\_env env, napi\_callback\_info info);
    static napi\_value RegisterStartPip(napi\_env env, napi\_callback\_info info);
    static napi\_value DeletePip(napi\_env env, napi\_callback\_info info);
    static napi\_value StopPip(napi\_env env, napi\_callback\_info info);
    static napi\_value RegisterLifecycleListener(napi\_env env, napi\_callback\_info info);
    static void getElement(napi\_env &env, uint32\_t size, napi\_value &controlGroupValue,
                PictureInPicture\_PipControlGroup controlGroup\[\]);
};

napi\_value PiPManager::CreatePip(napi\_env env, napi\_callback\_info info)
{
    size\_t argc = 1;
    napi\_value argv\[1\] = {nullptr};
    napi\_get\_cb\_info(env, info, &argc, argv, nullptr, nullptr);
    napi\_value config = argv\[0\];

    napi\_value mainWindowIdValue = nullptr;
    napi\_value pipTemplateTypeValue = nullptr;
    napi\_value widthValue = nullptr;
    napi\_value heightValue = nullptr;
    napi\_value controlGroupValue = nullptr;
    napi\_value pipControllerIdValue = nullptr;
    
    uint32\_t controllerId = -1;
    uint32\_t mainWindowId = -1;
    PictureInPicture\_PipTemplateType pipTemplateType = PictureInPicture\_PipTemplateType::VIDEO\_PLAY;
    uint32\_t width = -1;
    uint32\_t height = -1;
    
    napi\_get\_named\_property(env, config, "mainWindowId", &mainWindowIdValue);
    napi\_get\_named\_property(env, config, "pipTemplateType", &pipTemplateTypeValue);
    napi\_get\_named\_property(env, config, "width", &widthValue);
    napi\_get\_named\_property(env, config, "height", &heightValue);
    napi\_get\_named\_property(env, config, "controlGroup", &controlGroupValue);
    napi\_get\_named\_property(env, config, "pipControllerId", &pipControllerIdValue);
    
    ConvertFromJsValue(env, mainWindowIdValue, mainWindowId);
    ConvertFromJsValue(env, pipTemplateTypeValue, pipTemplateType);
    ConvertFromJsValue(env, widthValue, width);
    ConvertFromJsValue(env, heightValue, height);
    ConvertFromJsValue(env, pipControllerIdValue, controllerId);
    
    uint32\_t size = 0;
    napi\_get\_array\_length(env, controlGroupValue, &size);
    PictureInPicture\_PipControlGroup controlGroup\[size\];

    PiPManager::getElement(env, size, controlGroupValue, controlGroup);
    
    napi\_value result = nullptr;
    PictureInPicture\_PipConfig pipConfig;
    OH\_PictureInPicture\_CreatePipConfig(&pipConfig);
    OH\_PictureInPicture\_SetPipMainWindowId(pipConfig, mainWindowId);
    OH\_PictureInPicture\_SetPipTemplateType(pipConfig, pipTemplateType);
    OH\_PictureInPicture\_SetPipRect(pipConfig, width, height);
    OH\_PictureInPicture\_SetPipControlGroup(pipConfig, controlGroup, size);
    OH\_PictureInPicture\_SetPipNapiEnv(pipConfig, env);
    int32\_t res = OH\_PictureInPicture\_CreatePip(pipConfig, &controllerId);
    OH\_PictureInPicture\_DestroyPipConfig(&pipConfig);
    napi\_create\_uint32(env, controllerId, &result);
    return result;
}

void PiPManager::getElement(napi\_env &env, uint32\_t size, napi\_value &controlGroupValue,
    PictureInPicture\_PipControlGroup controlGroup\[\])
{
    for (uint32\_t i = 0; i < size; i++) {
        napi\_value getElementValue = nullptr;
        napi\_get\_element(env, controlGroupValue, i, &getElementValue);
        PictureInPicture\_PipControlGroup controlType;
        if (ConvertFromJsValue(env, getElementValue, controlType)) {
            controlGroup\[i\] = controlType;
        }
        LOG("controlType: %{public}d", controlType);
    }
}

napi\_value PiPManager::StartPip(napi\_env env, napi\_callback\_info info)
{
    size\_t argc = 1;
    napi\_value argv\[1\] = {nullptr};
    napi\_get\_cb\_info(env, info, &argc, argv, nullptr, nullptr);
    napi\_value controlIdValue = argv\[0\];
    uint32\_t controlId = -1;
    ConvertFromJsValue(env, controlIdValue, controlId);
    napi\_value resultValue = nullptr;
    int32\_t result = OH\_PictureInPicture\_StartPip(controlId);
    napi\_create\_uint32(env, result, &resultValue);
    return resultValue;
}

napi\_value PiPManager::RegisterStartPip(napi\_env env, napi\_callback\_info info)
{
    size\_t argc = 2;
    napi\_value argv\[2\] = {nullptr};
    napi\_get\_cb\_info(env, info, &argc, argv, nullptr, nullptr);
    napi\_value controllerIdValue = argv\[0\];
    uint32\_t controlId = -1;
    napi\_status status = napi\_create\_reference(env, argv\[1\], 1, &jsCallback);
    env\_ = env;
    ConvertFromJsValue(env, controllerIdValue, controlId);
    napi\_value resultValue = nullptr;
    int32\_t result = OH\_PictureInPicture\_RegisterStartPipCallback(controlId, PipStartPipCallback);
    napi\_create\_uint32(env, result, &resultValue);
    return resultValue;
}

napi\_value PiPManager::DeletePip(napi\_env env, napi\_callback\_info info)
{
    size\_t argc = 1;
    napi\_value argv\[1\] = {nullptr};
    napi\_get\_cb\_info(env, info, &argc, argv, nullptr, nullptr);
    uint32\_t controlId = -1;
    ConvertFromJsValue(env, argv\[0\], controlId);
    napi\_value resultValue = nullptr;
    int32\_t result = OH\_PictureInPicture\_DeletePip(controlId);
    napi\_create\_uint32(env, result, &resultValue);
    return resultValue;
}

napi\_value PiPManager::StopPip(napi\_env env, napi\_callback\_info info)
{
    size\_t argc = 1;
    napi\_value argv\[1\] = {nullptr};
    napi\_get\_cb\_info(env, info, &argc, argv, nullptr, nullptr);
    uint32\_t controlId = -1;
    napi\_value resultValue = nullptr;

    ConvertFromJsValue(env, argv\[0\], controlId);
    uint32\_t result = OH\_PictureInPicture\_StopPip(controlId);
    napi\_create\_uint32(env, result, &resultValue);
    return resultValue;
}

napi\_value PiPManager::RegisterLifecycleListener(napi\_env env, napi\_callback\_info info)
{
    size\_t argc = 2;
    napi\_value argv\[2\] = {nullptr};
    napi\_get\_cb\_info(env, info, &argc, argv, nullptr, nullptr);
    uint32\_t controlId = -1;
    napi\_status status = napi\_create\_reference(env, argv\[1\], 1, &jsLifecycleCallback);
    lifeEnv\_ = env;
    if (status != napi\_ok) {
        LOG("register failed %{public}d", status);
    }
    ConvertFromJsValue(env, argv\[0\], controlId);
    
    napi\_value resultValue = nullptr;
    int32\_t result = OH\_PictureInPicture\_RegisterLifecycleListener(controlId, LifecycleCallback);
    napi\_create\_uint32(env, result, &resultValue);
    return resultValue;
}

EXTERN\_C\_START
static napi\_value Init(napi\_env env, napi\_value exports)
{
    napi\_property\_descriptor desc\[\] = {
        {"createPip", nullptr, PiPManager::CreatePip, nullptr, nullptr, nullptr, napi\_default, nullptr},
        {"startPip", nullptr, PiPManager::StartPip, nullptr, nullptr, nullptr, napi\_default, nullptr},
        {"registerStartPip", nullptr, PiPManager::RegisterStartPip, nullptr, nullptr, nullptr, napi\_default, nullptr},
        {"deletePip", nullptr, PiPManager::DeletePip, nullptr, nullptr, nullptr, napi\_default, nullptr},
        {"stopPip", nullptr, PiPManager::StopPip, nullptr, nullptr, nullptr, napi\_default, nullptr},
        {"registerLifecycleListener", nullptr, PiPManager::RegisterLifecycleListener,
            nullptr, nullptr, nullptr, napi\_default, nullptr},
    };
    napi\_define\_properties(env, exports, sizeof(desc) / sizeof(desc\[0\]), desc);
    return exports;
}
EXTERN\_C\_END

static napi\_module demoModule = {
    .nm\_version = 1,
    .nm\_flags = 0,
    .nm\_filename = nullptr,
    .nm\_register\_func = Init,
    .nm\_modname = "entry",
    .nm\_priv = ((void\*)0),
    .reserved = { 0 },
};
extern "C" \_\_attribute\_\_((constructor)) void RegisterEntryModule(void)
{
    napi\_module\_register(&demoModule);
}

Node-API接口声明。

// Index.d.ts
export enum PiPControlGroup {
  VIDEO\_PLAY\_VIDEO\_PREVIOUS\_NEXT = 101,
  VIDEO\_PLAY\_FAST\_FORWARD\_BACKWARD = 102,
  VIDEO\_CALL\_MICROPHONE\_SWITCH = 201,
  VIDEO\_CALL\_HANG\_UP\_BUTTON = 202,
  VIDEO\_CALL\_CAMERA\_SWITCH = 203,
  VIDEO\_CALL\_MUTE\_SWITCH = 204,
  VIDEO\_MEETING\_HANG\_UP\_BUTTON = 301,
  VIDEO\_MEETING\_CAMERA\_SWITCH = 302,
  VIDEO\_MEETING\_MUTE\_SWITCH = 303,
  VIDEO\_MEETING\_MICROPHONE\_SWITCH = 304,
  VIDEO\_LIVE\_VIDEO\_PLAY\_PAUSE = 401,
  VIDEO\_LIVE\_MUTE\_SWITCH = 402,
}
export interface PiPConfig {
  mainWindowId: number;
  pipTemplateType: number;
  width: number;
  height: number;
  controlGroup: Array<PiPControlGroup>;
}
export declare const createPip: (config: PiPConfig) => number;
export declare const startPip: (controllerId: number) => number;
export declare const registerStartPip: (controllerId: number, jsCallback: Function) => number;
export declare const deletePip: (controllerId: number) => number;
export declare const stopPip: (controllerId: number) => number;
export declare const registerLifecycleListener: (controllerId: number, jsCallback: Function) => number;

CMakeLists.txt文件，用于生成对应的库文件。

\# CMakeLists.txt
# the minimum version of CMake.
cmake\_minimum\_required(VERSION 3.5.0)
set(CMAKE\_CXX\_STANDARD 17)
set(CMAKE\_CXX\_STANDARD\_REQUIRED ON)
project(MyApplication)
set(NATIVERENDER\_ROOT\_PATH ${CMAKE\_CURRENT\_SOURCE\_DIR})
if(DEFINED PACKAGE\_FIND\_FILE)
    include(${PACKAGE\_FIND\_FILE})
endif()
include\_directories(${NATIVERENDER\_ROOT\_PATH}
                    ${NATIVERENDER\_ROOT\_PATH}/include)
add\_library(entry SHARED napi\_init.cpp)
target\_link\_libraries(entry PUBLIC libace\_napi.z.so libace\_ndk.z.so libnative\_window\_manager.so libhilog\_ndk.z.so)

EntryAbility文件示例。

// entryability/EntryAbility.ets
import { BusinessError } from '@kit.BasicServicesKit';
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { PipManager } from '../nodefree/PipManager';
import { Logger } from '../util/LogUtil';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    AppStorage.setOrCreate('UIAbilityContext', this.context);
    try {
      this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR\_MODE\_NOT\_SET);
    } catch (err) {
      Logger.error('testTag', 'Failed to set color mode: %{public}s', JSON.stringify(err));
    }
  }

  onDestroy(): void {
    Logger.info('testTag', '%{public}s', 'Ability onDestroy');
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    // Main window is created, set main page for this ability
    Logger.info('testTag', '%{public}s', 'Ability onWindowStageCreate');
    let windowClass: window.Window | undefined = undefined;
    let windowClassId: number = -1;

    windowStage.getMainWindow().then((window) => {
      if (window == null) {
        Logger.error('Failed to obtaining the window. Cause: The data is empty');
        return;
      }
      windowClass = window;
      windowClass.setUIContent('pages/Index');
      windowClassId = windowClass.getWindowProperties().id;
      AppStorage.setOrCreate('windowId', windowClassId);
      Logger.info('Succeeded in obtaining the window')

      let ctx = window.getUIContext();
      AppStorage.setOrCreate('UIContext', ctx);
      // 通过主窗口UIContext创建typeNode节点
      PipManager.getInstance().makeTypeNode(ctx);
    }).catch((err: BusinessError) => {
      Logger.error(\`Failed to obtaining the window. Cause code: ${err.code}, message: ${err.message}\`);
    });
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        Logger.error('testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
        return;
      }
      Logger.info('testTag', 'Succeeded in loading the content.');
    });
  }
  onWindowStageDestroy(): void {
    // Main window is destroyed, release UI related resources
    Logger.info('testTag', '%{public}s', 'Ability onWindowStageDestroy');
  }

  onForeground(): void {
    // Ability has brought to foreground
    Logger.info('testTag', '%{public}s', 'Ability onForeground');
  }

  onBackground(): void {
    // Ability has back to background
    Logger.info('testTag', '%{public}s', 'Ability onBackground');
  }
}

示例中的视频播放需要使用AVPlayer，具体实现可参考[视频播放](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/video-playback)。

// model/AVPlayer.ets
// 视频播放器简单实现
import media from '@ohos.multimedia.media';
import common from '@ohos.app.ability.common';
import { BusinessError } from '@ohos.base';
import resourceManager from '@ohos.resourceManager';
import { Logger } from '../util/LogUtil';

export class AVPlayer {
  public avPlayer?: media.AVPlayer;
  private count: number = 0;
  private surfaceID: string; // surfaceID用于播放画面显示，具体的值需要通过Xcomponent接口获取，相关文档链接见上面Xcomponent创建方法
  public jumpNext: boolean = false;
  public type: number = 0; // 用于区分主界面的player还是pip界面的player
  public state\_: string = ''
  public playStatus: boolean = true;

  constructor(surfaceID: string, type: number) {
    this.surfaceID = surfaceID;
    this.type = type
  }

  setSurfaceId(id: string) {
    if (this.avPlayer) {
      this.surfaceID = id;
      this.avPlayer.surfaceId = id;
    }
  }

  updatePlayStatus(status: boolean) {
    this.playStatus = status;
  }
  // 注册avplayer回调函数
  setAVPlayerCallback() {
    // seek操作结果回调函数
    this.avPlayer?.on('seekDone', (seekDoneTime: number) => {
      Logger.info(\`PipMain AVPlayer seek succeeded, seek time is ${seekDoneTime}\`);
    })
    // error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程
    this.avPlayer?.on('error', (err: BusinessError) => {
      Logger.error(\`PipMain Invoke avPlayer failed, code is ${err.code}, message is ${err.message}\`);
      this.avPlayer?.reset(); // 调用reset重置资源，触发idle状态
    })
    // 状态机变化回调函数
    this.avPlayer?.on('stateChange', async (state, reason) => {
      if (!this.avPlayer) {
        return;
      }
      this.state\_ = state;
      switch (state) {
        case 'idle': // 成功调用reset接口后触发该状态机上报
          Logger.info('AVPlayer state idle called.');
          if (!this.jumpNext) {
            this.avPlayer.release(); // 调用release接口销毁实例对象
          } else {
            let uiContext: UIContext = AppStorage.get('UIAbilityContext') as UIContext;
            let context = uiContext.getHostContext() as common.UIAbilityContext;
            let fileDescriptor: resourceManager.RawFileDescriptor;
            fileDescriptor = await context.resourceManager.getRawFd('640x360.mp4');
            // 为fdSrc赋值触发initialized状态机上报
            this.avPlayer.fdSrc = fileDescriptor;
          }
          break;
        case 'initialized': // avplayer 设置播放源后触发该状态上报
          Logger.info('initialized called.');
          this.avPlayer.surfaceId = this.surfaceID; // 设置显示画面，当播放的资源为纯音频时无需设置
          this.avPlayer.prepare().then(() => {
            Logger.info('AVPlayer prepare succeeded.');
          }, (err: BusinessError) => {
            Logger.error(\`Invoke prepare failed, code is ${err.code}, message is ${err.message}\`);
          });
          break;
        case 'prepared': // prepare调用成功后上报该状态机
          Logger.info('AVPlayer state prepared called.');
          this.avPlayer.play(); // 调用播放接口开始播放
          break;
        case 'playing': // play成功调用后触发该状态机上报
          Logger.info('AVPlayer state playing called.');
          this.jumpNext = false;
          this.count++;
          break;
        case 'paused': // pause成功调用后触发该状态机上报
          Logger.info('AVPlayer state paused called.');
          // this.avPlayer.play(); // 再次播放接口开始播放
          break;
        case 'completed': // 播放结束后触发该状态机上报
          Logger.info('AVPlayer state completed called.');
          this.playNext();
          ; // 调用播放结束接口
          break;
        case 'stopped': // stop接口成功调用后触发该状态机上报
          Logger.info('AVPlayer state stopped called.');
          this.avPlayer.reset(); // 调用reset接口初始化avplayer状态
          break;
        case 'released':
          Logger.info('AVPlayer state released called.');
          break;
        default:
          Logger.info('AVPlayer state unknown called.');
          break;
      }
    })
    this.avPlayer?.on('videoSizeChange', (width: number, height: number) => {
      Logger.info('videoSizeChange width:' + width + ' height:' + height);
      let context = AppStorage.get('UIAbilityContext') as common.UIAbilityContext;
    })
  }

  // 以下demo为使用资源管理接口获取打包在HAP内的媒体资源文件并通过fdSrc属性进行播放示例
  async avPlayerFdSrc() {
    Logger.info('avPlayerFdSrc');
    try {
      // 创建avPlayer实例对象
      this.avPlayer = await media.createAVPlayer();
    } catch (err) {
      let error = err as BusinessError;
      Logger.error(\`createAVPlayer failed, code: ${error.code}, msg: ${error.message}\`);
      return;
    }

    // 创建状态机变化回调函数
    this.setAVPlayerCallback();
    // 通过UIAbilityContext的resourceManager成员的getRawFd接口获取媒体资源播放地址
    // 返回类型为{fd,offset,length},fd为HAP包fd地址，offset为媒体资源偏移量，length为播放长度

    try {
      let context = AppStorage.get('UIAbilityContext') as common.UIAbilityContext;
      let fileDescriptor = await context.resourceManager.getRawFd('640x360.mp4');
      Logger.info('getRawFd');
      // 为fdSrc赋值触发initialized状态机上报
      this.avPlayer.fdSrc = fileDescriptor;
    } catch (err) {
      let error = err as BusinessError;
      Logger.error(\`getRawFd failed, code=${error.code}, msg=${error.message}\`);
    }
  }

  async playNext() {
    if (this.avPlayer === null) {
      return;
    }
    this.jumpNext = true;
    this.avPlayer?.stop()
    .then(() => {
      Logger.info(\`Succeeded to stop avPlayer\`);
    }).catch((err: BusinessError) => {
      Logger.error(\`Failed to stop avPlayer, Cause:${err.code}, message:${err.message}\`);
    });
  }

  play() {
    if (this.state\_ === 'paused') {
      this.avPlayer?.play()
        .then(() => {
          Logger.info(\`Succeeded to play avPlayer\`);
        }).catch((err: BusinessError) => {
          Logger.error(\`Failed to play avPlayer, Cause:${err.code}, message:${err.message}\`);
      });
    }
  }

  pause() {
    if (this.state\_ === 'playing') {
      this.avPlayer?.pause()
        .then(() => {
          Logger.info(\`Succeeded to pause avPlayer\`);
        }).catch((err: BusinessError) => {
          Logger.error(\`Failed to pause avPlayer, Cause:${err.code}, message:${err.message}\`);
      });
    }
  }

  stopAvPlayer() {
    Logger.info('stopAvPlayer>>>')
    if (!this.avPlayer) {
      return;
    }
    this.avPlayer.stop()
      .then(() => {
        Logger.info(\`Succeeded to stop avPlayer\`);
      }).catch((err: BusinessError) => {
        Logger.error(\`Failed to stop avPlayer, Cause:${err.code}, message:${err.message}\`);
    });
    Logger.info('stopping>>>');
    this.avPlayer.reset()
      .then(() => {
        Logger.info(\`Succeeded to reset avPlayer\`);
      }).catch((err: BusinessError) => {
        Logger.error(\`Failed to reset avPlayer, Cause:${err.code}, message:${err.message}\`);
    });
  }
}

应用界面布局文件，用于演示画中画基本功能。

// pages/Index.ets
// 应用首页
import { router } from '@kit.ArkUI';

@Entry
@Component
struct Index {
  pathStack: NavPathStack = new NavPathStack();

  build() {
    Navigation(this.pathStack) {
      Scroll() {
        Flex({ direction: FlexDirection.Column }) {
          // ...
          this.featureButton('使用NDK接口实现画中画（C++）', this.ndkImplement);
        }
      }
    }
    .hideBackButton(true)
    .titleMode(NavigationTitleMode.Mini)
    .backgroundColor('#FFF1F3F5')
    .mode(NavigationMode.Stack)
    .title('画中画SampleCode')
  }

  @Builder
  featureButton(buttonText: string, callbackOnClick: () => void) {
    Button({ type: ButtonType.Normal }) {
      Row() {
        Column() {
          Text(buttonText)
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor('#000000')
          Rect()
            .radius(1)
            .fill('#0A59F7')
            .height(2)
            .width(30)
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
      }
      .width('100%')
    }
    .width('90%')
    .padding('5%')
    .margin({ top: '3%', bottom: '2%', right: '3%' })
    .backgroundColor('#FFFFFF')
    .borderRadius(20)
    .onClick(callbackOnClick)
  }

  // ...
  private ndkImplement = () => {
    this.getUIContext().getRouter().pushUrl({ url: 'pages/NDKImplementPage' }, router.RouterMode.Standard)
  }
}

// pages/NDKImplementIndexPage.ets
// 画中画功能演示界面
import testNapi, {PiPConfig} from 'libentry.so';
import { PiPWindow } from '@kit.ArkUI';
import { AVPlayer } from '../model/NDKAVPlayer';
import { Logger } from '../util/LogUtil';

const TAG = 'PipMain';
export enum PiPControlGroup {
  VIDEO\_PLAY\_VIDEO\_PREVIOUS\_NEXT = 101,
  VIDEO\_PLAY\_FAST\_FORWARD\_BACKWARD = 102,
  VIDEO\_CALL\_MICROPHONE\_SWITCH = 201,
  VIDEO\_CALL\_HANG\_UP\_BUTTON = 202,
  VIDEO\_CALL\_CAMERA\_SWITCH = 203,
  VIDEO\_CALL\_MUTE\_SWITCH = 204,
  VIDEO\_MEETING\_HANG\_UP\_BUTTON = 301,
  VIDEO\_MEETING\_CAMERA\_SWITCH = 302,
  VIDEO\_MEETING\_MUTE\_SWITCH = 303,
  VIDEO\_MEETING\_MICROPHONE\_SWITCH = 304,
  VIDEO\_LIVE\_VIDEO\_PLAY\_PAUSE = 401,
  VIDEO\_LIVE\_MUTE\_SWITCH = 402,
}

@Entry
@Component
struct NDKImplementIndexPage {
  @State message: string = 'Hello World';
  mXComponentController: XComponentController | null = new XComponentController();
  private controllerId: number = -1;
  private contentWidth: number = 1920;
  private contentHeight: number = 1080;
  private pipType: PiPWindow.PiPTemplateType = PiPWindow.PiPTemplateType.VIDEO\_PLAY;
  private pipControlGroups: Array<PiPControlGroup> = \[\];
  player?: AVPlayer;
  surfaceId = '';

  changeSurface = (surfaceId: string) => {
    if(this.player) {
      this.player.setSurfaceId(surfaceId);
      return;
    }
    Logger.info(\`\[${TAG}\] change surface failed\`);
  }

  private onStateChange = (state: PiPWindow.PiPState) => {
    switch(state) {
      case PiPWindow.PiPState.ABOUT\_TO\_START:
        Logger.info(\`\[${TAG}\] ABOUT\_TO\_START\`);
        break;
      case PiPWindow.PiPState.STARTED:
        Logger.info(\`\[${TAG}\] STARTED\`);
        break;
      case PiPWindow.PiPState.ABOUT\_TO\_STOP:
        Logger.info(\`\[${TAG}\] ABOUT\_TO\_STOP\`);
        break;
      case PiPWindow.PiPState.STOPPED:
        if (this.mXComponentController) {
          this.changeSurface(this.mXComponentController?.getXComponentSurfaceId());
        }
        Logger.info(\`\[${TAG}\] STOPPED\`);
        break;
      case PiPWindow.PiPState.ABOUT\_TO\_RESTORE:
        this.changeSurface(this.surfaceId);
        Logger.info(\`\[${TAG}\] ABOUT\_TO\_RESTORE\`);
        break;
      case PiPWindow.PiPState.ERROR:
        Logger.info(\`\[${TAG}\] ERROR\`);
        break;
      default:
        break;
    }
  }


  build() {
    RelativeContainer() {
      Row({ space: 20 }) {

        Button('更换模板')
          .bindMenu(\[
            {
              value: '视频',
              action: () => {
                this.pipType = PiPWindow.PiPTemplateType.VIDEO\_PLAY;
                this.pipControlGroups = \[PiPControlGroup.VIDEO\_PLAY\_VIDEO\_PREVIOUS\_NEXT\];
              }
            },
            {
              value: '通话',
              action: () => {
                this.pipType = PiPWindow.PiPTemplateType.VIDEO\_CALL;
                this.pipControlGroups = \[PiPControlGroup.VIDEO\_CALL\_HANG\_UP\_BUTTON,
                  PiPControlGroup.VIDEO\_CALL\_CAMERA\_SWITCH, PiPControlGroup.VIDEO\_CALL\_MICROPHONE\_SWITCH\];
              }
            },
            {
              value: '会议',
              action: () => {
                this.pipType = PiPWindow.PiPTemplateType.VIDEO\_MEETING;
                this.pipControlGroups = \[PiPControlGroup.VIDEO\_MEETING\_MICROPHONE\_SWITCH,
                  PiPControlGroup.VIDEO\_MEETING\_HANG\_UP\_BUTTON,
                  PiPControlGroup.VIDEO\_MEETING\_CAMERA\_SWITCH\];
              }
            },
            {
              value: '直播',
              action: () => {
                this.pipType = PiPWindow.PiPTemplateType.VIDEO\_LIVE;
                this.pipControlGroups = \[PiPControlGroup.VIDEO\_LIVE\_VIDEO\_PLAY\_PAUSE,
                  PiPControlGroup.VIDEO\_LIVE\_MUTE\_SWITCH\];
              }
            }
          \])
      }
      .size({ width: '100%', height: 60 })
      .backgroundColor('#DDDDDD')
      .justifyContent(FlexAlign.SpaceAround)
      .alignRules({
        top: { anchor: '\_\_container\_\_', align: VerticalAlign.Top },
        middle: { anchor: '\_\_container\_\_', align: HorizontalAlign.Center }
      })
      .id('pip\_type\_control')
      XComponent({
        type: XComponentType.SURFACE,
        controller: this.mXComponentController
      })
        .onLoad(() => {
          if (this.mXComponentController) {
            this.surfaceId = this.mXComponentController.getXComponentSurfaceId();
          }
          this.player = new AVPlayer(this.surfaceId, 1);
          this.player.avPlayerFdSrc();
        })
        .onDestroy(() => {
          Logger.info(\`\[${TAG}\] XComponent onDestroy\`);
        })
        .size({ width: '100%', height: '800px' })
        .margin({ top: 10 })
        .backgroundColor('#888888')
        .alignRules({
          bottom: { anchor: '\_\_container\_\_', align: VerticalAlign.Center },
          middle: { anchor: '\_\_container\_\_', align: HorizontalAlign.Center }
        })
        .id('x\_component')
        .size({ width: '100%', height: '800px' })
      Row({ space: 0 }) {
        Button('创建画中画')
          .onClick(() => {
            let windowId: number | undefined = AppStorage.get('windowId');
            let config: PiPConfig = {
              mainWindowId: windowId as number,
              pipTemplateType: this.pipType,
              width: this.contentWidth,
              height: this.contentHeight,
              controlGroup: this.pipControlGroups
            }
            this.controllerId = testNapi.createPip(config);
            testNapi.registerStartPip(this.controllerId, this.changeSurface);
            testNapi.registerLifecycleListener(this.controllerId, this.onStateChange);
          })
        Button('开启画中画')
          .onClick(() => {
            testNapi.startPip(this.controllerId);
          })
      }
      .size({ width: '100%', height: 60 })
      .alignRules({
        top: { anchor: 'x\_component', align: VerticalAlign.Bottom },
        left: { anchor: '\_\_container\_\_', align: HorizontalAlign.Start }
      })
      .id('pip\_control')
      Row({ space: 0 }) {
        Button('关闭画中画')
          .onClick(() => {
            testNapi.stopPip(this.controllerId);
          })
        Button('删除控制器')
          .onClick(() => {
            testNapi.deletePip(this.controllerId);
          })
      }
      .size({ width: '100%', height: 60 })
      .alignRules({
        top: { anchor: 'pip\_control', align: VerticalAlign.Bottom },
        left: { anchor: '\_\_container\_\_', align: HorizontalAlign.Start }
      })
    }
    .size({ width: '100%', height: '100%' })
  }
}

以上示例代码对应的示意图如下所示：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/3d/v3/MGne5rpURvmkn2Xgrjwbug/zh-cn_image_0000002625107897.gif?HW-CC-KV=V1&HW-CC-Date=20260612T073325Z&HW-CC-Expire=86400&HW-CC-Sign=8220D023C3654EA58DAC2C8CA03C2E8CE4983C1679FAB1C8895A6F4A65F4ABBE)