在 HarmonyOS NEXT 应用开发中,视频处理是构建社交、内容创作等场景应用的核心能力。本文将从视频选择播放控制上传服务三个维度,结合实际代码案例,系统讲解如何构建完整的视频处理链路。

HarmonyOS NEXT 的一大设计理念是隐私安全优先——系统提供了 Picker 组件,允许应用在不申请相册或存储权限的情况下,让用户主动选择并授权访问特定媒体资源。这一机制贯穿视频选择与播放的全流程。


一、视频选择:从相册获取视频文件

在 HarmonyOS NEXT 中,通过 photoAccessHelper.PhotoViewPicker 即可从系统相册选择视频,无需申请 ohos.permission.READ_MEDIA 权限。

1.1 基础选择流程

以下代码演示如何从相册中选择单个视频,并获取其 URI 路径:

typescript

import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { BusinessError } from '@kit.BasicServicesKit';

async function selectVideoFromAlbum(): Promise<string> {
  try {
    // 1. 配置选择参数
    const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
    // 仅显示视频文件
    photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE;
    // 单次最多选择 1 个
    photoSelectOptions.maxSelectNumber = 1;

    // 2. 创建选择器并拉起系统相册
    const photoPicker = new photoAccessHelper.PhotoViewPicker();
    const result: photoAccessHelper.PhotoSelectResult = await photoPicker.select(photoSelectOptions);
    
    // 3. 返回选中视频的 URI
    if (result.photoUris && result.photoUris.length > 0) {
      console.info('选中的视频 URI:', result.photoUris[0]);
      return result.photoUris[0];
    }
  } catch (error) {
    const err = error as BusinessError;
    console.error(`选择视频失败: ${err.code}, ${err.message}`);
  }
  return '';
}

1.2 获取视频元信息

当选中的 URI 需要进一步获取视频的时长、宽高等元数据时,可通过 photoAccessHelper.getAssets 配合查询条件实现:

typescript

import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { dataSharePredicates } from '@kit.ArkData';

async function getVideoMetadata(uri: string): Promise<photoAccessHelper.PhotoAsset | undefined> {
  try {
    const context = getContext(this) as common.UIAbilityContext;
    const phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
    
    const predicates = new dataSharePredicates.DataSharePredicates();
    predicates.equalTo('uri', uri);
    
    const fetchOptions: photoAccessHelper.FetchOptions = {
      fetchColumns: [
        photoAccessHelper.PhotoKeys.WIDTH,
        photoAccessHelper.PhotoKeys.HEIGHT,
        photoAccessHelper.PhotoKeys.DURATION,
        photoAccessHelper.PhotoKeys.TITLE
      ],
      predicates: predicates
    };
    
    const fetchResult = await phAccessHelper.getAssets(fetchOptions);
    const asset = await fetchResult.getFirstObject();
    
    console.info('视频时长(ms):', asset.get(photoAccessHelper.PhotoKeys.DURATION));
    console.info('视频宽高:', asset.get(photoAccessHelper.PhotoKeys.WIDTH), 'x', asset.get(photoAccessHelper.PhotoKeys.HEIGHT));
    return asset;
  } catch (error) {
    console.error('获取元数据失败:', JSON.stringify(error));
  }
  return undefined;
}

1.3 多选与预览场景

若需实现类似朋友圈的多图/多视频发布功能,可配置 maxSelectNumber 并监听选择回调。开源案例 publishmultimediaupdates 展示了完整的懒加载列表与媒体发布流程:

typescript

// 支持多选,最大可选 9 个媒体文件
photoSelectOptions.maxSelectNumber = 9;
photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_VIDEO_TYPE; // 图片和视频混合

二、视频播放:从基础组件到专业 SDK

HarmonyOS NEXT 提供了两种视频播放方案:系统 Video 组件(轻量、快速集成)和专业播放器 SDK(如阿里云播放器,支持加密、倍速、清晰度切换等高级功能)。

2.1 使用系统 Video 组件

Video 组件封装了完整的播放控制 UI 和生命周期事件,适用于大多数常规场景:

typescript

@Entry
@Component
struct VideoPlayerPage {
  @State videoSrc: string = ''; // 从相册选择的视频 URI
  @State isAutoPlay: boolean = true;
  @State showControls: boolean = true;
  private controller: VideoController = new VideoController();

