简述

最近在使用EXOplayer做关于音频的开发,目标如下

  1. 通过service绑定activity,能在后台播放,同时,在退出activity之后,显示一个悬浮窗,悬浮窗能同步播放器的进度,点击则进入播放界面。

关于ExoPlayer

这个库是goole官方推出的,十分强大,根据项目需要我主要使用它来进行音频播放。

列举几篇有参考意义的参考文献

  1. 官方介绍

先来一张图片镇楼


Android app内部悬浮直播窗 安卓悬浮窗播放器_Android app内部悬浮直播窗

使用步骤

  1. 需要创建DefaultDataSourceFactory

进度条监听

在常见的music软件中进度条的监听是必不可少的,在本项目中,使用的进度条是defaultTimeBar,UI的更新在android使用handler是必不可少的,在配上接口回调,相当的nice。

public Runnable loadStatusRunable = new Runnable() {
        @Override
        public void run() {
            long durationUs = 0;
            int adGroupCount = 0;
            long currentWindowTimeBarOffsetMs = 0;
            Timeline currentTimeline = mSimpleExoPlayer.getCurrentTimeline();
            if (!currentTimeline.isEmpty()) {
                int currentWindowIndex = mSimpleExoPlayer.getCurrentWindowIndex();


                int firstWindowIndex = currentWindowIndex;
                int lastWindowIndex = currentWindowIndex;
                for (int i = firstWindowIndex; i <= lastWindowIndex; i++) {
                    if (i == currentWindowIndex) {
                        currentWindowTimeBarOffsetMs = C.usToMs(durationUs);
                    }
                    currentTimeline.getWindow(i, window);
                    if (window.durationUs == C.TIME_UNSET) {
                        break;
                    }
                    for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) {
                        currentTimeline.getPeriod(j, period);
                        int periodAdGroupCount = period.getAdGroupCount();
                        for (int adGroupIndex = 0; adGroupIndex < periodAdGroupCount; adGroupIndex++) {
                            long adGroupTimeInPeriodUs = period.getAdGroupTimeUs(adGroupIndex);
                            if (adGroupTimeInPeriodUs == C.TIME_END_OF_SOURCE) {
                                if (period.durationUs == C.TIME_UNSET) {
                                    continue;
                                }
                                adGroupTimeInPeriodUs = period.durationUs;
                            }
                            long adGroupTimeInWindowUs = adGroupTimeInPeriodUs + period.getPositionInWindowUs();
                            if (adGroupTimeInWindowUs >= 0 && adGroupTimeInWindowUs <= window.durationUs) {
                                if (adGroupCount == adGroupTimesMs.length) {
                                    int newLength = adGroupTimesMs.length == 0 ? 1 : adGroupTimesMs.length * 2;
                                    adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, newLength);
                                    playedAdGroups = Arrays.copyOf(playedAdGroups, newLength);
                                }
                                adGroupTimesMs[adGroupCount] = C.usToMs(durationUs + adGroupTimeInWindowUs);
                                playedAdGroups[adGroupCount] = period.hasPlayedAdGroup(adGroupIndex);
                                adGroupCount++;
                            }
                        }
                    }
                    durationUs += window.durationUs;
                }
            }

            durationUs = C.usToMs(window.durationUs);
            long curtime = currentWindowTimeBarOffsetMs + mSimpleExoPlayer.getContentPosition();
            long bufferedPosition = currentWindowTimeBarOffsetMs + mSimpleExoPlayer.getContentBufferedPosition();

            if (mediaControlListener != null) {
                mediaControlListener.setCurTimeString("" + Util.getStringForTime(formatBuilder, formatter, curtime));
                //  > 1000 ? durationUs - 1000 : durationUs
                mediaControlListener.setDurationTimeString("" + Util.getStringForTime(formatBuilder, formatter, durationUs));
                mediaControlListener.setBufferedPositionTime(bufferedPosition);
                mediaControlListener.setCurPositionTime(curtime);
                mediaControlListener.setDurationTime(durationUs);
            }

            mHandler.removeCallbacks(loadStatusRunable);
            int playbackState = mSimpleExoPlayer == null ? Player.STATE_IDLE : mSimpleExoPlayer.getPlaybackState();

            // 播放器未开始播放后者播放器播放结束
            if (playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED) {
                long delayMs = 0;
                // 当正在播放状态时
                if (mSimpleExoPlayer.getPlayWhenReady() && playbackState == Player.STATE_READY) {
                    float playBackSpeed = mSimpleExoPlayer.getPlaybackParameters().speed;
                    if (playBackSpeed <= 0.1f) {
                        delayMs = 1000;
                    } else if (playBackSpeed <= 5f) {
                        // 中间更新周期时间
                        long mediaTimeUpdatePeriodMs = 1000 / Math.max(1, Math.round(1 / playBackSpeed));
                        // 当前进度时间与中间更新周期之间的多出的不足一个中间更新周期时长的时间
                        long surplusTimeMs = curtime % mediaTimeUpdatePeriodMs;
                        // 播放延迟时间
                        long mediaTimeDelayMs = mediaTimeUpdatePeriodMs - surplusTimeMs;
                        if (mediaTimeDelayMs < (mediaTimeUpdatePeriodMs / 5)) {
                            mediaTimeDelayMs += mediaTimeUpdatePeriodMs;
                        }
                        delayMs = playBackSpeed == 1 ? mediaTimeDelayMs : (long) (mediaTimeDelayMs / playBackSpeed);
                    } else {
                        delayMs = 200;
                    }
                } else {
                    // 当暂停状态时
                    delayMs = 1000;
                }
                mHandler.postDelayed(this, delayMs);
            }
        }
    };

