HarmonyOS 制作简易视频播放器过程详解

奶盖
发布于 2021-7-20 14:29
浏览
3收藏

1. 介绍

 

播放视频的多媒体应用程序通常包含两个部分:

1.给定媒体源的播放器加载媒体资源,并通过Surface来进行画面渲染,将其呈现为视频。
2.具有传输控件的用户界面(UI),以承载播放器并显示播放器的状态。
本应用程序原理图如下图:

HarmonyOS 制作简易视频播放器过程详解-鸿蒙开发者社区

本篇Codelab将实现的内容

 

        本篇Codelab旨在让开发者了解手机HarmonyOS应用开发,常用布局、典型控件、FA组件、媒体-视频、跨设备协同的体验以及从工程创建到代码和布局的编写,再到编译构建、部署和运行全过程。

 

 

 

        您将构建一个基于HarmonyOS

Player

      类实现的应用程序,该应用程序功能为播放本地视频资源或从Internet获得的视频资源。效果图如下:

HarmonyOS 制作简易视频播放器过程详解-鸿蒙开发者社区

您将会学到什么

 

  • 如何使用Player类播放视频
  • 如何使用自定义控件来控制视频播放
  • 如何添加并使用媒体事件的事件侦听器和回调

 

硬件要求

 

  • 操作系统:Windows10 64位
  • 内存:8GB及以上
  • 硬盘:100GB及以上
  • 分辨率:1280*800像素及以上

 

软件要求

 

  • 安装DevEco Studio,详情请参考DevEco Studio下载
  • 设置Huawei DevEco Studio开发环境,Huawei DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境
    1. 如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作
    2. 如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境
    3.  说明:
      如需要在手机中运行程序,则需要提前申请证书,如使用模拟器可忽略
    4.  

 

技能要求

 

  • 具备DevEco Studio中创建、构建和运行应用经验
  • 熟悉Ability和AbilitySlice生命周期及使用PA/FA的能力

     

     

 

 

2. 代码结构

 

 

本篇Codelab只对核心代码进行讲解,对于完整代码,我们在参考提供下载方式。接下来我们会讲解整个工程的代码结构,如下图:HarmonyOS 制作简易视频播放器过程详解-鸿蒙开发者社区

  • 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>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.

 

该布局文件有两个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();
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

 

将预置的视频资源初始化为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);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

 

—-结束

创建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(); 
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.

通过HmPlayer.Builder构造器设置播放源、开始时间等播放参数并初始化HmPlayer。

player = new HmPlayer.Builder(this).setFilePath(url).create();
  • 1.

添加播放器状态、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); 
    } 
}));
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

设置播放器生命周期与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(); 
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.

如果您还不了解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) {
            }
        }));
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

PlayerView绑定HmPlayer:

if (findComponentById(ResourceTable.Id_player_view) instanceof PlayerView) { 
    playerView = (PlayerView) findComponentById(ResourceTable.Id_player_view); 
} 
playerView.bind(player);
  • 1.
  • 2.
  • 3.
  • 4.

视频尺寸与播放框架尺寸适配。

@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)); 
    } 
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

PlayerView屏幕手势功能。

gestureDetector = new GestureDetector(gestureView); 
surfaceView.setTouchEventListener((component, touchEvent) ->canGesture() && gestureDetector.onTouchEvent(touchEvent));
  • 1.
  • 2.

 

编译运行该应用程序

 

应用启动后,视频文件将被打开并开始播放,持续播放到最后。效果如下图:HarmonyOS 制作简易视频播放器过程详解-鸿蒙开发者社区

 

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(); 
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

 

其中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); 
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

 

initListener方法是对HmPlayer和播放控制器相互之间状态变化的监听处理。

implPlayer.addPlayerStatusCallback(statusChangeListener);
  • 1.

 

添加HmPlayer状态变化的监听,例如当视频播放完毕时,回调StatusChangeListener的statusCallback来刷新对控制器中各种组件的状态和显示值。HmPlayer中HmPlayerCallback中通过底层播放回调onPlayBackComplete来对界面视频状态进行更改。

@Override 
public void onPlayBackComplete() { 
    for (StatusChangeListener callback : statusChangeCallbacks) { 
        status = PlayerStatus.COMPLETE; 
        callback.statusCallback(PlayerStatus.COMPLETE); 
    } 
    stop(); 
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

 

在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); 
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

此时播放按钮更新成待刷新图标,进度条不可拖拽。

 

创建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); 
        } 
    }); 
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

 

编译运行该应用程序

 

经过上面的步骤,此时运行程序就可以看到一个有前进、后退、播放、暂停的界面,用户可以自主控制该视频播放,效果如下图:

HarmonyOS 制作简易视频播放器过程详解-鸿蒙开发者社区

 

5. 恭喜你

 

通过本篇Codelab你学到了:

  • HarmonyOS中一个完整的视频播放应用需包括UI、Surface和媒体播放器。
  • 使用player.setSource(source)指定视频文件的路径。
  • 使用SurfaceOps.Callback来处理surface创建、状态改变和销毁的回调。
  • 创建内部类HmPlayerCallback实现Player.IPlayerCallback的接口,监听视频状态改变,添加对控制器组件状态和缓冲界面的回调方法。
  • 创建HmPlayerLifeCycle来管理HmPlayer生命周期。

 

6. 参考

 

gitee源码

github源码

已于2021-7-20 14:29:26修改
3
收藏 3
回复
举报
3
3
回复
    相关推荐