You need to enable JavaScript to run this app.
导航
集成 SDK
最近更新时间:2024.11.27 14:08:55首次发布时间:2024.08.16 14:11:59

本文为您介绍如何将 HarmonyOS NEXT 点播 SDK 集成至您的项目中。

功能详情

当前版本的主要功能如下表所示。

功能

说明

播放协议与格式

音视频格式

支持 HLS、MP4 等点播场景常见的视频格式。

DirectUrl 播放

支持以 DirectUrl 方式播放本地视频和网络视频。

H.264 编码格式

支持播放 H.264 编码协议的视频流。

H.265 编码格式

支持播放 H.265 编码协议的视频流。

播放控制

基础播放控制

支持开始、结束、暂停和恢复等播放控制功能。

Seek

支持拖动到指定位置。

重播

支持视频播放结束后手动触发重播。

续播

支持设置续播起播时间点。

循环播放

支持视频播放结束后自动重播。

倍速播放

支持变速播放,与此同时音频变速不变调。

清晰度切换

支持用户流畅无卡顿地切换视频的多路清晰度流。

音频效果

音量调节

支持调用系统接口调节观看视频的音量。

静音

支持开启和关闭静音。

视频效果

填充模式

默认为等比例填充。若视频比例和容器比例不一致,可能存在黑边

播放性能

预渲染

在播放当前视频时,提前创建播放器并对下一个视频进行解码和渲染,同时可将预渲染的首帧用作视频封面,提前展示给用户。

多实例

支持在同一界面添加多个播放器并同时播放。

播放失败重试

播放失败时自动重试。

事件回调

支持对播放状态回调、首帧回调、播放完成或失败回调。

视频安全

Referer 黑白名单

支持通过播放请求中携带的 Referer 字段识别请求来源,以黑名单或白名单方式对请求来源进行控制。

质量上报

日志上报

支持上报播放器 SDK 日志,统计点播视频播放的埋点数据。

播放数据大盘

支持观测播放量、播放质量等大盘数据。

集成准备

环境要求

类别

说明

开发环境

DevEco Studio(推荐使用最新版本)

兼容的最低 SDK 版本

"compatibleSdkVersion": "5.0.0(12)"

ABI 兼容性

架构要求:arm64

创建应用并获取 License

添加仓库

在项目根目录下创建 .ohpmrc 文件并配置 OpenHarmony 三方库中心仓和火山引擎仓库地址。

registry=https://ohpm.openharmony.cn/ohpm/,https://artifact.bytedance.com/repository/byted-ohpm/

示意图如下:

添加 SDK 依赖

entry 下的 oh-package.json5 中添加 SDK 依赖:

说明

请参见发布历史获取 SDK 最新版本号。

{
  "name": "entry",
  "version": "1.0.0",
  "description": "Please describe the basic information.",
  "main": "",
  "author": "",
  "license": "",
  "dependencies": {
     // 将 xxx 替换成最新版本号
    "@simplayer/simkit_api": "xxx",
  }
}

示意图如下:

声明权限

enrty 下的 module.json5 文件中声明权限:

"requestPermissions": [
  {
    // 网络权限,建议添加
    "name": "ohos.permission.INTERNET"
  },
  {
    // 获取 WIFI 信息,如获取 mac,建议添加
    "name": "ohos.permission.GET_WIFI_INFO"
  },
  { 
    // 获取网络信息,建议添加
    "name": "ohos.permission.GET_NETWORK_INFO"
  },
  {
    // 资产持久化,建议添加
    "name": "ohos.permission.STORE_PERSISTENT_DATA"
  },
],

快速开始

开启日志调试

初始化点播 SDK 之前,开启日志,便于调试和排查问题。

注意

线上版本请务必关闭日志,减少性能开销。

VodEnv.openLog()

初始化 SDK

初始化 SDK 并传入 License 文件。这是全局接口,app 生命周期内仅需调用一次。

注意

License 文件需由应用服务端下发,定期更新,否则 License 过期后您将无法使用点播 SDK。更多信息,请见如何处理 License 相关错误?

// 初始化 applog,需传入您在视频点播控制台获取的 App ID
ApplogWrapper.intApplog(getContext(), 'your app id')
// 初始化 license 模块
VodEnv.init()
// 获取 license 见集成准备“创建应用并获取 License”部分
VodEnv.addLicenseFile('{"Signature":"A1VA/mZ63eEUKLZEs4mzMa8xLVqUHkEzeUdC........')
// 初始化 SDK
SimKitService.instance().init(new SimKitInitModel() ,getContext());

