鸿蒙VR视频播放库——md360player

md360player库通过 OpenGL ES 实现 360° 视频的实时渲染,支持触摸与陀螺仪交互,并提供普通与 VR 两种显示模式。

仓库地址:

https://ohpm.openharmony.cn/#/cn/detail/@ohos%2Fmd360player

代码地址:

https://gitee.com/openharmony-tpc-incubate/md360player

整体架构

采用三层架构:

  1. 上层(ArkTS):VideoPlayerPage.ets、MDVRLibrary.ets - 业务逻辑与 UI
  2. 中间层(NAPI):md360player_napi.cc - TypeScript 与 C++ 的桥接
  3. 底层(C++):md_renderer.cc、md_vr_library.h - OpenGL 渲染核心

核心实现原理

1. 视频解码与纹理绑定

VideoPlayerPage.ets Lines 893-1004

TypeScript 复制代码
async initVR(surfaceId: string) {

    let videoFilePath: string = this.videoUrl;

    

    // 如果没有指定 URL,尝试从 rawfile 加载

    if (!videoFilePath) {

      try {

        const context = getContext() as common.UIAbilityContext;

        const resourceManager = context.resourceManager;

        

        const rawFile = await resourceManager.getRawFileContent('test360video.mp4');

        const filesDir = context.filesDir;

        const targetPath = `${filesDir}/test360video.mp4`;

        

        const writeFile = fs.openSync(targetPath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY | fs.OpenMode.TRUNC);

        fs.writeSync(writeFile.fd, rawFile.buffer);

        fs.closeSync(writeFile);

        

        videoFilePath = targetPath;

        console.log('VideoPlayerPage: Video file copied from rawfile to:', videoFilePath);

      } catch (error) {

        console.error('VideoPlayerPage: Failed to load video from rawfile:', error);

        // 使用默认网络 URL

        videoFilePath = 'https://vd3.bdstatic.com/mda-pdc2kmwtd2vxhiy4/cae_h264/1681502407203843413/mda-pdc2kmwtd2vxhiy4.mp4';

      }

    }

    

    this.videoCallback = new VideoSurfaceCallback(videoFilePath);

    

    // 设置播放状态回调

    const playbackCallback = new PlaybackStateCallbackImpl(this);

    this.videoCallback.setStateCallback(playbackCallback);

    

    // 创建并保存 Context 实例,后续切换模式时使用同一个实例

    PerformanceMonitor.measure('new Context()', () => {

      this.vrContext = new Context();

    });

    

    if (!this.videoCallback) {

      throw new Error('videoCallback is null');

    }

    

    // 拆分链式调用,对每个方法进行耗时统计

    let builder = PerformanceMonitor.measure('MDVRLibrary.with()', () => {

      return MDVRLibrary.with(this.vrContext!);

    });

    

    builder = PerformanceMonitor.measure('builder.displayMode()', () => {

      return builder.displayMode(MDVRLibrary.DISPLAY_MODE_NORMAL);

    });

    

    builder = PerformanceMonitor.measure('builder.interactiveMode()', () => {

      return builder.interactiveMode(MDVRLibrary.INTERACTIVE_MODE_TOUCH); // 默认使用触摸模式

    });

    

    builder = PerformanceMonitor.measure('builder.asVideo()', () => {

      return builder.asVideo(this.videoCallback!);

    });

    

    this.vrLibrary = PerformanceMonitor.measure('builder.build()', () => {

      return builder.build(this.xComponentController);

    });

    

    // 初始化触摸手势状态

    this.updateTouchGestureState(MDVRLibrary.INTERACTIVE_MODE_TOUCH);

    

    PerformanceMonitor.measure('vrLibrary.onSurfaceReady()', () => {

      this.vrLibrary!.onSurfaceReady(surfaceId);

    });

    

    // 初始化视口(延迟执行,确保 NAPI 已准备好)

    setTimeout(() => {

      this.initViewport();

    }, 500);

    

    // 从 C++ 侧获取 NativeImage 的 surfaceId

    const maxRetries = 30;

    let retryCount = 0;

    

    console.log('VideoPlayerPage: Starting to get video surfaceId, maxRetries:', maxRetries);

    

    const tryConnectVideo = async () => {

流程:

  • 使用 AVPlayer 解码视频
  • C++ 层创建 NativeImage 获取 surfaceId
  • 将 surfaceId 绑定到 AVPlayer,视频帧通过 Surface 传递到 C++ 层

2. OpenGL 渲染核心

md_renderer.cc Lines 56-90

TypeScript 复制代码
  gl_Position = u_MVPMatrix * a_Position;

    }

)";

const char* VR_FRAGMENT_SHADER = R"(

    #extension GL_OES_EGL_image_external : require

    precision mediump float;

    varying vec2 vTexCoord;

    uniform samplerExternalOES u_Texture;

    uniform vec4 u_DistortionParams;

    vec2 BarrelDistortion(vec2 texCoord) {

        vec2 coords = texCoord - vec2(0.5);

        float rSq = coords.x * coords.x + coords.y * coords.y;

        vec2 distorted = coords * (u_DistortionParams.x + u_DistortionParams.y * rSq);

        return distorted + vec2(0.5);

    }

    void main() {

        vec2 distortedCoord = vTexCoord;

        

        // 只有当u_DistortionParams.y不为0时才应用桶形畸变

        if (u_DistortionParams.y != 0.0) {

            distortedCoord = BarrelDistortion(vTexCoord);

        }

        

        // 边界处理

        if (distortedCoord.x < 0.0 || distortedCoord.x > 1.0 || 

            distortedCoord.y < 0.0 || distortedCoord.y > 1.0) {

            gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);

        } else {

            gl_FragColor = texture2D(u_Texture, distortedCoord);

        }

    }

)";

