HarmonyOS学习路之开发篇—多媒体开发(媒体会话管理开发)

简介: AVSession是一套媒体播放控制框架,对媒体服务和界面进行解耦,并提供规范的通信接口,使应用可以自由、高效地在不同的媒体之间完成切换。

一、媒体会话管理开发

AVSession是一套媒体播放控制框架,对媒体服务和界面进行解耦,并提供规范的通信接口,使应用可以自由、高效地在不同的媒体之间完成切换。


约束与限制

在使用完AVSession类后,需要及时进行资源释放。

播放器类需要使用ohos.media.player.Player,否则无法正常接收按键事件。

场景介绍

AVSession框架有四个主要的类,控制着整个框架的核心,下图简单的说明四个核心媒体框架控制类的关系。


AVBrowser

媒体浏览器,通常在客户端创建,成功连接媒体服务后,通过媒体控制器AVController向服务端发送播放控制指令。


其主要流程为,调用connect方法向AVBrowserService发起连接请求,连接成功后在回调方法AVConnectionCallback.onConnected中发起订阅数据请求,并在回调方法AVSubscriptionCallback.onAVElementListLoaded中保存请求的媒体播放数据。


调用AVBrowser的subscribeByParentMediaId(String, AVSubscriptionCallback)之前,需要先执行unsubscribeByParentMediaId(String),防止重复订阅。


AVController

媒体控制器,在客户端AVBrowser连接服务成功后的回调方法AVConnectionCallback.onConnected中创建,用于向Service发送播放控制指令,并通过实现AVControllerCallback回调来响应服务端媒体状态变化,例如曲目信息变更、播放状态变更等,从而完成UI刷新。


AVBrowserService

媒体浏览器服务,通常在服务端,通过媒体会话 AVSession 与媒体浏览器建立连接,并通过实现 Player 进行媒体播放。其中有两个重要的方法:


onGetRoot,处理从媒体浏览器AVBrowser发来的连接请求,通过返回一个有效的AVBrowserRoot对象表示连接成功;

onLoadAVElementList,处理从媒体浏览器AVBrowser发来的数据订阅请求,通过 AVBrowserResult.sendAVElementList(List<AVElement>) 方法返回媒体播放数据。

使用onLoadAVElementList(String, AVBrowserResult)的result返回数据前,需要执行detachForRetrieveAsync()。


AVSession

媒体会话,通常在AVBrowserService的onStart中创建,通过setAVToken方法设置到AVBrowserService中,并通过实现AVSessionCallback回调来接收和处理媒体控制器 AVController 发送的播放控制指令,如播放、暂停、跳转至上一曲、跳转至下一曲等。


除了上述四个类,AVSession框架还有AVElement。


AVElement

媒体元素,用于将播放列表从AVBrowserService传递给AVBrowser。


接口说明

AVBrowser的主要接口

image.pngimage.png

AVBrowserService的主要接口

image.pngimage.png

AVController的主要接口

image.png

image.png

AVElement的主要接口


image.png

开发步骤

使用AVSession媒体框架创建一个播放器示例,分为创建客户端和创建服务端。


创建客户端


在客户端AVClientAbility中声明avBrowser和avController,通过avBrowser并向服务端发送连接请求,然后将avController注册到ability。

public class AVClientAbility extends Ability {
    // 媒体浏览器
    private AVBrowser avBrowser;
    // 媒体控制器
    private AVController avController;
    // 服务端回传的媒体列表
    List<AVElement> avElements;
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        // 用于指向媒体浏览器服务的包路径和类名
        ElementName elementName = new ElementName("", "com.samples.audioplayer", "com.samples.audioplayer.AVService");
        // connectionCallback在调用avBrowser.connect方法后进行回调。
        avBrowser = new AVBrowser(context, elementName, connectionCallback, null);
        // avBrowser发送对媒体浏览器服务的连接请求,connect方法需要确保当前处于断开连接状态。
        avBrowser.connect();
        // 将媒体控制器注册到ability以接收按键事件。
        AVController.setControllerForAbility(this, avController);
    }
}

AVConnectionCallback回调接口中的方法为可选实现,通常需要会在onConnected中订阅媒体数据和创建媒体控制器AVController。


// 发起连接(avBrowser.connect)后的回调方法实现
private AVConnectionCallback connectionCallback = new AVConnectionCallback() {
    @Override
    public void onConnected() {
        // 成功连接媒体浏览器服务时回调该方法,否则回调onConnectionFailed()。
        // 重复订阅会报错,所以先解除订阅。
        avBrowser.unsubscribeByParentMediaId(avBrowser.getRootMediaId());
        // 第二个参数AVSubscriptionCallback,用于处理订阅信息的回调。
        avBrowser.subscribeByParentMediaId(avBrowser.getRootMediaId(), avSubscriptionCallback);
        AVToken token = avBrowser.getAVToken();
        avController = new AVController(AVClient.this, token); // AVController第一个参数为当前类的context
        // 参数AVControllerCallback,用于处理服务端播放状态及信息变化时回调。
        avController.setAVControllerCallback(avControllerCallback);
        // ...
    }
    // 其它回调方法(可选)
    // ...
};


通常在订阅成功时,在AVSubscriptionCallback回调接口onAVElementListLoaded中保存服务端回传的媒体列表。

// 发起订阅信息(avBrowser.subscribeByParentMediaId)后的回调方法实现
private AVSubscriptionCallback avSubscriptionCallback = new AVSubscriptionCallback() {
    @Override
    public void onAVElementListLoaded(String parentId, List<AVElement> children) {
        // 订阅成功时回调该方法,parentID为标识,children为服务端回传的媒体列表
        super.onAVElementListLoaded(parentId, children);
        avElements.addAll(children);
        // ...
    }
};

AVControllerCallback回调接口中的方法均为可选方法,主要用于服务端播放状态及信息的变化后对客户端的回调,客户端可在这些方法中实现UI的刷新。


// 服务对客户端的媒体数据或播放状态变更后的回调 
private AVControllerCallback avControllerCallback = new AVControllerCallback() {
    @Override
    public void onAVMetadataChanged(AVMetadata metadata) {
        // 当服务端调用avSession.setAVMetadata(avMetadata)时,此方法会被回调。
        super.onAVMetadataChanged(metadata);
        AVDescription description = metadata.getAVDescription();
        String title = description.getTitle().toString();
        PixelMap pixelMap = description.getIcon();
        // ...
    }
    @Override
    public void onAVPlaybackStateChanged(AVPlaybackState playbackState) {
        // 当服务端调用avSession.setAVPlaybackState(...)时,此方法会被回调。
        super.onAVPlaybackStateChanged(playbackState);
        long position = playbackState.getCurrentPosition();
        // ...
    }
    // 其它回调方法(可选)
    // ...
};


完成以上实现后,则应用可以在UI事件中调用avController的方法向服务端发送播放控制指令。


// 在UI播放与暂停按钮的点击事件中向服务端发送播放或暂停指令
public void toPlayOrPause() {
    switch (avController.getAVPlaybackState().getAVPlaybackState()) {
        case AVPlaybackState.PLAYBACK_STATE_NONE: {
            avController.getPlayControls().prepareToPlay();
            avController.getPlayControls().play();
            break;
        }
        case AVPlaybackState.PLAYBACK_STATE_PLAYING: {
            avController.getPlayControls().pause();
            break;
        }
        case AVPlaybackState.PLAYBACK_STATE_PAUSED: {
            avController.getPlayControls().play();
            break;
        }
        default: {
            // ...
        }
    }
}

其它播放控制根据业务是否需要实现,比如:


avController.getPlayControls().playNext();
avController.getPlayControls().playPrevious();
avController.getPlayControls().playFastForward();
avController.getPlayControls().rewind();
avController.getPlayControls().seekTo(1000);
avController.getPlayControls().stop();
// ...
也可以主动获取媒体信息、播放状态等数据:
AVMetadata avMetadata = avController.getAVMetadata();
AVPlaybackState avPlaybackState = avController.getAVPlaybackState();
// ...

创建服务端


在服务端AVService中声明AVSession和Player。

public class AVService extends AVBrowserService {
    // 根媒体ID
    private static final String AV_ROOT_ID = "av_root_id";
    // 媒体会话
    private AVSession avSession;
    // 媒体播放器
    private Player player;
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        avSession = new AVSession(this, "AVService");
        setAVToken(avSession.getAVToken());
        // 设置sessioncallback,用于响应客户端的媒体控制器发起的播放控制指令。
        avSession.setAVSessionCallback(avSessionCallback);
        // 设置播放状态初始状态为AVPlaybackState.PLAYBACK_STATE_NONE。
        AVPlaybackState playbackState = new AVPlaybackState.Builder().setAVPlaybackState(AVPlaybackState.PLAYBACK_STATE_NONE, 0, 1.0f).build();
        avSession.setAVPlaybackState(playbackState);
        // 完成播放器的初始化,如果使用多个Player,也可以在执行播放时初始化。
        player = new Player(this);
    }
    @Override
    public AVBrowserRoot onGetRoot(String clientPackageName, int clientUid, PacMap rootHints) {
        // 响应客户端avBrowser.connect()方法。若同意连接,则返回有效的AVBrowserRoot实例,否则返回null
        return new AVBrowserRoot(AV_ROOT_ID, null);
    }
    @Override
    public void onLoadAVElementList(String parentId, AVBrowserResult result) {
         HiLog.info(TAG, "onLoadChildren");
         // 响应客户端avBrowser.subscribeByParentMediaId(...)方法。
         // 先执行该方法detachForRetrieveAsync() 
         result.detachForRetrieveAsync();
         // externalAudioItems缓存媒体文件,请开发者自行实现。
         result.sendAVElementList(externalAudioItems.getAudioItems());
    }
    @Override
    public void onLoadAVElementList(String s, AVBrowserResult avBrowserResult, PacMap pacMap) {
         // 响应客户端avBrowser.subscribeByParentMediaId(String, PacMap, AVSubscriptionCallback)方法。
    }
    @Override
    public void onLoadAVElement(String s, AVBrowserResult avBrowserResult) {
        // 响应客户端avBrowser.getAVElement(String, AVElementCallback)方法。
    }
}


响应客户端的媒体控制器发起的播放控制指令的回调实现。


private AVSessionCallback avSessionCallback = new AVSessionCallback() {
    @Override
    public void onPlay() {
        super.onPlay();
        // 当客户端调用avController.getPlayControls().play()时,该方法会被回调。
        // 响应播放请求,开始播放。
        if (avSession.getAVController().getAVPlaybackState() == AVPlaybackState.PLAYBACK_STATE_PAUSED) {
            if (player.play()) {
                AVPlaybackState playbackState = new AVPlaybackState.Builder().setAVPlaybackState(
                    AVPlaybackState.PLAYBACK_STATE_PLAYING, player.getCurrentTime(),
                    player.getPlaybackSpeed()).build();
                avSession.setAVPlaybackState(playbackState);
            }
        }
    }
    @Override
    public void onPause() {
        super.onPause();
        // 当客户端调用avController.getPlayControls().pause()时,该方法会被回调。
        // 响应暂停请求,暂停播放。
    }
    @Override
    public void onPlayNext() {
        super.onPlayNext();
        // 当客户端调用avController.getPlayControls().playNext()时,该方法会被回调。
        // 响应播放下一曲请求,通过avSession.setAVMetadata设置下一曲曲目的信息。
        avSession.setAVMetadata(avNextMetadata);
    }
    // 重写以处理按键事件
    @Override
    public boolean onMediaButtonEvent(Intent mediaButtonIntent) {
        KeyEvent ke = mediaButtonIntent.getSequenceableParam(AVSession.PARAM_KEY_EVENT);
        if (ke == null) {
            HiLog.error(TAG, "getSequenceableParam failed");
            return false;
        }
        if (ke.isKeyDown()) {
            // 只处理按键抬起事件
            return true;
        }
        switch (ke.getKeyCode()) {
            case KeyEvent.KEY_MEDIA_PLAY_PAUSE: {
                if (playbackState.getAVPlaybackState() == AVPlaybackState.PLAYBACK_STATE_PAUSED) {
                    onPlay();
                    break;
                }
                if (playbackState.getAVPlaybackState() == AVPlaybackState.PLAYBACK_STATE_PLAYING) {
                    onPause();
                    break;
                }
                break;
            }
            case KeyEvent.KEY_MEDIA_PLAY: {
                onPlay();
                break;
            }
            case KeyEvent.KEY_MEDIA_PAUSE: {
                onPause();
                break;
            }
            case KeyEvent.KEY_MEDIA_STOP: {
                onStop();
                break;
            }
            case KeyEvent.KEY_MEDIA_NEXT: {
                onPlayNext();
                break;
            }
            case KeyEvent.KEY_MEDIA_PREVIOUS: {
                onPlayPrevious();
                break;
            }
            default: {
                break;
            }
        }
        return true;
    }
    // 其它回调方法(可选)
    // ...
}


相关文章
|
14天前
|
缓存 API 数据安全/隐私保护
自学记录:学习HarmonyOS Location Kit构建智能定位服务
作为一名对新技术充满好奇心的开发者,我选择了HarmonyOS Next 5.0.1(API 13)作为挑战对象,深入研究其强大的定位服务API——Location Kit。从权限管理、获取当前位置、逆地理编码到地理围栏,最终成功开发了一款智能定位应用。本文将结合代码和开发过程,详细讲解如何实现这些功能,并分享遇到的挫折与兴奋时刻。希望通过我的经验,能帮助其他开发者快速上手HarmonyOS开发,共同探索更多可能性。
103 5
|
13天前
|
存储 人工智能 JavaScript
Harmony OS开发-ArkTS语言速成二
本文介绍了ArkTS基础语法,包括三种基本数据类型(string、number、boolean)和变量的使用。重点讲解了let、const和var的区别,涵盖作用域、变量提升、重新赋值及初始化等方面。期待与你共同进步!
73 47
Harmony OS开发-ArkTS语言速成二
|
14天前
|
前端开发 API 数据库
鸿蒙开发:异步并发操作
在结合async/await进行使用的时候,有一点需要注意,await关键字必须结合async,这两个是搭配使用的,缺一不可,同步风格在使用的时候,如何获取到错误呢,毕竟没有catch方法,其实,我们可以自己创建try/catch来捕获异常。
鸿蒙开发:异步并发操作
|
14天前
|
API
鸿蒙开发:实现popup弹窗
目前提供了两种方式实现popup弹窗,主推系统实现的方式,几乎能满足我们常见的所有场景,当然了,文章毕竟有限,尽量还是以官网为主。
鸿蒙开发:实现popup弹窗
|
5天前
|
存储 JSON 区块链
【HarmonyOS NEXT开发——ArkTS语言】购物商城的实现【合集】
HarmonyOS应用开发使用@Component装饰器将Home结构体标记为一个组件,意味着它可以在界面构建中被当作一个独立的UI单元来使用,并且按照其内部定义的build方法来渲染具体的界面内容。txt:string定义了一个名为Data的接口,用于规范表示产品数据的结构。src:类型为,推测是用于引用资源(可能是图片资源等)的一种特定类型,用于指定产品对应的图片资源。txt:字符串类型,用于存放产品的文字描述,比如产品名称等相关信息。price:数值类型,用于表示产品的价格信息。
26 5
|
5天前
|
开发工具 开发者 容器
【HarmonyOS NEXT开发——ArkTS语言】欢迎界面(启动加载页)的实现【合集】
从ArkTS代码架构层面而言,@Entry指明入口、@Component助力复用、@Preview便于预览,只是初窥门径,为开发流程带来些许便利。尤其动画回调与Blank组件,细节粗糙,后续定当潜心钻研,力求精进。”,字体颜色为白色,字体大小等设置与之前类似,不过动画配置有所不同,时长为。,不过这里没有看到额外的动画效果添加到这个特定的图片元素上(与前面带动画的元素对比而言)。这是一个显示文本的视图,文本内容为“奇怪的知识”,设置了字体颜色为灰色(的结构体,它代表了整个界面组件的逻辑和视图结构。
25 1
|
14天前
|
开发框架 物联网 API
HarmonyOS开发:串行通信开发详解
在电子设备和智能系统的设计中,数据通信是连接各个组件和设备的核心,串行通信作为一种基础且广泛应用的数据传输方式,因其简单、高效和成本效益高而被广泛采用。HarmonyOS作为一个全场景智能终端操作系统,不仅支持多种设备和场景,还提供了强大的开发框架和API,使得开发者能够轻松实现串行通信功能。随着技术的不断进步,串行通信技术也在不断发展。在HarmonyOS中,串行通信的开发不仅涉及到基本的数据发送和接收,还包括设备配置、错误处理和性能优化等多个方面。那么本文就来深入探讨在HarmonyOS中如何开发串行通信应用,包括串行通信的基础知识、HarmonyOS提供的API、开发步骤和实际代码示例
29 2
|
14天前
鸿蒙语言开发 几十套鸿蒙ArkTs app毕业设计及课程作业
鸿蒙语言开发 几十套鸿蒙ArkTs app毕业设计及课程作业
19 1
|
15天前
|
API 索引
鸿蒙开发:实现一个超简单的网格拖拽
实现拖拽,最重要的三个方法就是,打开编辑状态editMode,实现onItemDragStart和onItemDrop,设置拖拽移动动画和交换数据,如果想到开启补位动画,还需要实现supportAnimation方法。
71 13
鸿蒙开发:实现一个超简单的网格拖拽
|
14天前
|
索引 API
鸿蒙开发:自定义一个股票代码选择键盘
金融类的软件,特别是股票基金类的应用,在查找股票的时候,都会有一个区别于正常键盘的键盘,也就是股票代码键盘,和普通键盘的区别就是,除了常见的数字之外,也有一些常见的股票代码前缀按钮,方便在查找股票的时候,更加方便的进行检索。
鸿蒙开发:自定义一个股票代码选择键盘