创建播放器

this.simPlayer = SimKitService.instance().createSimPlayer();

构造播放源

当前仅支持以 DirectUrl 方式播放。您需要将 url 参数设为视频播放地址。播放地址可以是第三方点播地址或视频点播服务生成的播放地址。此外,你还必须设置以下参数:

  • sourceID:播放源唯一标识,必须与视频源一一对应。sourceID 可以是您自己的视频管理系统中的唯一标识,也可以在 App 层自行生成一个 ID。此 ID 会用于点播 SDK 的日志上报、预渲染等功能中。
  • fileHash:作为缓存 key 使用,需能和视频资源文件一一对应,不带特殊字符,能作为文件名。可使用 URL 的 MD5 作为缓存 key。
// 播放源唯一标识
let sourceID = "source id";
// 视频 URL
let url = "http://www.example.com/h264.mp4";
// 缓存 key 
let fileHash = getMd5(url);

let urlModel = new PlayUrlModel([url]);
urlModel.setFileHash(fileHash)
let playRequest = new PlayRequest(sourceID, urlModel)

构造视图控件

build() {
  Stack() {
    // 构造视图控件并传入播放器和播放源
    SimVideoView({
      playRequest: this.playRequest,
      simPlayer: this.simPlayer,
    })
      .width('100%')
      .height('100%')
  }
  .width('100%')
  .height('100%')
}

开始播放

调用 play 开始播放。SDK 默认会边播边缓存。

this.simPlayer.play(playRequest, PlayOptions.create()
  .setEnableMdl(true));

说明

HLS 视频目前不支持边播边缓存。播放 HLS 视频时,setEnableMdl 需设置为 false

释放播放器

this.simPlayer.release(this.playRequest.getSourceID())

基础功能

播放回调

设置播放器回调监听

/**
  * 订阅播放回调。每次调用此接口,SDK 内部会创建新的 IPlayEventObserver 并返回
  * 同一次播放可订阅多次回调。使用完需解除订阅
  * @param sourceID 对应 IPlayRequest 的 sourceID
  * @param observerName Observer 命名标识,标记所在业务场景,方便后续异常定位
  * @returns
  */
subscribeObserver(sourceID:string, observerName: string): IPlayEventObserver

/**
  * 取消订阅,确保与订阅方法逻辑对称
  * @param sourceID 对应 IPlayRequest 的 sourceID
  * @param playEventObserver,需要解除订阅的 IPlayEventObserver
  */
unsubscribeObserver(sourceID:string, playEventObserver: IPlayEventObserver)

回调事件

export interface IPlayEventObserver {

  /**
   * 首帧回调
   */
  onRenderFirstFrame(callback: Callback<string | undefined>): IPlayEventObserver
  
  /**
   * prepare 完成回调
   */
  onPrepared(callback: Callback<string | undefined>): IPlayEventObserver

  /**
   * 开始播放回调
   */
  onPlaying(callback: Callback<string | undefined>): IPlayEventObserver

  /**
   * 调用暂停后回调
   */
  onPaused(callback: Callback<string | undefined>): IPlayEventObserver

  /**
   * 调用 stop 后回调
   */
  onStopped(callback: Callback<string | undefined>): IPlayEventObserver

  /**
   * 播放至结尾回调。不论用户是否开启循环播放,播放至结尾均触发此回调
   */
  onPlayEnd(callback: Callback<string | undefined>): IPlayEventObserver

  /**
   * 出现卡顿回调
   * @param start 为 true 表示开始卡顿;start 为 false 表示卡顿结束。
   */
  onBuffering(callback: (sourceID: string | undefined, start: boolean) => void): IPlayEventObserver

  /**
   * 播放器已经缓存的可播放进度
   * @param percent 百分比
   */
  onBufferingPercent(callback: (sourceID: string | undefined, percent: number) => void): IPlayEventObserver

  /**
   * 播放进度变化回调,默认间隔 1 秒回调,因用户操作 (seek) 产生进度变化 SDK 会立刻回调
   * @param currentDuration 当前播放进度,单位 ms
   */
  onTimeChange(callback: (sourceID: string | undefined, currentDuration: number) => void): IPlayEventObserver

  /**
   * Seek 完成回调
   * @param seekDoneTime Seek 到的位置,单位 ms。
   * 精准的播放位置需要通过 onTimeChange 获取,seek 回调的 time 仅代表完成用户某一次请求。
   */
  onSeekDone(callback: (sourceID: string | undefined, seekDoneTime: number) => void): IPlayEventObserver
  