其次是悬浮窗的设置,悬浮窗是属于window层的,所以是需要权限的,在这里感谢前辈们的开源代码,这是传送门

我将music的管理以及悬浮窗的管理都放在了service中,这里就涉及到了service的启动。

service

service的启动有两种方式,一种是startService ,另外一种是bundService .


Android app内部悬浮直播窗 安卓悬浮窗播放器_android_02

  1. 绑定开启bindService,使用unbindService解绑关闭。
    bindServic和unbindService一一对应,一个绑定开启,一个解绑结束。

这是一篇关于service声明周期的详细介绍

关于service,要说的就很多了,这里就不岔开话题了。

package com.haodong.musicplayer.myplayer;

import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.ImageView;

import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ui.TimeBar;
import com.haodong.musicplayer.R;
import com.haodong.musicplayer.permission.AVCallFloatView;
import com.haodong.musicplayer.permission.FloatWindowManager;


import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;

import androidx.annotation.Nullable;


/**
 * created by linghaoDo on 2019-08-25
 * <p>
 * description:
 */
public class ExoPlayerService extends Service implements FloatWindowManager.OnWindowLis {
    private static final String TAG = "lhl-->ExoPlayerService";
    private ImageView ivCover;
    private CircleTimeBar timeBar;
    private ImageView ivClose;
    private boolean isWindowDismiss = true;
    private WindowManager windowManager = null;
    private WindowManager.LayoutParams mParams = null;
    private AVCallFloatView floatView = null;
    private OnProgressLis onProgressLis;
    private String mCoverUrl;
    private boolean isListenerIniteed = false;

    public boolean isWindowDismiss() {
        return isWindowDismiss;
    }

    private static final String ACTIVITY_NAME="com.maxwon.mobile.module.business.activities.knowledge.KnowledgeMusicActivity";

    public OnProgressLis getOnProgressLis() {
        return onProgressLis;
    }