  build() {
    Column() {
      Video({
        src: this.videoSrc,
        controller: this.controller
      })
        .width('100%')
        .aspectRatio(16/9)
        .autoPlay(this.isAutoPlay)
        .controls(this.showControls)
        .loop(false)
        .onPrepared((e?: DurationObject) => {
          console.info('视频准备完成,时长:', e?.duration);
        })
        .onUpdate((e?: TimeObject) => {
          // 播放进度更新
        })
        .onFinish(() => {
          console.info('播放结束');
        })
        .onError(() => {
          console.error('播放出错');
        });

      Row() {
        Button('播放').onClick(() => this.controller.start());
        Button('暂停').onClick(() => this.controller.pause());
        Button('跳转10s').onClick(() => this.controller.setCurrentTime(10, SeekMode.Accurate));
      }
    }
  }
}

组件限制:系统 Video 组件暂不支持直接控制音量,若需音量调节,建议使用 AVPlayer 或 AudioRenderer 进行开发。

2.2 使用阿里云播放器 SDK(高级场景)

对于需要支持加密视频播放(VidAuth/VidSts)、多清晰度切换倍速播放的商业应用,可集成阿里云播放器 SDK。

URL 直接播放

typescript

import { AliPlayerFactory, UrlSource } from 'premierlibrary';

const aliyunPlayer = AliPlayerFactory.createAliPlayer(getContext(), '');
const urlSource = new UrlSource();
urlSource.setUri('https://example.com/video.mp4');
aliyunPlayer.setUrlDataSource(urlSource);
aliyunPlayer.setAutoPlay(true);
aliyunPlayer.prepare();

VidAuth 加密播放(推荐)

typescript

import { VidAuth, VidPlayerConfigGen } from 'premierlibrary';

const vidAuthSource = new VidAuth();
vidAuthSource.setVid('your-video-id');       // 视频 ID
vidAuthSource.setPlayAuth('your-play-auth'); // 播放凭证
aliyunPlayer.setVidAuthDataSource(vidAuthSource);

播放控制与监听

typescript

// 倍速播放
aliyunPlayer.setRate(1.5);

// 跳转
aliyunPlayer.seekTo(30000); // 30秒

// 状态监听
aliyunPlayer.setOnPreparedListener(() => {
  console.info('准备完成,开始播放');
  aliyunPlayer.start();
});

aliyunPlayer.setOnErrorListener((error) => {
  console.error('播放错误:', error);
});

2.3 进阶:视频剪辑与压缩

在实际发布场景中,往往需要对视频进行剪辑或压缩以节省流量和存储。HarmonyOS 生态提供了基于 FFmpeg 的解决方案(参考开源案例 videotrimmer):通过 MP4Parser.getFrameAtTimeRange 获取关键帧截图,利用 FFmpeg 命令对沙箱中的视频进行裁剪和压缩,最后通过 request.agent 上传处理后的视频。


三、视频上传:从沙箱到服务器

视频上传涉及两个核心步骤:将相册视频拷贝至应用沙箱(Picker 返回的 URI 是临时只读权限),以及通过 request 模块进行断点续传

3.1 拷贝视频到沙箱缓存目录

Picker 返回的 URI 为临时授权访问,若需持久化操作(压缩、上传),应先将文件拷贝至应用沙箱:

typescript

import { fileIo as fs } from '@kit.CoreFileKit';

async function copyVideoToCache(sourceUri: string): Promise<string> {
  const context = getContext(this);
  const cacheDir = context.cacheDir;
  const fileName = sourceUri.split('/').pop() || 'temp_video.mp4';
  const destPath = `${cacheDir}/${fileName}`;
  
  try {
    // 以只读方式打开源文件
    const sourceFile = fs.openSync(sourceUri, fs.OpenMode.READ_ONLY);
    // 创建目标文件(写入)
    const destFile = fs.openSync(destPath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY);
    
    // 分片拷贝
    const buffer = new ArrayBuffer(8192);
    let readLen = 0;
    while ((readLen = fs.readSync(sourceFile.fd, buffer)) > 0) {
      fs.writeSync(destFile.fd, buffer.slice(0, readLen));
    }
    
    fs.closeSync(sourceFile.fd);
    fs.closeSync(destFile.fd);
    console.info('视频已拷贝至沙箱:', destPath);
    return destPath;
  } catch (error) {
    console.error('拷贝失败:', JSON.stringify(error));
  }
  return '';
}

3.2 使用 request.uploadFile 上传

request.uploadFile 支持 multipart/form-data 格式上传,可同时携带文本字段(标题、描述)和文件字段(视频文件、封面图)。

关键注意事项

  • 文件路径必须位于沙箱 cache 目录,且使用 internal://cache/ 前缀

  • 推荐监听 progress 事件展示上传进度,监听 complete 事件处理完成回调

