HarmonyOS 制作简易视频播放器过程详解
1. 介绍
播放视频的多媒体应用程序通常包含两个部分:
1.给定媒体源的播放器加载媒体资源,并通过Surface来进行画面渲染,将其呈现为视频。
2.具有传输控件的用户界面(UI),以承载播放器并显示播放器的状态。
本应用程序原理图如下图:
本篇Codelab将实现的内容
- 本篇Codelab旨在让开发者了解手机HarmonyOS应用开发,常用布局、典型控件、FA组件、媒体-视频、跨设备协同的体验以及从工程创建到代码和布局的编写,再到编译构建、部署和运行全过程。
- 您将构建一个基于HarmonyOS
- 类实现的应用程序,该应用程序功能为播放本地视频资源或从Internet获得的视频资源。效果图如下:
您将会学到什么
- 如何使用Player类播放视频
- 如何使用自定义控件来控制视频播放
- 如何添加并使用媒体事件的事件侦听器和回调
硬件要求
- 操作系统:Windows10 64位
- 内存:8GB及以上
- 硬盘:100GB及以上
- 分辨率:1280*800像素及以上
软件要求
- 安装DevEco Studio,详情请参考DevEco Studio下载。
- 设置Huawei DevEco Studio开发环境,Huawei DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境
- 生成秘钥和申请证书,详情请参考准备签名文件
技能要求
- 具备DevEco Studio中创建、构建和运行应用经验
- 熟悉Ability和AbilitySlice生命周期及使用PA/FA的能力
2. 代码结构
本篇Codelab只对核心代码进行讲解,对于完整代码,我们在参考提供下载方式。接下来我们会讲解整个工程的代码结构,如下图:
- api:视频播放状态改变及屏幕状态变化监听。
- constant:定义视频状态、进度条和控制器状态。
- factoty:创建SourceFactory类来根据视频来源创建视频源。
- manager:创建HmPlayerLifecycle来处理Player类的生命周期。
- view:创建PlayerLoading、SimplePlayerController类分别为视频加载状态及进度条控制类文件。
- HmPlayer:封装播放器的主要功能方法。
- slice:创建MainAbilitySlice、SimplePlayerAbilitySlice分别为进入应用的主程序页面和视频播放页面。
- utils:存放所有封装好的公共方法,如DateUtils,LogUtils等。
- resources:存放工程使用到的资源文件,其中resources\base\layout下存放xml布局文件;resources\base\media下存放视频文件。
- config.json:Ability声明及权限配置。
3. 创建视频播放业务逻辑
该应用程序可播放的视频格式包括mp4、mov、3gp、mkv,首先准备一份视频文件并复制到"resources/base/layout/media"文件目录。下面将会介绍视频列表布局及播放逻辑。
创建视频播放页面文件及布局
Step 1 - 创建simple_video_play_layout.xml布局文件展示视频列表。
<DependentLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:id="$+id:parent"
ohos:height="match_parent"
ohos:width="match_parent"
ohos:background_element="#00000000"
ohos:alignment="center">
<com.huawei.codelab.player.view.PlayerView
ohos:id="$+id:player_view"
ohos:height="match_parent"
ohos:width="match_parent"/>
<com.huawei.codelab.player.view.PlayerLoading
ohos:id="$+id:loading_view"
ohos:height="match_parent"
ohos:width="match_parent"/>
<com.huawei.codelab.player.view.SimplePlayerController
ohos:id="$+id:controller_view"
ohos:height="match_parent"
ohos:width="match_parent"/>
</DependentLayout>
该布局文件有两个id,parent是整个播放页面的布局id,parent_layout是视频画面的布局id。
Step 2 - 创建SimplePlayerAbilitySlice类,初次创建该页面进行初始化
@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setUIContent(ResourceTable.Layout_simple_video_play_layout);
player = new HmPlayer.Builder(this).setFilePath(url).create();
player.getLifecycle().onStart();
initComponent();
}
将预置的视频资源初始化为url对象,并通过initView方法对视频播放的控件进行初始化及赋值。
private void initComponent() {
if (findComponentById(ResourceTable.Id_parent) instanceof DependentLayout) {
parentLayout = (DependentLayout) findComponentById(ResourceTable.Id_parent);
}
if (findComponentById(ResourceTable.Id_player_view) instanceof PlayerView) {
playerView = (PlayerView) findComponentById(ResourceTable.Id_player_view);
}
if (findComponentById(ResourceTable.Id_loading_view) instanceof PlayerLoading) {
loadingView = (PlayerLoading) findComponentById(ResourceTable.Id_loading_view);
}
if (findComponentById(ResourceTable.Id_controller_view) instanceof SimplePlayerController) {
controllerView = (SimplePlayerController) findComponentById(ResourceTable.Id_controller_view);
}
playerView.bind(player);
loadingView.bind(player);
controllerView.bind(player);
}
—-结束
创建HmPlayer
HmPlayer类是以HarmonyOS Player为基础封装了复杂播放流程并对外开放ImPlayer接口类以实现所有支持的功能。
/**
* IPlayer interface
*
* @since 2021-04-04
*/
public interface ImplPlayer {
/**
* addSurface
*
* @param surface surface
*/
void addSurface(Surface surface);
/**
* addPlayerStatuCallback
*
* @param callback callback
*/
void addPlayerStatuCallback(StatuChangeListener callback);
/**
* removePlayerStatuCallback
*
* @param callback callback
*/
void removePlayerStatuCallback(StatuChangeListener callback);
/**
* addPlayerViewCallback
*
* @param callback callback
*/
void addPlayerViewCallback(ScreenChangeListener callback);
/**
* removePlayerViewCallback
*
* @param callback callback
*/
void removePlayerViewCallback(ScreenChangeListener callback);
/**
* play
*/
void play();
/**
* replay
*/
void replay();
/**
* reload
*
* @param filepath filepath
* @param startMillisecond startMillisecond
*/
void reload(String filepath, int startMillisecond);
/**
* resume
*/
void resume();
/**
* pause
*/
void pause();
/**
* getCurrentPosition
*
* @return current position
*/
int getCurrentPosition();
/**
* getDuration
*
* @return duration
*/
int getDuration();
/**
* getVolume
*
* @return float
*/
float getVolume();
/**
* set play volume
*
* @param volume 0~1
*/
void setVolume(float volume);
/**
* set play speed
*
* @param speed 0~12
*/
void setPlaySpeed(float speed);
/**
* getVideoScale
*
* @return double
*/
double getVideoScale();
/**
* rewindTo
*
* @param startMicrosecond startMicrosecond(ms)
*/
void rewindTo(int startMicrosecond);
/**
* isPlaying
*
* @return isPlaying
*/
boolean isPlaying();
/**
* stop
*/
void stop();
/**
* release
*/
void release();
/**
* getLifecycle
*
* @return ImplLifecycle
*/
ImplLifecycle getLifecycle();
/**
* getBuilder
*
* @return Builder
*/
HmPlayer.Builder getBuilder();
/**
* getPlayerStatu
*
* @return PlayerStatu
*/
PlayerStatu getPlayerStatu();
/**
* resizeScreen
*
* @param width width
* @param height height
*/
void resizeScreen(int width, int height);
/**
* openGesture
*
* @param isOpen isOpen
*/
void openGesture(boolean isOpen);
/**
* openGesture
*
* @return isGestureOpen
*/
boolean isGestureOpen();
}
通过HmPlayer.Builder构造器设置播放源、开始时间等播放参数并初始化HmPlayer。
player = new HmPlayer.Builder(this).setFilePath(url).create();
添加播放器状态、UI变化监听,在监听中处理逻辑。
player.addPlayerStatuCallback(statu -> mContext.getUITaskDispatcher().asyncDispatch(() -> {
switch (statu) {
case PREPARING:
case BUFFERING:
show();
break;
case PLAY:
hide();
break;
default:
break;
}
}));
player.addPlayerViewCallback((width, height) -> mContext.getUITaskDispatcher().asyncDispatch(() -> {
if (width > 0) {
setWidth(width);
}
if (height > 0) {
setHeight(height);
}
}));
设置播放器生命周期与slice生命周期一致。
@Override
public void onStart(Intent intent) {
super.onStart(intent);
...
player.getLifecycle().onStart();
}
@Override
public void onForeground(Intent intent) {
player.getLifecycle().onForeground();
super.onForeground(intent);
}
@Override
protected void onBackground() {
player.getLifecycle().onBackground();
super.onBackground();
}
@Override
protected void onStop() {
...
player.getLifecycle().onStop();
super.onStop();
}
如果您还不了解HarmonyOS Player,请参考视频播放开发指导。
创建PlayerView
图像渲染在屏幕上需要使用SurfaceProvider,该类控制surface的尺寸和格式,修改surface的像素,监视surface的变化等等。当底层显示系统第一次创建surface之后会调用surfaceCreated(SurfaceOps surfaceOps)回调函数。
surfaceView.getSurfaceOps().ifPresent(surfaceOps -> surfaceOps.addCallback(new SurfaceOps.Callback() {
@Override
public void surfaceCreated(SurfaceOps surfaceOps) {
surface = surfaceOps.getSurface();
if (player != null) {
player.addSurface(surface);
}
}
@Override
public void surfaceChanged(SurfaceOps surfaceOps, int info, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceOps surfaceOps) {
}
}));
PlayerView绑定HmPlayer:
if (findComponentById(ResourceTable.Id_player_view) instanceof PlayerView) {
playerView = (PlayerView) findComponentById(ResourceTable.Id_player_view);
}
playerView.bind(player);
视频尺寸与播放框架尺寸适配。
@Override
public void onRefreshed(Component component) {
int newWidth = component.getWidth();
int newHeight = component.getHeight();
double videoScale = player.getVideoScale();
if (videoScale != Constants.NUMBER_NEGATIVE_1 && (newWidth != viewWidth || newHeight != viewHeight)) {
viewWidth = newWidth;
viewHeight = newHeight;
mContext.getUITaskDispatcher().asyncDispatch(() -> updateVideoSize(videoScale));
}
}
private void updateVideoSize(double videoScale) {
if (videoScale > 1) {
surfaceView.setWidth(viewWidth);
surfaceView.setHeight((int) Math.min(viewWidth / videoScale, viewHeight));
} else {
surfaceView.setHeight(viewHeight);
surfaceView.setWidth((int) Math.min(viewHeight * videoScale, viewWidth));
}
}
PlayerView屏幕手势功能。
gestureDetector = new GestureDetector(gestureView);
surfaceView.setTouchEventListener((component, touchEvent) ->canGesture() && gestureDetector.onTouchEvent(touchEvent));
编译运行该应用程序
应用启动后,视频文件将被打开并开始播放,持续播放到最后。效果如下图:
4. 创建视频控制业务逻辑
上面的章节实现了视频播放的基本功能,本小节将创建一个控制器,包含基本的媒体控制UI元素如播放、暂停、恢复、重新加载按钮以及进度条。该控制器将与HmPlayer类一起提供一个基本功能全面且可操作的视频播放器。
创建SimpleVideoPlayerController
- SimplePlayerController类为自定义组件,包括控制视频的播放、暂停、恢复以及进度条等控件。此处使用HarmonyOS EventHandler来进行UI更新,请参考HarmonyOS开发者文档
public SimplePlayerController(Context context, ImplPlayer player) {
super(context);
this.context = context;
implPlayer = player;
// 创建子线程给自己发消息来及时更新UI
createHandler();
initView();
initListener();
}
其中initView方法初始化播放控制的控件。
Component playerController = LayoutScatter.getInstance(context).parse(
ResourceTable.Layout_simple_player_controller_layout, null, false);
addComponent(playerController);
if (playerController.findComponentById(ResourceTable.Id_play_controller) instanceof Image) {
// 播放或者暂停按钮
playToogle = (Image) playerController.findComponentById(ResourceTable.Id_play_controller);
}
if (playerController.findComponentById(ResourceTable.Id_play_forward) instanceof Image) {
// 前进按钮
imageForward = (Image) playerController.findComponentById(ResourceTable.Id_play_forward);
}
if (playerController.findComponentById(ResourceTable.Id_play_backward) instanceof Image) {
// 后退按钮
imageBackward = (Image) playerController.findComponentById(ResourceTable.Id_play_backward);
}
if (playerController.findComponentById(ResourceTable.Id_progress) instanceof Slider) {
// 进度条
progressBar = (Slider) playerController.findComponentById(ResourceTable.Id_progress);
}
initListener方法是对HmPlayer和播放控制器相互之间状态变化的监听处理。
implPlayer.addPlayerStatusCallback(statusChangeListener);
添加HmPlayer状态变化的监听,例如当视频播放完毕时,回调StatusChangeListener的statusCallback来刷新对控制器中各种组件的状态和显示值。HmPlayer中HmPlayerCallback中通过底层播放回调onPlayBackComplete来对界面视频状态进行更改。
@Override
public void onPlayBackComplete() {
for (StatusChangeListener callback : statusChangeCallbacks) {
status = PlayerStatus.COMPLETE;
callback.statusCallback(PlayerStatus.COMPLETE);
}
stop();
}
在SimplePlayerController的statusCallback中更新控制按钮状态
if (status == PlayerStatus.STOP || status == PlayerStatus.COMPLETE) {
controllerHandler.sendEvent(Constants.PLAYER_PROGRESS_RUNNING, EventHandler.Priority.IMMEDIATE);
playToogle.setPixelMap(ResourceTable.Media_ic_update);
progressBar.setEnabled(false);
}
此时播放按钮更新成待刷新图标,进度条不可拖拽。
创建PlayerLoading
在视频画面缓冲没有完成时,播放界面如果提供加载进度信息,用户体验更好。创建的PlayerLoading类设置一个布局并且添加StatusChangeListener监听回调,使得该控件可以根据状态显示或隐藏。
public PlayerLoading(Context context, ImplPlayer player) {
super(context);
this.player = player;
initView(context);
initListener();
}
private void initListener() {
player.addPlayerStatusCallback(new StatusChangeListener() {
@Override
public void statusCallback(PlayerStatus status) {
//获取主线程更新UI
mContext.getUITaskDispatcher().delayDispatch(
new Runnable() {
@Override
public void run() {
if (status == PlayerStatus.PREPARING || status == PlayerStatus.BUFFERING) {
show();
} else if (status == PlayerStatus.PLAY) {
hide();
} else {
LogUtil.info(PlayerLoading.class.getName(), "statuCallback else message");
}
}
}, 0);
}
});
}
编译运行该应用程序
经过上面的步骤,此时运行程序就可以看到一个有前进、后退、播放、暂停的界面,用户可以自主控制该视频播放,效果如下图:
5. 恭喜你
通过本篇Codelab你学到了:
- HarmonyOS中一个完整的视频播放应用需包括UI、Surface和媒体播放器。
- 使用
player.setSource(source)
指定视频文件的路径。 - 使用
SurfaceOps.Callback
来处理surface创建、状态改变和销毁的回调。 - 创建内部类HmPlayerCallback实现
Player.IPlayerCallback
的接口,监听视频状态改变,添加对控制器组件状态和缓冲界面的回调方法。 - 创建HmPlayerLifeCycle来管理HmPlayer生命周期。
6. 参考