    @Subscribe(threadMode = ThreadMode.ASYNC, sticky = true, priority = 8)
    public ExoPlayerService setOnProgressLis(OnProgressLis onProgressLis) {
        this.onProgressLis = onProgressLis;
        LogUtil.i("setOnProgressLis()");
        return this;
    }
    public class MusicBinder extends Binder {
        public ExoPlayerService getService() {
            return ExoPlayerService.this;
        }
    }


    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        LogUtil.i("执行了onStartCommand()");
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onCreate() {
        EventBus.getDefault().register(this);
        LogUtil.i("onCreate");
        super.onCreate();
    }


    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new MusicBinder();
    }

    @Override
    public boolean onUnbind(Intent intent) {
        return super.onUnbind(intent);
    }

    public void initListener() {
        ExoPlayerManager.getDefault().addMediaListener(new ExoPlayerManager.MediaControlListener() {
            @Override
            public void setCurPositionTime(long curPositionTime) {
                if (timeBar != null)
                    timeBar.setPosition(curPositionTime);
                if (onProgressLis != null) {
                    onProgressLis.onPositionChanged(curPositionTime);
                }
            }

            @Override
            public void setDurationTime(long durationTime) {
                if (timeBar != null)
                    timeBar.setDuration(durationTime);
                if (onProgressLis != null)
                    onProgressLis.onDurationChanged(durationTime);
            }

            @Override
            public void setBufferedPositionTime(long bufferedPosition) {
                if (onProgressLis != null) {
                    onProgressLis.onBufferedPositionChanged(bufferedPosition);
                }

            }

            @Override
            public void setCurTimeString(String curTimeString) {
                if (onProgressLis != null) {
                    onProgressLis.onCurTimeStringChanged(curTimeString);
                }
            }

            @Override
            public void setDurationTimeString(String durationTimeString) {
                if (onProgressLis != null) {
                    onProgressLis.onDurationTimeStringChanged(durationTimeString);
                }
            }
        });
        ExoPlayerManager.getDefault().addListener(new Player.EventListener() {
            @Override
            public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
                Log.i(TAG,  "playWhenReady-->" + playWhenReady + "playbackState-->" + playbackState);
                if (onProgressLis != null) {
                    onProgressLis.onPlayerStateChanged(playWhenReady, playbackState);
                }
            }

            @Override
            public void onLoadingChanged(boolean isLoading) {
                if (isLoading) {
                    ExoPlayerManager.getDefault().startListenProgress();
                }
            }
        });
    }


    @Subscribe(sticky = true)
    public void doStart(DoStartEvent startEvent) {
        mCoverUrl = startEvent.getImgUrl();
        if (!isListenerIniteed)
            initListener();
        ExoPlayerManager.getDefault().startRadio(startEvent.getMusicUrl());
    }

    public void show() {
        LogUtil.i("show");
        initFloatingWindow();
    }

    public interface OnProgressLis {
        void onPositionChanged(long curPositionTime);

        void onDurationChanged(long durationTime);

        void onBufferedPositionChanged(long bufferedPosition);

        void onCurTimeStringChanged(String curTimeString);

        void onDurationTimeStringChanged(String durationTimeString);

        void onPlayerStateChanged(boolean playWhenReady, int playbackState);
    }


    @Override
    public void onDestroy() {
        LogUtil.i("music service destoryed");
        EventBus.getDefault().unregister(this);
        ExoPlayerManager.getDefault().releasePlayer();
        super.onDestroy();
    }

    private void initFloatingWindow() {
        if (!isWindowDismiss) {
            Log.e(TAG, "view is already added here");
            return;
        }
        isWindowDismiss = false;
        if (windowManager == null) {
            windowManager = (WindowManager) this.getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
        }

        Point size = new Point();
        windowManager.getDefaultDisplay().getSize(size);
        int screenWidth = size.x;
        int screenHeight = size.y;

        mParams = new WindowManager.LayoutParams();
        mParams.packageName = this.getPackageName();
        mParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
        mParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
        mParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
                | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
        int mType;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            mType = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        } else {
            mType = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
        }
        mParams.type = mType;
        mParams.format = PixelFormat.RGBA_8888;
        mParams.gravity = Gravity.LEFT | Gravity.TOP;
        mParams.x = screenWidth - dp2px(this, 100);
        mParams.y = screenHeight - dp2px(this, 171);
        floatView = new AVCallFloatView(this.getApplicationContext());
        floatView.setParams(mParams);
        floatView.setIsShowing(true);
        floatView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = null;
                try {
                    intent = new Intent(ExoPlayerService.this.getApplicationContext(),Class.forName(ACTIVITY_NAME));
                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }

                PendingIntent pendingIntent =
                        PendingIntent.getActivity(ExoPlayerService.this.getApplicationContext(), 0, intent, 0);
                try {
                    pendingIntent.send();
                } catch (PendingIntent.CanceledException e) {
                    e.printStackTrace();
                }
            }
        });
        /*init*/
        ivClose = floatView.findViewById(R.id.iv_stop);
        ivClose.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ExoPlayerManager.getDefault().pauseRadio();
                dismissWindow();
            }
        });
        ivCover = floatView.findViewById(R.id.iv_cover);
        timeBar = floatView.findViewById(R.id.circle_time_bar);
        timeBar.addListener(new TimeBar.OnScrubListener() {
            @Override
            public void onScrubStart(TimeBar timeBar, long position) {
            }

            @Override
            public void onScrubMove(TimeBar timeBar, long position) {
                ExoPlayerManager.getDefault().seekToTimeBarPosition(position);
            }

            @Override
            public void onScrubStop(TimeBar timeBar, long position, boolean canceled) {

            }
        });
        windowManager.addView(floatView, mParams);
        initListener();
    }

    @Override
    public void showWindow() {
        show();
    }

    @Override
    public void dismissWindow() {
        if (isWindowDismiss) {
            Log.e(TAG, "window can not be dismiss cause it has not been added");
            return;
        }
        isWindowDismiss = true;
        floatView.setIsShowing(false);
        if (windowManager != null && floatView != null) {
            windowManager.removeViewImmediate(floatView);
        }
    }


    private int dp2px(Context context, float dp) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dp * scale + 0.5f);
    }

}