  /**
   * 播放错误回调
   * @param error 错误信息,error.code 为错误码
   */
  onError(callback: (sourceID: string | undefined, error: BusinessError) => void): IPlayEventObserver
  
  /**
   * 视频宽高变化回调,仅系统播放器回调
   * @param width 视频宽度
   * @param height 视频高度
   */
  onVideoSizeChange(callback: (sourceID: string | undefined, width: number, height: number) => void): IPlayEventObserver
}

示例代码

// 订阅回调
this.playEventObserver = this.simPlayer
.subscribeObserver(this.playRequest.getSourceID(), 'FeedItemView')
  .onPrepared((sourceID) => {
  })
  .onPaused((sourceID) => {
  })
  .onPlaying((sourceID) => {
  })
  .onBuffering((sourceID, start) => {
  })
  .onTimeChange((sourceID, currentDuration) => {
  })
  .onBufferingPercent((sourceID, percent) => {
  })
  .onError((sourceID, error) => {
  })
  
// 结束播放时,取消订阅
this.simPlayer.unsubscribeObserver(this.playRequest.getSourceID(), this.playEventObserver)

暂停与恢复播放

// 暂停播放
this.simPlayer.pause(this.playRequest.getSourceID())
 
// 恢复播放
this.simPlayer.resume(this.playRequest.getSourceID())

Seek 到指定位置播放

// 演示 seek 到 1 秒的位置
this.simPlayer.seek(1000);

播放器静音

// 静音
this.simPlayer.setVolume(0);

// 取消静音
this.simPlayer.setVolume(1);

倍速播放

// 默认值为 1。支持取值:0.5, 1, 1.5, 2, 2.5, 3
this.simPlayer.setSpeed(2)

获取视频时长

// 单位为毫秒
let duration =  this.simPlayer.getDuration()

视频硬解

this.simPlayer.play(playRequest,
  PlayOptions.create()
    .setEnableHardwareDecode(true));

循环播放

// 循环播放默认关闭
this.simPlayer.play(playRequest,
  PlayOptions.create()
    .setLoop(true));

从指定时间起播

