跳到主要内容

使用NDK接口实现画中画功能开发(C/C++)

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

  • 从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在应用工程中的使用指导

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

// 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);
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
}

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,具体实现可参考视频播放

// 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() {
// 创建avPlayer实例对象
Logger.info('avPlayerFdSrc');
this.avPlayer = await media.createAVPlayer();

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

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;
}

async playNext() {
if (this.avPlayer === null) {
return;
}
this.jumpNext = true;
this.avPlayer?.stop();
}

play() {
if (this.state_ === 'paused') {
this.avPlayer?.play();
}
}

pause() {
if (this.state_ === 'playing') {
this.avPlayer?.pause();
}
}

stopAvPlayer() {
Logger.info('stopAvPlayer>>>')
if (!this.avPlayer) {
return;
}
this.avPlayer.stop();
Logger.info('stopping>>>');
this.avPlayer.reset();
}
}

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

// 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%' })
}
}

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