这里面提供了两个思路:

  1. 静态绑定service,可以通过EventBus来与activity进行交互。
  2. 动态绑定service,在activity中拿到service的引用,以此来与activity进行交互,当然,还有诸如广播之类的方法等鞥,在这里我就不一一列举啦

我在这里就用bindService 举例

package com.haodong.musicplayer;

import androidx.appcompat.app.AppCompatActivity;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.Unbinder;

import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;

import com.google.android.exoplayer2.ui.DefaultTimeBar;
import com.google.android.exoplayer2.ui.TimeBar;
import com.google.android.exoplayer2.util.Util;
import com.haodong.musicplayer.event.MusicShowWindowEvent;
import com.haodong.musicplayer.myplayer.Chapter;
import com.haodong.musicplayer.myplayer.ExoPlayerManager;
import com.haodong.musicplayer.myplayer.ExoPlayerService;
import com.haodong.musicplayer.myplayer.LogUtil;
import com.haodong.musicplayer.permission.FloatWindowManager;

import org.greenrobot.eventbus.EventBus;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity implements ExoPlayerService.OnProgressLis{
    @BindView(R.id.iv_music_previous)
    ImageView ivPrevious;
    @BindView(R.id.iv_music_next)
    ImageView ivNext;
    @BindView(R.id.tv_start_time)
    TextView tvStartTime;
    @BindView(R.id.tv_end_time)
    TextView tvEndTime;
    @BindView(R.id.iv_cover)
    ImageView ivCover;
    @BindView(R.id.iv_audio_switch)
    ImageView ivStop;
    @BindView(R.id.exo_progress)
    DefaultTimeBar timeBar;
    private List<Chapter> chapterList = new ArrayList<>();
    @BindView(R.id.btn_show_floating)
    Button btnFloating;
    Unbinder unbinder;
    private ExoPlayerService.MusicBinder myBinder;
    private ExoPlayerService mService;

    private Intent serviceIntent;
    private ServiceConnection serviceConnection;

    @OnClick({R.id.iv_music_previous, R.id.iv_music_next, R.id.iv_audio_switch, R.id.btn_show_floating})
    void onBtnClick(View v) {
        final int id = v.getId();
        if (id == R.id.iv_music_next) {

        } else if (id == R.id.iv_music_previous) {


        } else if (id == R.id.iv_audio_switch) {
            if (!ExoPlayerManager.getDefault().isPaused()){
                ivStop.setImageResource(R.mipmap.ic_knowledge_audio_suspended);
                ExoPlayerManager.getDefault().pauseRadio();
            }else {
                ivStop.setImageResource(R.mipmap.ic_knowledge_audio_play);
                ExoPlayerManager.getDefault().resumeRadio();
            }
        } else if (id == R.id.btn_show_floating) {
            LogUtil.i();
            if (FloatWindowManager.getInstance().applyOrShowFloatWindow(this)) {
                EventBus.getDefault().postSticky(new MusicShowWindowEvent());
            }
        }
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        unbinder= ButterKnife.bind(this);
        initWidget();
    }
    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        setIntent(intent);
    }
    @Override
    public void onBackPressed() {
        /*这是重点*/
        moveTaskToBack(true);
    }

    private void initWidget() {
        bindService();

        timeBar.addListener(new TimeBar.OnScrubListener() {
            @Override
            public void onScrubStart(TimeBar timeBar, long position) {
                tvStartTime.setText(Util.getStringForTime(ExoPlayerManager.getDefault().getFormatBuilder()
                        , ExoPlayerManager.getDefault().getFormatter(), position));
            }

            @Override
            public void onScrubMove(TimeBar timeBar, long position) {
                ExoPlayerManager.getDefault().seekToTimeBarPosition(position);
            }

            @Override
            public void onScrubStop(TimeBar timeBar, long position, boolean canceled) {

            }
        });


    }
    private void bindService() {
        LogUtil.i();
        serviceIntent = new Intent(MainActivity.this, ExoPlayerService.class);
        if(serviceConnection == null) {
            serviceConnection = new ServiceConnection() {

                @Override
                public void onServiceConnected(ComponentName name, IBinder service) {
                    mService= ((ExoPlayerService.MusicBinder)service).getService();
                    String uri = "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3";
                    mService.setOnProgressLis(MainActivity.this).initListener();
                    new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            /*模仿网络请求*/
                            String uri = "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3";
                            ExoPlayerManager.getDefault().startRadio(uri);
                        }
                    },1000);
                }

                @Override
                public void onServiceDisconnected(ComponentName name) {

                }
            };
            bindService(serviceIntent, serviceConnection, BIND_AUTO_CREATE);
        }
    }
    private void unbindService() {
        if(null != serviceConnection) {
            unbindService(serviceConnection);
            serviceConnection = null;
        }
    }

    @Override
    protected void onDestroy() {
        unbindService();
        unbinder.unbind();
        super.onDestroy();
    }

    @Override
    public void onPositionChanged(long curPositionTime) {
        if (timeBar != null)
            timeBar.setPosition(curPositionTime);
    }

    @Override
    public void onDurationChanged(long durationTime) {
        if (timeBar != null)
            timeBar.setDuration(durationTime);
    }

    @Override
    public void onBufferedPositionChanged(long bufferedPosition) {
        if (timeBar != null)
            timeBar.setBufferedPosition(bufferedPosition);
    }

    @Override
    public void onCurTimeStringChanged(String curTimeString) {
        if (tvStartTime != null) {
            tvStartTime.setText(curTimeString);
        }

    }

    @Override
    public void onDurationTimeStringChanged(String durationTimeString) {
        if (tvEndTime != null) {
            tvEndTime.setText(durationTimeString);
        }

    }

    @Override
    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {

    }
}