typescript

import { request } from '@kit.BasicServicesKit';
import { BusinessError } from '@kit.BasicServicesKit';

function uploadVideo(
  videoPath: string,          // 沙箱中的视频路径,如 /data/.../cache/video.mp4
  coverPath: string,          // 封面图路径
  title: string,
  desc: string,
  uploadUrl: string,
  callback: (success: boolean, msg: string) => void
) {
  // 构建内部缓存 URI(必须使用 internal://cache/ 前缀)
  const videoFileName = videoPath.split('/').pop() || 'video.mp4';
  const videoCacheUri = `internal://cache/${videoFileName}`;
  const coverCacheUri = `internal://cache/${coverPath.split('/').pop()}`;
  
  const config: request.UploadConfig = {
    method: 'POST',
    url: uploadUrl,
    header: { 'Content-Type': 'multipart/form-data' },
    files: [
      {
        filename: videoFileName,
        name: 'video',
        uri: videoCacheUri,
        type: 'video/mp4'
      },
      {
        filename: coverPath.split('/').pop() || 'cover.jpg',
        name: 'cover',
        uri: coverCacheUri,
        type: 'image/jpeg'
      }
    ],
    data: [
      { name: 'title', value: title },
      { name: 'description', value: desc },
      { name: 'timestamp', value: Date.now().toString() }
    ]
  };

  request.uploadFile(getContext(this), config)
    .then((task: request.UploadTask) => {
      // 监听上传进度
      task.on('progress', (progress: request.UploadProgress) => {
        const percent = (progress.uploadedSize / progress.totalSize) * 100;
        console.info(`上传进度: ${percent.toFixed(1)}%`);
      });
      
      // 监听完成事件
      task.on('complete', () => {
        console.info('上传完成');
        callback(true, '上传成功');
      });
    })
    .catch((err: BusinessError) => {
      console.error('上传失败:', err.message);
      callback(false, err.message);
    });
}

3.3 使用专业上传 SDK(火山引擎)

对于视频点播场景,火山引擎提供了 HarmonyOS NEXT 上传 SDK,支持分片上传、断点续传、自动抽帧等高级功能:

typescript

import { TTUploaderUtil, TTVideoUploader } from '@bytedance/bduploader';

// 1. 初始化 SDK(应用启动时执行一次)
TTUploaderUtil.init(getContext(this).getApplicationContext());

// 2. 创建上传实例
const videoUploader = new TTVideoUploader();
videoUploader.setPathName(videoFilePath);
videoUploader.setSpaceName('your-space');
videoUploader.setTopAccessKey(accessKey);
videoUploader.setTopSecretKey(secretKey);
videoUploader.setTopSessionToken(stsToken);

// 3. 设置回调监听
const listener = new UploaderVideoListener();
videoUploader.setVideoInfoListener(listener);

// 4. 开始上传
await videoUploader.start();

// 监听类示例
class UploaderVideoListener implements VideoInfoListener {
  async onNotify(what: number, info: BDVideoInfo): Promise<void> {
    if (what === TTVideoUploadNotifier.MsgIsComplete) {
      console.info('上传完成');
    } else if (what === TTVideoUploadNotifier.MsgIsUpdateProgress) {
      // 进度更新
    }
  }
}

四、完整实战:视频发布流程

将以上三部分串联,一个完整的视频发布场景应包含以下步骤:

  1. 选择:通过 PhotoViewPicker 让用户从相册选择视频

  2. 预览:使用 Video 组件或播放器 SDK 进行播放预览

  3. 处理:将视频拷贝至沙箱 cache 目录(若需剪辑/压缩则在此步骤处理)

  4. 上传:调用 request.uploadFile 或专业上传 SDK,展示进度条

  5. 完成:上传成功后更新 UI,释放资源

完整代码框架可参考开源案例 publishmultimediaupdates(多媒体发布)和 videotrimmer(视频剪辑上传),已在 Gitee 鸿蒙案例库中提供。


五、注意事项与最佳实践

维度 建议
权限管理 优先使用 Picker 组件,避免申请敏感权限;相机拍摄需动态授权 ohos.permission.CAMERA
性能优化 大文件上传使用分片策略;列表视频使用懒加载(LazyForEach)和缓存(cachedCount
沙箱路径 上传时需将文件置于 cache 目录,URI 前缀为 internal://cache/
播放器选型 常规场景选系统 Video 组件;加密、多清晰度、商业需求选阿里云/火山引擎 SDK
错误处理 务必监听 onError(播放)和 catch(上传),给用户友好提示
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