Android蓝牙音乐(基于Android10)

Android蓝牙音乐

项目背景: 展示蓝牙音乐信息(歌曲名称、播放进度、歌手、歌词等等)和控制操作蓝牙音乐

1.监听蓝牙设备状态

注意点 :需要动态注册广播,和动态申请权限,不然会接收不到广播

class BluetoothBroadcastReceiver : BroadcastReceiver() {
    companion object {
        private const val TAG = "BluetoothMusicTool"
    }

    override fun onReceive(context: Context, intent: Intent) {
        log("intent.action :${intent.action}")
        when (intent.action) {
            BluetoothAdapter.ACTION_STATE_CHANGED -> {
                when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0)) {
                    BluetoothAdapter.STATE_ON -> {
                        log("open bluetooth")
                    }
                    BluetoothAdapter.STATE_OFF -> {
                        log("close bluetooth")
                    }
                }
            }

            BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED -> {
                when (intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, 0)) {
                    BluetoothAdapter.STATE_CONNECTED -> {
                        PreferenceCompanion.setBt()
                        log("bluetooth connect device===${PreferenceCompanion.getBt()}")
                    }
                    BluetoothAdapter.STATE_DISCONNECTED -> {
                        PreferenceCompanion.setBt(IBluetoothMusicTool.STATE_DISCONNECT)
                        log("bluetooth disconnect device===${PreferenceCompanion.getBt()}")
                    }
                }
            }
        }
    }
}

申请权限

fun initPermission(activity: Activity?) {
        activity?.parent?.let {
            ActivityCompat.requestPermissions(
                it, arrayOf(
                    Manifest.permission.BLUETOOTH,
                    Manifest.permission.BLUETOOTH_ADMIN,
                    Manifest.permission.ACCESS_FINE_LOCATION,
                    Manifest.permission.READ_PHONE_STATE,
                    Manifest.permission.BLUETOOTH_PRIVILEGED
                ), BLUETOOTH_REQUEST_CODE
            )
        }
    }

2.媒体会话连接管理工具类

class MediaServiceConnection(context: Context, serviceComponent: ComponentName) {
    val isConnected = MutableLiveData<Boolean>()
        .apply { postValue(false) }

    val rootMediaId: String get() = mediaBrowser.root

    val playbackState = MutableLiveData<PlaybackStateCompat>()
        .apply { postValue(EMPTY_PLAYBACK_STATE) }
    val nowPlaying = MutableLiveData<MediaMetadataCompat>()
        .apply { postValue(NOTHING_PLAYING) }

    val transportControls: MediaControllerCompat.TransportControls
        get() = mediaController.transportControls

    private val mediaBrowserConnectionCallback = MediaBrowserConnectionCallback(context)
    private val mediaBrowser = MediaBrowserCompat(
        context,
        serviceComponent,
        mediaBrowserConnectionCallback, null
    ).apply { connect() }
    lateinit var mediaController: MediaControllerCompat

    fun subscribe(parentId: String, callback: MediaBrowserCompat.SubscriptionCallback) {
        mediaBrowser.subscribe(parentId, callback)
    }

    fun unsubscribe(parentId: String, callback: MediaBrowserCompat.SubscriptionCallback) {
        mediaBrowser.unsubscribe(parentId, callback)
    }

    fun sendCommand(command: String, parameters: Bundle?) =
        sendCommand(command, parameters) { _, _ -> }