为了方便activity的管理,我将activity的启动模式设为singleInstance

设置为singleInstance之后,还是有很多坑的,首先,在退出activity的时候,为了不退扎,我们需要屏蔽掉onBackPresee;

@Override
    public void onBackPressed() {
        if (FloatWindowManager.getInstance().applyOrShowFloatWindow(this) && !ExoPlayerManager.getDefault().isStoped()) {
            EventBus.getDefault().postSticky(new ShowWindowEvent());
        } else if (!FloatWindowManager.getInstance().getService().isWindowDismiss()) {
            FloatWindowManager.getInstance().getService().dismissWindow();
        }
        moveTaskToBack(true);
    }

其次,再进入activity的时候,如果是使用startActivity等等,因为activity没有退栈,所以要走onNewIntent,不会再走onCreate()啦,如果你不想你的getIntent调用的东西的一样的,请务必要使用setIntent(intent);重置一下intent.

@Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        setIntent(intent);
        checkComeFrom();
    }

最后,我把所有资源都放在github啦,有需要的可以看看,欢迎star,当然,在我的资源里还有很多不完善的地方,需要大家去踩,(代码资源是足够大家去扩展的)毕竟没有踩过坑不知道坑有多深,只有大家动手写写,印象才够深刻。

写在最后

发现要写好一篇博客还是很难的,真的是字数不够代码来凑,感谢那些乐于分享的大佬,才有了我们这些低级码农的快速成长。也希望自己以后能分享出更多的干货。