HarmonyOS 制作简易视频播放器过程详解
头像 lili123123 2021-07-21 11:50:47    发布
3960 浏览 29 点赞 25 收藏

1. 介绍

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

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

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

本篇Codelab将实现的内容

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

您将构建一个基于HarmonyOS

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

  • 设置Huawei DevEco Studio开发环境,Huawei DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境
  • 技能要求

    • 具备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>
    

    该布局文件有两个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


     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); 
            } 
        }); 
    }

    编译运行该应用程序

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

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

    5. 恭喜你

    通过本篇Codelab你学到了:

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

    6. 参考

    gitee源码

    github源码



    ©本站发布的所有内容,包括但不限于文字、图片、音频、视频、图表、标志、标识、广告、商标、商号、域名、软件、程序等,除特别标明外,均来源于网络或用户投稿,版权归原作者或原出处所有。我们致力于保护原作者版权,若涉及版权问题,请及时联系我们进行处理。
    分类
    其它
    地址:北京市朝阳区北三环东路三元桥曙光西里甲1号第三置业A座1508室 商务内容合作QQ:2291221 电话:13391790444或(010)62178877
    版权所有:电脑商情信息服务集团 北京赢邦策略咨询有限责任公司
    声明:本媒体部分图片、文章来源于网络,版权归原作者所有,我司致力于保护作者版权,如有侵权,请与我司联系删除
    京ICP备:2022009079号-2
    京公网安备:11010502051901号
    ICP证:京B2-20230255