    fun sendCommand(
        command: String,
        parameters: Bundle?,
        resultCallback: ((Int, Bundle?) -> Unit)
    ) = if (mediaBrowser.isConnected) {
        mediaController.sendCommand(command, parameters, object : ResultReceiver(Handler()) {
            override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
                resultCallback(resultCode, resultData)
            }
        })
        true
    } else {
        false
    }

    private inner class MediaBrowserConnectionCallback(private val context: Context) :
        MediaBrowserCompat.ConnectionCallback() {
        /**
         * Invoked after [MediaBrowserCompat.connect] when the request has successfully
         * completed.
         */
        override fun onConnected() {
            log("onConnect")
            mediaController = MediaControllerCompat(context, mediaBrowser.sessionToken).apply {
                registerCallback(MediaControllerCallback())
            }
            mediaController.playbackState
            isConnected.postValue(true)
        }

        /**
         * Invoked when the client is disconnected from the media browser.
         */
        override fun onConnectionSuspended() {
            isConnected.postValue(false)
        }

        /**
         * Invoked when the connection to the media browser failed.
         */
        override fun onConnectionFailed() {
            isConnected.postValue(false)
        }
    }

    private inner class MediaControllerCallback : MediaControllerCompat.Callback() {

        override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {
            //PlaybackState {state=3, position=53956, buffered position=0, speed=1.0, updated=2876951, actions=55,
            // error code=0, error message=null, custom actions=[], active item id=-1}
            log("state===$state")
            playbackState.postValue(state ?: EMPTY_PLAYBACK_STATE)
        }

        override fun onMetadataChanged(metadata: MediaMetadataCompat?) {
            nowPlaying.postValue(
                if (metadata?.id == null) {
                    NOTHING_PLAYING
                } else {
                    metadata
                }
            )
        }

        override fun onQueueChanged(queue: MutableList<MediaSessionCompat.QueueItem>?) {
        }

        override fun onSessionEvent(event: String?, extras: Bundle?) {
            super.onSessionEvent(event, extras)
        }

        /**
         * Normally if a [MediaBrowserServiceCompat] drops its connection the callback comes via
         * [MediaControllerCompat.Callback] (here). But since other connection status events
         * are sent to [MediaBrowserCompat.ConnectionCallback], we catch the disconnect here and
         * send it on to the other callback.
         */
        override fun onSessionDestroyed() {
            mediaBrowserConnectionCallback.onConnectionSuspended()
        }
    }

    companion object {
        @Volatile
        private var instance: MediaServiceConnection? = null

        fun getInstance(context: Context, serviceComponent: ComponentName) =
            instance ?: synchronized(this) {
                instance ?: MediaServiceConnection(context, serviceComponent)
                    .also { instance = it }
            }
    }
}

@Suppress("PropertyName")
val EMPTY_PLAYBACK_STATE: PlaybackStateCompat = PlaybackStateCompat.Builder()
    .setState(PlaybackStateCompat.STATE_NONE, 0, 0f)
    .build()

@Suppress("PropertyName")
val NOTHING_PLAYING: MediaMetadataCompat = MediaMetadataCompat.Builder()
    .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, "")
    .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, 0)
    .build()

3.连接蓝牙音乐监听状态变化

注意点 获取歌词需要在手机端App上面 开启外界设备蓝牙歌词

interface IBluetoothMusicTool {
	fun init(app: Application)
}
object BluetoothMusicTool : IBluetoothMusicTool {

    private const val TAG = "BluetoothMusicTool"
    
	//蓝牙音乐service
    private const val MEDIA_BROWSER_SERVICE_PACKAGE = "com.android.bluetooth"
    private const val MEDIA_BROWSER_SERVICE =
        "com.android.bluetooth.avrcpcontroller.BluetoothMediaBrowserService"
    private lateinit var app: Application
    private lateinit var mediaBrowser: MediaBrowserCompat
    private lateinit var mediaControllerCompat: MediaControllerCompat
    private const val BLUETOOTH_REQUEST_CODE = 100
    private var isPlaying = false

    private var mediaMetadataCompat = MutableLiveData<MediaMetadataCompat>()