原理:

  • 球面投影:将 360° 视频纹理映射到球体(半径约 18.0)
  • 相机位于球心,通过 MVP 矩阵控制视角
  • 使用 samplerExternalOES 采样视频纹理
  • 支持桶形畸变校正(VR 模式)

3. 交互控制

触摸控制

MDVRLibrary.ets Lines 471-473

TypeScript 复制代码
  onDrag(distanceX: number, distanceY: number): void {

    this.mInteractiveModeManager?.handleDrag(Math.floor(distanceX), Math.floor(distanceY));

  }
陀螺仪控制

MDVRLibrary.ets Lines 272-276

TypeScript 复制代码
 public updateGyroData(x: number, y: number, z: number, w: number): void {

    if (this.mNapi && typeof this.mNapi.updateGyroData === 'function') {

      this.mNapi.updateGyroData(x, y, z, w);

    }

  }
  • 触摸:计算拖拽距离,更新 MVP 矩阵的旋转
  • 陀螺仪:使用旋转矢量(四元数)更新相机方向

4. 显示模式

普通模式
  • 单屏渲染,全屏显示
VR 模式

md_renderer.cc Lines 636-745

TypeScript 复制代码
int RenderVRStereo() {

        if (surface_width_ <= 0 || surface_height_ <= 0) {

            return MD_ERR;

        }

        

        if (!vr_shaders_initialized_) {

            

   ...

        int surfaceHeight = surface_height_;

        

        if (surfaceWidth <= 0 || surfaceHeight <= 0) {

            // 使用默认值

            surfaceWidth = 1920;

            surfaceHeight = 1080;

        }

        

        // VR模式:将屏幕水平分成两半

        // 注意:这里需要确保每个眼睛的视口精确分割屏幕

        width = surfaceWidth / 2;

        height = surfaceHeight;

        x = (eye == LEFT_EYE) ? 0 : width;

        y = 0;

        

        // 如果屏幕宽度是奇数,需要调整第二个眼睛的宽度

        if (eye == RIGHT_EYE && surfaceWidth % 2 != 0) {

            // 第二个眼睛的宽度需要增加1个像素,确保覆盖整个屏幕

            width = surfaceWidth - (surfaceWidth / 2);

            x = surfaceWidth / 2; // 从中间开始

        }

    }
  • 分屏渲染:屏幕水平分为左右两半
  • 立体效果:通过 IPD(瞳距)偏移左右眼投影矩阵
  • 桶形畸变:适配 VR 眼镜的光学特性

5. 渲染循环

MDVRLibrary.ets Lines 278-283

TypeScript 复制代码
  private startLoop(): void {

    if (this.mTimerId !== -1) clearInterval(this.mTimerId);

    this.mTimerId = setInterval(() => {

      this.mRenderer?.onDrawFrame(null!);

    }, 16);

  }
  • 约 60fps(16ms 间隔)
  • 每帧更新 MVP 矩阵并渲染

数据流向

AVPlayer

↓ (Surface)

C++ NativeImage

↓ (OpenGL Texture)

球面投影渲染

↓ (MVP矩阵变换)

XComponent Surface

屏幕显示

关键技术点

  1. 球面投影:将 360° 视频映射到球体表面
  2. MVP 矩阵:控制视角旋转
  3. 双模式渲染:普通单屏与 VR 分屏
  4. 多交互方式:触摸与陀螺仪
相关推荐
FF2501_940228587 小时前
HarmonyOS应用开发实战:猫猫大作战-在排行榜和历史记录查询中,排序是必不可少的操作【apple_product_name】
华为·harmonyos·鸿蒙
weixin_495248408 小时前
短剧视频翻译配音指南:古装、甜宠与悬疑如何本地化?
音视频
禹亮科技9 小时前
制造业100㎡跨国技术评审会议室音视频整体解决方案(亿联+思必驰+讯飞AI落地实践)
人工智能·音视频·视频会议系统集成
潜创微科技9 小时前
HDMI 分配器芯片方案商 IT66630 有源分配芯片方案
音视频
不爱记笔记10 小时前
多模态AI如何理解视频内容?从视觉、语音到语义的三层技术拆解
人工智能·自然语言处理·nlp·音视频·多模态
程序员黑豆11 小时前
鸿蒙应用开发:@Provider 与 @Consumer 跨组件双向同步详解
前端·harmonyos
程序员黑豆12 小时前
鸿蒙应用开发之跨组件传参:@Provide 与 @Consume 跨层级数据同步详解
前端·harmonyos
论迹复利12 小时前
Sniff 省电模式参数调优——续航与音频稳定性平衡实操
音视频·sniff·bt·功耗、
懿路向前12 小时前
【HarmonyOS学习笔记】2026-07-30 | 小艺开放平台智能体接入实战
笔记·学习·harmonyos
大锅盖112 小时前
HarmonyOS ArkTS 的新手练手样例:从 Text 和 Button 开始,做一个会变化的计数页面
华为·harmonyos