鸿蒙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. 多交互方式:触摸与陀螺仪
相关推荐
小白学大数据9 小时前
Java 异步爬虫高效获取小红书短视频内容
java·开发语言·爬虫·python·音视频
FL162386312910 小时前
基于yolo11实现的车辆实时交通流量进出统计与速度测量系统python源码+演示视频
开发语言·python·音视频
Jacen.L10 小时前
如何选择视频测试编码器? -- 编码器测试序列选择方法论
音视频·视频编解码
z日火10 小时前
腾讯云VOD AIGC视频生成工具 回调实现
aigc·音视频·腾讯云
EasyGBS10 小时前
EasyGBS打造变电站高效智能视频监控解决方案
网络·人工智能·音视频
gf132111112 小时前
制作卡点视频
数据库·python·音视频
sam.li13 小时前
鸿蒙HAR对外发布安全流程
安全·华为·harmonyos
sam.li13 小时前
鸿蒙APP安全体系
安全·华为·harmonyos
EasyCVR13 小时前
安防监控视频汇聚平台EasyCVR打造出入口匝道安全畅行智慧管理方案
安全·音视频
weixin_4368040713 小时前
在线音频音量调节器 - 免费批量调整声音大小与音量控制
音视频