    private var playbackStateCompat = MutableLiveData<PlaybackStateCompat>()
    private var isPlayAudioFocus = false
    private const val UNKNOWN = "未知"

//在Application中初始化蓝牙连接操作
    override fun init(app: Application) {
        BluetoothMusicTool.app = app
        val componentName = ComponentName(MEDIA_BROWSER_SERVICE_PACKAGE, MEDIA_BROWSER_SERVICE)
        mediaBrowser = MediaBrowserCompat(
            app,
            componentName,
            connectionCallbacks,
            null
        )
        mediaBrowser.connect()
        //注册蓝牙状态监听广播
        registerBluetoothBroadcast()
    }
    private fun registerBluetoothBroadcast() {
        val broadcastReceiver = BluetoothBroadcastReceiver()
        val intentFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
        intentFilter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)
        App.get().registerReceiver(broadcastReceiver, intentFilter)
    }


    private val connectionCallbacks: MediaBrowserCompat.ConnectionCallback =
        object : MediaBrowserCompat.ConnectionCallback() {
            override fun onConnected() {
            // Android 7-9的service     com.android.bluetooth.a2dpsink.mbs.A2dpMediaBrowserService
            //蓝牙音乐服务连接成功
                log("bluetooth music connect success")
                mediaControllerCompat = MediaControllerCompat(app, mediaBrowser.sessionToken)
                //注册蓝牙音乐信息状态监听
                if (mediaControllerCompat.metadata != null) {
                    mediaMetadataCompat.value = mediaControllerCompat.metadata
                }                mediaControllerCompat.registerCallback(controllerCallback)
            }

            override fun onConnectionSuspended() {
                log("bluetooth music connect suspend")
            }

            override fun onConnectionFailed() {
                log("bluetooth music connect failed")
            }
        }

    private val controllerCallback: MediaControllerCompat.Callback =
        object : MediaControllerCompat.Callback() {
            override fun onMetadataChanged(metadata: MediaMetadataCompat?) {
//蓝牙音乐信息变化之后在这里进行回调
                if (playbackStateCompat.value?.state == PlaybackStateCompat.STATE_PLAYING) {
                //捕获异常的工具类
                    catchException {
                        metadata?.apply {
                            if (album == currentAlbum) {
                            //音乐信息相同直接返回
                                return@apply
                            }
                            mediaMetadataCompat.value = this
                            //歌词信息 title是kotlin扩展函数
                   //getString(MediaMetadataCompat.METADATA_KEY_TITLE)
                            val title = title
                            //歌曲名称
                  //getString(MediaMetadataCompat.METADATA_KEY_ARTIST)
                            val songName = (artist?.substringBefore("-") ?: artist) ?: UNKNOWN
                            //getString(MediaMetadataCompat.METADATA_KEY_ALBUM)
                            val album = album
                            //歌曲播放总时长
                            //getLong(MediaMetadataCompat.METADATA_KEY_DURATION)
                            val duration = duration
                            //歌手
                            val artist = (artist?.substringAfter("-") ?: artist) ?: UNKNOWN
                            //音频封面
                            val albumArtUri = albumArtUri ?: UNKNOWN
                            var lyric = "暂无歌词"
                            if (title != album) {
                                //有歌词
                                title?.apply {
                                    lyric = this
                                }
                            }
                        }
                    }
                }
            }

            override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {
                state?.let { playInfo ->
                    playbackStateCompat.value = playInfo
                    //更新播放状态
                    // 播放状态和进度
                }
            }
        }
    fun unregisterCallback() {
        mediaControllerCompat.unregisterCallback(controllerCallback)
    }

    fun initPermission(activity: Activity?) {
        activity?.parent?.let {
            ActivityCompat.requestPermissions(
                it, arrayOf(
                    Manifest.permission.BLUETOOTH,
                    Manifest.permission.BLUETOOTH_ADMIN,
                    Manifest.permission.ACCESS_FINE_LOCATION,
                    Manifest.permission.READ_PHONE_STATE,
                    Manifest.permission.BLUETOOTH_PRIVILEGED
                ), BLUETOOTH_REQUEST_CODE
            )
        }
    }

    fun playOrPause() {
        if (PlayControl.get().playbackState.value?.state == PlaybackStateCompat.STATE_PAUSED) {
            play()
        } else {
            pause()
        }
    }
}

4.控制蓝牙音乐


    fun skipToPrevious() {
        catchException {
            mediaControllerCompat.transportControls.skipToPrevious()
        }

    }

    fun skipToNext() {
        catchException {
            mediaControllerCompat.transportControls.skipToNext()
        }

    }

    fun play() {
        catchException {
            mediaControllerCompat.transportControls.play()
        }

    }

    fun pause() {
        catchException {
            mediaControllerCompat.transportControls.pause()
        }

    }

    fun seekTo(@FloatRange(from = 0.0, to = 1.0) pos: Float) {
        catchException {
            val duration = mediaMetadataCompat.value?.duration
            duration?.apply {
                mediaControllerCompat.transportControls.seekTo((this * pos).toLong())
            }
        }
    }

    private fun catchException(command: (() -> Unit)) {
        try {
            command()
        } catch (e: Exception) {
            log("catchException :$e")
        }
    }
  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值