this.simPlayer.play(playRequest,
  PlayOptions.create()
     // 单位为毫秒,以下示例表示从 1 秒处起播
    .setInitialStartTimeMs(1000);

进阶功能

播放火山引擎私有加密视频

火山引擎视频点播私有加密方案采用火山引擎自研加密算法,安全级别高,能够便捷、高效、安全地保护您的音视频版权。更多信息,请见火山引擎私有加密方案介绍。点播 SDK 支持通过 DirectUrl 模式播放私有加密视频。应用服务端从视频点播服务获取用于解密的密钥 playAuth 并下发给点播 SDK,即可播放火山引擎私有加密视频。

// 播放源唯一标识,视频源与 sourceID 必须一一对应
let sourceID = "source id";
// 视频 URL
let url = "http://www.example.com/h264.mp4";
// 缓存 Key,如 URL 的 MD5 
let fileHash = getMd5(url);
// 从视频点播服务获取的用于解密的密钥,详见[获取播放信息](https://www.volcengine.com/docs/4/2918)
String playAuth = "l7wZ9Em+A/xxxxxxx";

let urlModel = new PlayUrlModel([url]);
urlModel.setFileHash(fileHash)
urlModel.setDecryptionKey(playAuth)
let playRequest = new PlayRequest(sourceID, urlModel)

// 播放
this.simPlayer.play(playRequest,
  PlayOptions.create()
    .setEnableMdl(true));

最佳实践

短视频场景预渲染

Feed 流页面

export struct SmallVideoFeed {
  
  // 2. Feed 流数据
  private data: FeedDataSource = new FeedDataSource();
  // 3. 记录当前页面 index
  @State currentIndex: number = 0
  // 4. 初始化短视频场景播放器
  private scenePlayer: IScene = SimKitService.instance().createScene('FeedIView')

  aboutToAppear(): void {
    let playRequests = new Array<Pair<IPlayRequest, PlayOptions>>()
    // 5. 设置播放列表,列表内容与 FeedDataSource 中一致
    this.scenePlayer.setPlayRequests(playRequests)
  }

  build() {
    // 1. 使用 Swiper 实现端视频 feed 流
    Swiper() {
      LazyForEach(this.data, (videoSource: VideoSource, index: number) => {
        TTPlayerItemView({
            sourceID: videoSource.source_id,
            playUrl: videoSource.play_url,
            scene: this.scenePlayer,
            index: index,
            currentIndex: this.currentIndex
        })
          .width('100%')
          .height("100%")
      }, (videoSource: VideoSource) => videoSource.source_id)
    }
    .vertical(true)
    .loop(false)
    .cachedCount(0)
    .displayCount(1)
    .width('100%')
    .height('100%')
    .indicator(false)
    .onChange((index) => {
      // 6. 页面上下滑动,更新 scenePlayer index
      this.currentIndex = index
      this.scenePlayer.videoSelected(index)
    })
  }
}

Item 播放页面

export struct TTPlayerItemView {
  index: number = 0
  // 1. Feed 页面滑动,currentIndex 更新,会通知到 onIndexChange
  @Watch('onIndexChange') @Prop currentIndex: number = 0
  private sourceID: string = '';
  private playUrl: string = '';
  @State playRequest ?: IPlayRequest = new PlayRequestBuilder().build()
  private simPlayer?: ISimPlayer;
  private scene?: IScene;

  aboutToAppear() {
    this.playRequest = new PlayRequestBuilder()
      .setSourceID(this.sourceID)
      .setPlayAddr([this.playUrl])
      .build()

    // 2. 用场景播放器获取播放实例
    this.simPlayer = this.scene?.getSimPlayer();
    if (this.index == 0) {
      // 3. index 为 0 表示第一个视频,可直接播放并设置 scene index
      this.simPlayer.play(this.playRequest, PlayOptions.create());
      this.scene?.videoSelected(0)
    }
  }

  aboutToDisappear() {
    // 6. 画面退出,释放播放器
    this.simPlayer?.release(this.playRequest?.getSourceID())
  }

  build() {
    Stack() {
      SimVideoView({
        playRequest: this.playRequest,
        simPlayer: this.simPlayer,
      })
    }
  }

  onIndexChange() {
    // 4. 页面切换,根据 index 判断开始播放或停止播放。
    if (this.index === this.currentIndex) {
      this.simPlayer?.play(this.playRequest, PlayOptions.create());
    } else {
      this.simPlayer?.release(this.playRequest?.getSourceID())
    }
  }
}

常见问题

如何处理 License 相关错误?

当 License 过期或其他错误发生时,播放器会报错 -30001,无法正常使用。此时建议切换至鸿蒙系统播放器进行播放。系统播放器效果较差,因此建议及时更新 License,避免过期。

监听 License 错误

this.playEventObserver = this.simPlayer
.subscribeObserver(this.playRequest.getSourceID(), 'FeedItemView')
  .onError((sourceID, error) => {
      // License 校验失败
      if (error.codec == TTPlayerErrorCode.LICENSE_FAIL) {
      }
  })

方案 1:使用点播 SDK 封装的系统播放器

VodEnv.addLicenseFile(this.license)

.....
// addLicenseFile 后,检查 license,如不可用设置使用系统播放器播放。
if (!VodEnv.isLicenseAvailable()) {
  VodEnv.setUseOwnPlayer(false)
}

使用系统播放器时,视频拉伸变形铺满视图。您需根据视频宽高比自行调整 SimVideoView 宽高。

build() {
  Stack() {
    // 构造视图控件并传入播放器和播放源
    SimVideoView({
      playRequest: this.playRequest,
      simPlayer: this.simPlayer,
    })
      .width(this.playerWidth + 'px')
      .height(this.playerHeight + 'px')
  }
  .width('100%')
  .height('100%')
}

this.playEventObserver = this.simPlayer
.subscribeObserver(this.playRequest.getSourceID(), 'FeedItemView')
.onVideoSizeChange((sourceID, width, height) => {
    // 获取屏幕宽高比
    let displaySc = display.getDefaultDisplaySync().width / display.getDefaultDisplaySync().height
    // 获取视频宽高比
    let videoSc = width / height;
    if (videoSc > displaySc) {
      // 视图宽填满屏幕,高等比例适配留黑边
      this.playerWidth = display.getDefaultDisplaySync().width;
      this.playerHeight = this.playerWidth * height / width;
    } else {
      // 视图高填满屏幕,宽等比例适配留黑边
      this.playerHeight = display.getDefaultDisplaySync().height;
      this.playerWidth = this.playerHeight * width / height;
    }
})

方案 2:自行封装鸿蒙系统播放器

VodEnv.addLicenseFile(this.license)

.....
// addLicenseFile 后,检查 license,如不可用,使用您自行封装的系统播放器
if (!VodEnv.isLicenseAvailable()) {
  // 自行封装鸿蒙系统播放器
}

详见以下鸿蒙官方文档: