音视频开发(三十八):ExoPlayer 音频播放器实践

通过上一篇的学习实践,我们了解了ExoPlayer的优缺点以及基本用法,今天我们进入ExoPlayer的音频播放实践,我们来一起实现一个简单的音频播放器。

目录

  1. 媒体播放框架MediaSession

  2. MediaSession框架+ExoPlayer 简单音乐播放器实践

  • 播放网络音乐

  • 播放/暂停

  • 歌曲切换

  • 倍速播放

一、媒体播放框架MediaSession

音频播放器并不总是需要使其UI可见。一旦开始播放音频,播放器就可以作为后台任务运行。用户可以切换到另一个应用程序,并继续听。
要在Android中实现这一设计,您可以使用两个组件构建一个音频应用程序:activity(展示所用) 和播放器service。如果用户切换到另一个应用程序,则该service可以在后台运行。通过将音频应用程序的两个部分分解为单独的组件,每个组件可以独立运行。与播放器相比,UI通常是短暂的,可能会在没有UI的情况下运行很长时间。

在设计音乐播放器APP架构时,有几种常用的做法

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓


方案一

  1. 注册Service,用于数据设置、音乐控制,在Service中自定义播放器的一些状态值和回调接口用于流程控制

  2. 通过广播、aidl等实现和页面层逻辑的通信,使得用户可以通过界面控制音乐的播放、暂停、切换、seek等操作

  3. 使用RemoteControlClient(低版本)或者MediaSession(>5.0或者MediaSessionCompat)进行多端设备或者跨APP媒体会话

方案二
Android5.0时推出的MediaSession框架(Supprot包中MediaSessionCompat也对低版本做了支持),专门用来解决媒体播放时界面和Service通信的问题,在结构低耦合方面的设计做的比较好

支持库提供了两个类来实现此客户端/服务器方法:MediaBrowserService和MediaBrowser。该服务组件被实现为包含媒体会话及其播放器的MediaBrowserService的子类。使用UI和媒体控制器的活动应包括与MediaBrowserService进行通信的MediaBrowser。
使用MediaBrowserService可以让随身设备(如Android Auto and Wear)轻松发现您的应用,连接到它,浏览内容和控制播放,而无需访问您的Activity

我们今天的学习实践是基于方案二的MediaSession的框架

MediaBrowser
用来连接MediaBrowserService和订阅数据,通过他的回调可以获取和Service的连接状态以及获取在Service中异步获取的音乐数据(这个一般不在Service中进行获取,因为涉及到的是具体的业务逻辑)

MediaBrowserService
是一个Service,封装了媒体相关的一些功能,通过onGetRoot的返回值决定是否允许客户端连接。onLoadChildren回调在Sercive中异步获取的数据给到MediaBrowser。也包含媒体播放器实例(比如我们本篇实践的ExoPlayer)

MediaSession
一般在MediaBrowserService的onCreate中创建,通过MediaSession.CallBack回调接收MediaController发来的指令,触发对应的播放器相关的操作

MediaController
MediaContoller的创建需要MediaSession的配对令牌,在MediaBrowser连接服务成功之后创建。MediaController可以主动的发送指令或者被动的接收MediaController.Callback回调来改变播放状态和界面刷新。

更详细的介绍请参考官方文档或者Android 媒体播放框架MediaSession分析与实践

二、 简单实践

下面我们看下如何使用MediaSession框架实现简单的音频播放

2.1 Server端实现

首先我们继承MediaBrowserServiceCompat实现和注册Service

public class MusicService extends MediaBrowserServiceCompat {

    private static final String TAG = "MusicService";
    private SimpleExoPlayer exoPlayer;
    private MediaSessionCompat mediaSession;

    /**
     * 当服务收到onCreate()生命周期回调方法时,它应该执行以下步骤:
     * 1. 创建并初始化media session
     * 2. 设置media session回调
     * 3. 设置media session token
     */
    @Override
    public void onCreate() {
        Log.i(TAG, "onCreate: ");
        super.onCreate();
        //1. 创建并初始化MediaSession
        mediaSession = new MediaSessionCompat(getApplicationContext(), TAG);

        mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
                | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);

        PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder()
                .setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PLAY_PAUSE
                        | PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_PLAY_PAUSE |
                        PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID |
                        PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH |
                        PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS |
                        PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SEEK_TO)
                .build();
        mediaSession.setPlaybackState(playbackState);

        //2. 设置mediaSession回调
        mediaSession.setCallback(new MyMediaSessionCallBack());

        //3. 设置mediaSessionToken
       setSessionToken(mediaSession.getSessionToken());

        //创建播放器实例
        exoPlayer = new SimpleExoPlayer.Builder(getApplicationContext()).build();
    }
}

MediaSessionCompat.Callback的回调用于接收业务层通过mediaController.getTransportControls进行播放相关操作(播放、暂停、seek、倍速等等)的回调

 /**
     * 用于接收由MediaControl触发的改变,内部封装实现播放器和播放状态的改变
     */
    private class MyMediaSessionCallBack extends MediaSessionCompat.Callback {


        @Override
        public void onPlay() {
            super.onPlay();

            Log.i(TAG, "onPlay: ");
            exoPlayer.play();
        }

        @Override
        public void onPause() {
            super.onPause();

            Log.i(TAG, "onPause: ");
            exoPlayer.pause();
        }

        @Override
        public void onSeekTo(long pos) {
            super.onSeekTo(pos);
            Log.i(TAG, "onSeekTo: pos=" + pos);

            exoPlayer.seekTo(pos);
        }

      ...
    }

MediaBrowserServiceCompat有两个回调方法onGetRoot和onLoadChildren。其中onGetRoot用于告诉MediaBrowser是否连接连接成功;onLoadChildren则是加载音视频数据。
具体使用如下:

 @Nullable
    @Override
    public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) {
        Log.i(TAG, "onGetRoot: clientPackageName=" + clientPackageName + " clientUid=" + clientUid + " pid=" + Binder.getCallingPid()
                + " uid=" + Binder.getCallingUid());
        //返回非空,表示连接成功
        return new BrowserRoot("media_root_id", null);
    }

    //获取音视频信息(这个更应该是在业务层处理事情)
    @Override
    public void onLoadChildren(@NonNull String parentId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
        Log.i(TAG, "onLoadChildren: parentId=" + parentId);
        List<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>();
        if (TextUtils.equals("media_root_id", parentId)) {

        }
        ArrayList<MusicEntity> musicEntityList = getMusicEntityList();

        for (int i = 0; i < musicEntityList.size(); i++) {
            MusicEntity musicEntity = musicEntityList.get(i);

             MediaMetadataCompat metadataCompat = buildMediaMetadata(musicEntity);

            if (i == 0) {
                mediaSession.setMetadata(metadataCompat);
            }

            mediaItems.add(new MediaBrowserCompat.MediaItem(metadataCompat.getDescription(), MediaBrowserCompat.MediaItem.FLAG_BROWSABLE));

            exoPlayer.addMediaItem(MediaItem.fromUri(musicEntity.source));
        }
        //当设置多首歌曲组成队列时报错
        // IllegalStateException: sendResult() called when either sendResult() or sendError() had already been called for: media_root_id
        //原因,之前在for处理了,应该在设置好mediaItems列表后,统一设置result
        result.sendResult(mediaItems);
        Log.i(TAG, "onLoadChildren: addMediaItem");

        initExoPlayerListener();

        exoPlayer.prepare();
        Log.i(TAG, "onLoadChildren: prepare");
    }

    private void initExoPlayerListener() {
        exoPlayer.addListener(new Player.EventListener() {
            @Override
            public void onPlaybackStateChanged(int state) {
                long currentPosition = exoPlayer.getCurrentPosition();
                long duration = exoPlayer.getDuration();

                //状态改变(播放器内部发生状态变化的回调,
                // 包括
                // 1. 用户触发的  比如:手动切歌曲、暂停、播放、seek等;
                // 2. 播放器内部触发 比如:播放结束、自动切歌曲等)

                //该如何通知给ui业务层呐??好些只能通过回调
                //那有该如何 --》查看源码得知通过setPlaybackState设置
                Log.i(TAG, "onPlaybackStateChanged: currentPosition=" + currentPosition + " duration=" + duration + " state=" + state);

                int playbackState;
                switch (state) {
                    default:
                    case Player.STATE_IDLE:
                        playbackState = PlaybackStateCompat.STATE_NONE;
                        break;
                    case Player.STATE_BUFFERING:
                        playbackState = PlaybackStateCompat.STATE_BUFFERING;
                        break;
                    case Player.STATE_READY:
                        if(exoPlayer.getPlayWhenReady()){
                            playbackState = PlaybackStateCompat.STATE_PLAYING;
                        }else {
                            playbackState = PlaybackStateCompat.STATE_PAUSED;
                        }
                        break;
                    case Player.STATE_ENDED:
                        playbackState = PlaybackStateCompat.STATE_STOPPED;
                        break;
                }
                //播放器的状态变化,通过mediasession告诉在ui业务层注册的MediaControllerCompat.Callback进行回调

                setPlaybackState(playbackState);
            }

           

    private void setPlaybackState(int playbackState) {
        float speed = exoPlayer.getPlaybackParameters() == null ? 1f : exoPlayer.getPlaybackParameters().speed;

        mediaSession.setPlaybackState(new PlaybackStateCompat.Builder().setState(playbackState, exoPlayer.getCurrentPosition(), speed).build());
    }

    @NotNull
    private ArrayList<MusicEntity> getMusicEntityList() {
        ArrayList<MusicEntity> list = new ArrayList<MusicEntity>();
    ...

        MusicEntity musicEntity2 = new MusicEntity();
        musicEntity2.id = "wake_up_02";
        musicEntity2.title = "Geisha";
        musicEntity2.album = "Wake Up";
        musicEntity2.artist = "Media Right Productions";
        musicEntity2.genre = "Electronic";
        musicEntity2.source = "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/02_-_Geisha.mp3";
        musicEntity2.image = "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg";
        musicEntity2.trackNumber = 2;
        musicEntity2.totalTrackCount = 13;
        musicEntity2.duration = 267;
        musicEntity2.site = "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/";

        list.add(musicEntity2);

        return list;
    }

2.2 Client端实现

下面我们再来看下Client端的实现

public class ExoSimpleAudioPlayerActivity extends Activity implements View.OnClickListener {
    private MediaBrowserCompat mediaBrowser;
    private MediaBrowserCompat.ConnectionCallback mConnectionCallbacks = new MyConnectionCallback();
    private MediaControllerCompat.Callback mMediaControllerCallback;
    private MediaBrowserCompat.SubscriptionCallback mSubscriptionCallback;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_simple_audio);
...
        //mConnectionCallbacks 是C-S连接的callback
        mediaBrowser = new MediaBrowserCompat(this, new ComponentName(this, MusicService.class),
                mConnectionCallbacks, null);
   }

    @Override
    protected void onStart() {
        super.onStart();
        Log.i(TAG, "onStart: ");
        //发出C-S连接请求 创建MusicService,收到onGetRoot回调值不为空说明建立连接成功--》然后触发MyConnectionCallback的回调onConnected
        mediaBrowser.connect();
//        subscribe();
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.i(TAG, "onStop: ");
        mediaBrowser.disconnect();
    }
}

MediaBrowserCompat.ConnectionCallback用于接收与Server端连接的状态回调

    public class MyConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
        @Override
        public void onConnected() {
            super.onConnected();
            Log.i(TAG, "onConnected: MyConnectionCallback");

            //MediaBrowser和MediaBrowerService建立连接之后会回调该方法
            MediaSessionCompat.Token sessionToken = mediaBrowser.getSessionToken();

            //建立连接之后再创建MediaController
            mediaController = new MediaControllerCompat(ExoSimpleAudioPlayerActivity.this, sessionToken);

            MediaControllerCompat.setMediaController(ExoSimpleAudioPlayerActivity.this, mediaController);

            subscribe();
            //MediaController发送命令
            buildTransportControls();
            if (mMediaControllerCallback == null) {
                //这个callback 是Controller的callback,即用户触发了播放、暂停,后发生状态变化的回调。
                //像播放结束、自动切歌,则无法收到该回调(那该如何处理呐?)

                mMediaControllerCallback = new MediaControllerCompat.Callback() {

                    //这里的回调,只有用户触发的才会有相应的回调。
                    //播放结束 这里没有
                    //ExoPlayer getDuration : https://stackoverflow.com/questions/35298125/exoplayer-getduration
                    // Override
                    public void onPlaybackStateChanged(PlaybackStateCompat state) {
                        super.onPlaybackStateChanged(state);
                        Log.i(TAG, "onPlaybackStateChanged: state=" + state.getState());
                        if (PlaybackStateCompat.STATE_PLAYING == state.getState()) {
                            playButton.setText("暂停");
                        } else {
                            playButton.setText("播放");
                        }
                        updatePlaybackState(state);

                        MediaMetadataCompat metadata = mediaController.getMetadata();
                        updateDuration(metadata);
                    }

                    @Override
                    public void onMetadataChanged(MediaMetadataCompat metadata) {
                        super.onMetadataChanged(metadata);
                        durationSet = false;
                        Log.i(TAG, "onMetadataChanged: metadata=" + metadata.toString());
                        updateDuration(metadata);

                    }
            }
            mediaController.registerCallback(mMediaControllerCallback);
            PlaybackStateCompat state = mediaController.getPlaybackState();
            updatePlaybackState(state);
            updateProgress();
            if (state != null && (state.getState() == PlaybackStateCompat.STATE_PLAYING ||
                    state.getState() == PlaybackStateCompat.STATE_BUFFERING)) {
                scheduleSeekbarUpdate();
            }

            //通过mediaController获取MediaMetadataCompat
            MediaMetadataCompat metadata = mediaController.getMetadata();
            updateDuration(metadata);
        }


        @Override
        public void onConnectionFailed() {
            super.onConnectionFailed();
        }
    }

2.3 基本功能

歌曲播放播放暂停
当用户点击了播放/暂停按钮后,获取当前的播放状态,通过mediaController.getTransportControls给到通过Binder给到mediaSession,在service中MediaSessionCompat.Callback改变Exoplayer的播放状态,exoplayer的onPlaybackStateChanged收到播放状态改变的通知后触发,给mediasession设置mediaSession.setPlaybackState

对应关键代码如下:

 client端用户点击事件处理
 
 //ExoSimpleAudioPlayerActivity.java
    
 PlaybackStateCompat playbackState = mediaController.getPlaybackState();
            int state = playbackState.getState();
            Log.i(TAG, "onClick: state=" + state);
            //通过 mediaController.getTransportControls 触发MediaSessionCompat.Callback回调--》进行播放控制
            if (state == PlaybackStateCompat.STATE_PLAYING) {
                mediaController.getTransportControls().pause();
            } else {
                mediaController.getTransportControls().play();
            }

//Server端MediasessionCallback实现,接收mediaController.getTransportControls()的事件

//com.example.myplayer.audio.MusicService.MyMediaSessionCallBack

       @Override
        public void onPlay() {
            super.onPlay();

            Log.i(TAG, "onPlay: ");
            exoPlayer.play();
        }

        @Override
        public void onPause() {
            super.onPause();

            Log.i(TAG, "onPause: ");
            exoPlayer.pause();
        }

//server端 exoplayer状态变化监听

//com.example.myplayer.audio.MusicService#initExoPlayerListener

exoPlayer.addListener(new Player.EventListener() {
            @Override
            public void onPlaybackStateChanged(int state) {
                long currentPosition = exoPlayer.getCurrentPosition();
                long duration = exoPlayer.getDuration();

                //状态改变(播放器内部发生状态变化的回调,
                // 包括
                // 1. 用户触发的  比如:手动切歌曲、暂停、播放、seek等;
                // 2. 播放器内部触发 比如:播放结束、自动切歌曲等)

                //该如何通知给ui业务层呐??好些只能通过回调
                //那有该如何 --》查看源码得知通过setPlaybackState设置
                Log.i(TAG, "onPlaybackStateChanged: currentPosition=" + currentPosition + " duration=" + duration + " state=" + state);

                int playbackState;
                switch (state) {
                    default:
                    case Player.STATE_IDLE:
                        playbackState = PlaybackStateCompat.STATE_NONE;
                        break;
                    case Player.STATE_BUFFERING:
                        playbackState = PlaybackStateCompat.STATE_BUFFERING;
                        break;
                    case Player.STATE_READY:
                        if(exoPlayer.getPlayWhenReady()){
                            playbackState = PlaybackStateCompat.STATE_PLAYING;
                        }else {
                            playbackState = PlaybackStateCompat.STATE_PAUSED;
                        }
                        break;
                    case Player.STATE_ENDED:
                        playbackState = PlaybackStateCompat.STATE_STOPPED;
                        break;
                }
                //播放器的状态变化,通过mediasession告诉在ui业务层注册的MediaControllerCompat.Callback进行回调

                setPlaybackState(playbackState);
            }
}

    private void setPlaybackState(int playbackState) {
        float speed = exoPlayer.getPlaybackParameters() == null ? 1f : exoPlayer.getPlaybackParameters().speed;

        mediaSession.setPlaybackState(new PlaybackStateCompat.Builder().setState(playbackState, exoPlayer.getCurrentPosition(), speed).build());
    }

虽然知道了怎么使用,但是整个流程是怎样的呐?
其中用到了Handler和Binder的线程和进程通信相关的知识,后续我们专题单独深入学习实践下,这里我们先顺着流程画下播放/暂停的流程图,从用户按下按钮到播放器开始播放以及页面更新的整个流程是怎样的。

上一首下一首切换
歌曲切换流程个上面的播放流程基本上一致,

//com.example.myplayer.audio.ExoSimpleAudioPlayerActivity#onClick

 if (id == R.id.prev) {
            if (mediaController != null) {
                mediaController.getTransportControls().skipToPrevious();
            }
        } else if (id == R.id.next) {
            if (mediaController != null) {
                mediaController.getTransportControls().skipToNext();
            }
        }

区别在于 没有触发ExoPlayer的播放回调,需要再sessionCallback中调用exoplayer的next/prev进行歌曲切换,并且设置新的playstate状态给到mession

//com.example.myplayer.audio.MusicService.MyMediaSessionCallBack
 
     @Override
        public void onSkipToNext() {
            super.onSkipToNext();
            Log.i(TAG, "onSkipToNext: ");
            exoPlayer.next();
            exoPlayer.setPlayWhenReady(true);
            setPlaybackState(PlaybackStateCompat.STATE_SKIPPING_TO_NEXT);
    mediaSession.setMetadata(getMediaMetadata(1));
        }

        @Override
        public void onSkipToPrevious() {
            super.onSkipToPrevious();
            Log.i(TAG, "onSkipToPrevious: ");
            exoPlayer.previous();
            exoPlayer.setPlayWhenReady(true);
            setPlaybackState(PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS);
    mediaSession.setMetadata(getMediaMetadata(0));

        }

最终MediaControllerCallback的onPlaybackStateChanged收到回调,根据状态进行

   public void onPlaybackStateChanged(PlaybackStateCompat state) {
            super.onPlaybackStateChanged(state);
            ...
                   if (state.getState() == PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS || state.getState() == PlaybackStateCompat.STATE_SKIPPING_TO_NEXT) {
            updateShowMediaInfo(description);
        }
}

       private void updateShowMediaInfo(MediaDescriptionCompat description) {
        if (description == null) return;

        titleView.setText(description.getTitle());
        artistView.setText(description.getSubtitle());

        Glide.with(ExoSimpleAudioPlayerActivity.this).load(description.getIconUri().toString()).into(iconView);
        Uri mediaUri = description.getMediaUri();
        Uri iconUri = description.getIconUri();
        Log.i(TAG, "onChildrenLoaded: title=" + description.getTitle() + " subtitle=" + description.getSubtitle()
                + " mediaUri=" + mediaUri + " iconUri=" + iconUri);
    }

 倍速

//com.example.myplayer.audio.ExoSimpleAudioPlayerActivity#onClick
if (id == R.id.speed) {
            if (mediaController != null) {
                float speed = getSpeed();
                speedView.setText("倍速 " + speed);
                mediaController.getTransportControls().setPlaybackSpeed(speed);
            }
        }

 float[] speedArray = new float[]{0.5f, 1f, 1.5f, 2f};
    int curSpeedIndex = 1;

    private float getSpeed() {
        if (curSpeedIndex > 3) {
            curSpeedIndex = 0;
        }
        return speedArray[curSpeedIndex++];
    }

 然后再MediaSessionCallBack中实现onSetPlaybackSpeed回调,进行播放倍速设置以及mession的设置

//com.example.myplayer.audio.MusicService.MyMediaSessionCallBack
  
 @Override
        public void onSetPlaybackSpeed(float speed) {
            super.onSetPlaybackSpeed(speed);
            Log.i(TAG, "onSetPlaybackSpeed: speed=" + speed);
            PlaybackParameters playParams = new PlaybackParameters(speed);
            exoPlayer.setPlaybackParameters(playParams);
            //重新设置mediaSession.setPlaybackState 告知 监听者 speed变化
            setPlaybackState(exoPlayer.getPlaybackState());
        }

   private void setPlaybackState(int playbackState) {
        float speed = exoPlayer.getPlaybackParameters() == null ? 1f : exoPlayer.getPlaybackParameters().speed;

        mediaSession.setPlaybackState(new PlaybackStateCompat.Builder().setState(playbackState, exoPlayer.getCurrentPosition(), speed).build());
    }

 需要注意
播放状态 MediaSession框架和ExoPlayer的不同与联系

//android.support.v4.media.session.PlaybackStateCompat
TATE_NONE, STATE_STOPPED, STATE_PAUSED, STATE_PLAYING, STATE_FAST_FORWARDING,
            STATE_REWINDING, STATE_BUFFERING, STATE_ERROR, STATE_CONNECTING,
            STATE_SKIPPING_TO_PREVIOUS, STATE_SKIPPING_TO_NEXT, STATE_SKIPPING_TO_QUEUE_ITEM

//com.google.android.exoplayer2.Player.State

STATE_IDLE, STATE_BUFFERING, STATE_READY, STATE_ENDED

其他

  1. android 禁用和开启四大组件的方法(setComponentEnabledSetting )

  2. Android 通知渠道Notification Channel

网络接口以及歌曲来源

来自google官方的uamp开源项目

http://storage.googleapis.com/automotive-media/music.json
https://storage.googleapis.com/uamp/catalog.json


Music provided by the [Free Music Archive](http://freemusicarchive.org/).

- [Irsen's Tale](http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/) by
[Kai Engel](http://freemusicarchive.org/music/Kai_Engel/).
- [Wake Up](http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/) by
[The Kyoto Connection](http://freemusicarchive.org/music/The_Kyoto_Connection/).


长音频:https://v.typlog.com/oohomechat/8385162738_706123.mp3

收获

通过本篇的学习实践,

  1. 了解媒体播放框架MediaSession

  2. 使用MediaSession框架实现简单的音频播放器(播放/暂停、切歌、倍速)

  3. 了解原理、具体实践以及流程分析,我们基本了解MediaSession的框架以及ExoPlayer简单实用。
    但是一个音频播放器以下功能也是基本功能:边缓存变播放、播放队列、淡入淡出、音频焦点、后台播放,该如何比较好的实现呐?在具体实践之前我们先来学习分析下uamp这个google开源的音频播放器是如何架构的,看看在数据源设置以及播放管理方面是否可以学习借鉴。

感谢你的阅读

下一篇我们继续学习实践ExoPlayer,分析uamp的设计与实现

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值