Kotlin

[android : kotlin] 코틀린 Notification MediaStyle 사용시 SeekBar 를 사용자가 사용할 수 있게 하는 방법

안드로이드 10부터 MediaStyle의 Notificaiotn(알림)을 사용할 때 SeekBar( 탐색 막대)가 표시됩니다. 그렇다면 단순이 플레이가 어디까지 되고 있는지 확인용으로 사용하는 것 뿐만 아니라 사용자가 SeekBar를 조절할 수 있게 하는 방법에 대해 알아봅니다.

 

MediaStyle 알림의 탐색 막대

Android 10부터 MediaStyle 알림에 탐색 막대가 표시됩니다. 탐색 막대는 PlaybackState.getPosition()으로부터 재생 진행 상황을 표시하며, 경우에 따라 재생 프로그램에서 위치를 탐색하는 데 사용할 수도 있습니다. 탐색 막대의 모양과 동작은 다음 규칙에 의해 제어됩니다.

  • 활성 MediaSession 및 지속 시간(MediaMetadata.METADATA_KEY_DURATION으로 지정)이 0보다 크면 탐색 막대가 나타납니다 즉, 라이브스트림 및 라디오 방송과 같은 불확실한 스트림에는 막대가 나타나지 않습니다.
  • 세션이 ACTION_SEEK_TO를 구현하면 사용자는 탐색 막대를 드래그하여 재생 위치를 제어할 수 있습니다.

 

 

ACTION_SEEK_TO 구현하는 방법에 대해 알아봅니다. 샘플코드가 없으니 기술검토를 하게 됩니다.

활성 >MediaSession 및 지속 시간(MediaMetadata.METADATA_KEY_DURATION으로 지정) 설정 방법에 대해서는 글 하단에 [연관 글 더보기]를 참고하세요.

 

미디어 세션을 만들 때 PlaybackStateCompat.ACTION_SEEK_TO를 추가해 주어야합니다. 아래 코드 스니펫을 보면 PlaybackStateCompat 빌더를 만들면서 setAcion메서드에 추가해 줍니다.

class MyMusicPlayerService : MediaBrowserServiceCompat() {

    private lateinit var exoPlayer: SimpleExoPlayer

    private lateinit var mediaSession: MediaSessionCompat

    override fun onCreate() {
        Log.e(TAG, "##RealMusicPlayerService## call onCreate() ")
        super.onCreate()
    
    
    
    
   	      mediaSession = MediaSessionCompat(this, "MyMusicPlayerService")
            .apply {
                // Enable callbacks from MediaButtons and TransportControls
                setFlags(
                    MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
                            or MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS
                            or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
                )

                // Set an initial PlaybackState with ACTION_PLAY, so media buttons can start the player
                stateBuilder = PlaybackStateCompat.Builder()
                    .setActions(
                        PlaybackStateCompat.ACTION_PLAY
                                or PlaybackStateCompat.ACTION_STOP
                                or PlaybackStateCompat.ACTION_PAUSE
                                or PlaybackStateCompat.ACTION_SEEK_TO
                                or PlaybackStateCompat.ACTION_SKIP_TO_NEXT
                                or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
                                or PlaybackStateCompat.ACTION_PLAY_PAUSE
                    )
                    //.setState(PlaybackStateCompat.STATE_STOPPED,0,1)

                //Set our PLaybackState for the MediaSession
                setPlaybackState(stateBuilder.build())

                // MySessionCallback() has methods that handle callbacks from a media controller
                setCallback(MyMediaSessionCallback())

                // Set the session's token so that client activities can communicate with it.
                setSessionToken(sessionToken)

                val activityIntent: Intent = Intent(applicationContext, MainActivity::class.java)
                setSessionActivity(
                    PendingIntent.getActivity(
                        applicationContext,
                        0,
                        activityIntent,
                        0
                    )
                )

                //미디어 세션을 구성 할 때와 재생을 시작하기 전에 설명서에 따라 setActive(true)를 호출해야합니다.
                isActive = true
            }
     
    }        

MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder(); 생성시 METADATA_KEY_DURATION값을 적용해주어야 SeekBar가 노출됩니다. 자세한 정보는 아래 연관글 참고하세요.

builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, player.getDuration());

 

다음 작업으로 미디어세션 콜백(MediaSessionCompat.Callback())을 만들때 onSeekTo 메서드를 오버라이드하여 처리해야하는데 이때 seekBar의 position값을 미디어 플레이어에 적용해주어야 합니다. 

그리고 seekBar 이동한 위치에 고정시키기 위해서 미디어세션의 setPlaybakState 정보를 업데이트해야합니다. 현재위치로 이동하기 위한 조치입니다.  그리고 또한 onPlay(), onPause() , onStop(), 등등메서드 안에서도 현재위치를 잡아줘야 합니다. 그렇지않으면 노래 재생이 0부터 다시 시작됩니다. (PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN)인 경우가 이에 해당합니다.  저는 exoPlayer를 사용함으로 exoPlayer의 currentPosition값을 적용해줍니다.

 

        override fun onSeekTo(pos: Long) {
            Log.e(TAG, "#### call MyMediaSessionCallback() : onSeekTo")
            super.onSeekTo(pos)
            exoPlayer.seekTo(pos)
            
            mediaSession.setPlaybackState(
                stateBuilder.setState(
                    PlaybackStateCompat.STATE_PLAYING,
                    pos, 1F
                ).build()
            )            
        }
                mediaSession.setPlaybackState(
                    stateBuilder.setState(
                        PlaybackStateCompat.STATE_PLAYING,
                        exoPlayer.currentPosition, 1F
                    ).build()
                )
    inner class MyMediaSessionCallback : MediaSessionCompat.Callback() {
        var currentState = PlaybackStateCompat.STATE_STOPPED
      
//        @RequiresApi(Build.VERSION_CODES.O)
        override fun onPlay() {
            Log.e(TAG, "#### call MyMediaSessionCallback() : onPlay")
            if(!exoPlayer.playWhenReady) {


                var item = SongLib.getCurrent()


                updateMetadata(item)

                prepareToPlay(item.contentUri)

                //Log.e(TAG, "#### call MyMediaSessionCallback() : exoPlayer.duration: ${exoPlayer.duration}")

                if (!audioFocusRequested) {
                    audioFocusRequested = true
                    val audioFocusResult: Int
                    audioFocusResult = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                        audioManager.requestAudioFocus(audioFocusRequest)
                    } else {
                        audioManager.requestAudioFocus(
                            audioFocusChangeListener,
                            AudioManager.STREAM_MUSIC,
                            AudioManager.AUDIOFOCUS_GAIN
                        )
                    }

                    if (audioFocusResult != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) return
                }


                mediaSession.isActive = true  //문제생기문 빼자

        //                mediaPlayer.setOnPreparedListener {
        //                    mediaPlayer.start()
        //                    Log.e(TAG, "##### DURATION" + mediaPlayer.duration.toString())
        //                }
        //                mediaPlayer.prepareAsync()

                exoPlayer.playWhenReady = true

                //문제생기문 빼자
                registerReceiver(
                    becomingNoisyReceiver,
                    IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
                )

                mediaSession.setPlaybackState(
                    stateBuilder.setState(
                        PlaybackStateCompat.STATE_PLAYING,
                        PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1F
                    ).build()
                )
                currentState = PlaybackStateCompat.STATE_PLAYING
                //문제생기문 빼자

                //getNotificaiton()
                refreshNotificationAndForegroundStatus(currentState)

            }
 
            super.onPlay()
        }

        override fun onPlayFromUri(uri: Uri?, extras: Bundle?) {
            Log.e(TAG, "#### call MyMediaSessionCallback() : onPlayFromUri :")
            super.onPlayFromUri(uri, extras)
            if(!exoPlayer.playWhenReady) {
 

                updateMetadata(SongLib.getCurrent())

                prepareToPlay(uri)

                if (!audioFocusRequested) {
                    audioFocusRequested = true
                    val audioFocusResult: Int
                    audioFocusResult = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                        audioManager.requestAudioFocus(audioFocusRequest)
                    } else {
                        audioManager.requestAudioFocus(
                            audioFocusChangeListener,
                            AudioManager.STREAM_MUSIC,
                            AudioManager.AUDIOFOCUS_GAIN
                        )
                    }

                    if (audioFocusResult != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) return
                }


                mediaSession.isActive = true  //문제생기문 빼자

//                mediaPlayer.setOnPreparedListener {
//                    mediaPlayer.start()
//                    Log.e(TAG, "##### DURATION" + mediaPlayer.duration.toString())
//                }
//                mediaPlayer.prepareAsync()


                //문제생기문 빼자
                registerReceiver(
                    becomingNoisyReceiver,
                    IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
                )

                mediaSession.setPlaybackState(
                    stateBuilder.setState(
                        PlaybackStateCompat.STATE_PLAYING,
                        PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1F
                    ).build()
                )
                currentState = PlaybackStateCompat.STATE_PLAYING
                //문제생기문 빼자

                //getNotificaiton()
                refreshNotificationAndForegroundStatus(currentState)

            }
        }



        override fun onStop() {
            Log.e(TAG, "#### call MyMediaSessionCallback() : onStop")
            val am = applicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
 

            if (exoPlayer.playWhenReady) {
                exoPlayer.playWhenReady = false
                unregisterReceiver(becomingNoisyReceiver)
            }


            if (audioFocusRequested) {
                audioFocusRequested = false

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    audioManager.abandonAudioFocusRequest(audioFocusRequest)
                } else {
                    audioManager.abandonAudioFocus(audioFocusChangeListener)
                }
            }

            // Set the session inactive  (and update metadata and state)
            mediaSession.isActive = false


            mediaSession.setPlaybackState(
                stateBuilder.setState(
                    PlaybackStateCompat.STATE_STOPPED,
                    PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1F
                ).build()
            )
            currentState = PlaybackStateCompat.STATE_STOPPED

            refreshNotificationAndForegroundStatus(currentState)

            // Take the service out of the foreground
            //service.stopForeground(false)
            //stopForeground(false)

            // Stop the service
            stopSelf()

            super.onStop()
        }

        override fun onPause() {
            Log.e(TAG, "#### call MyMediaSessionCallback() : onPause")
            super.onPause()

            val am = applicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
 
            if (exoPlayer.playWhenReady) {
                exoPlayer.playWhenReady = false
                unregisterReceiver(becomingNoisyReceiver)
            }


            mediaSession.setPlaybackState(
                stateBuilder.setState(
                    PlaybackStateCompat.STATE_PAUSED,
                    PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1F
                ).build()
            );
            currentState = PlaybackStateCompat.STATE_PAUSED

            // Take the service out of the foreground, retain the notification
            //stopForeground(false)

            refreshNotificationAndForegroundStatus(currentState)
        }


        override fun onSkipToPrevious() {
            Log.e(TAG, "#### call MyMediaSessionCallback() : onSkipToPrevious")
  
            var item = SongLib.getPrevious()
            updateMetadata(item)


            refreshNotificationAndForegroundStatus(currentState)

            prepareToPlay(item.contentUri)

            super.onSkipToPrevious()
        }
        override fun onSkipToNext() {
            var item = SongLib.getNext()
            updateMetadata(item)
            Log.e(TAG, "#### call MyMediaSessionCallback() : onSkipToNext")
            refreshNotificationAndForegroundStatus(currentState)

            prepareToPlay(item.contentUri)
            super.onSkipToNext()
        }

        override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
            super.onPlayFromMediaId(mediaId, extras)
            //here extras is empty
            Log.e(TAG, "#### call MyMediaSessionCallback() : onPlayFromMediaId")
            //playTrack(musicRepository.getTrackByIndex(Integer.parseInt(mediaId)))
        }

        override fun onSeekTo(pos: Long) {
            Log.e(TAG, "#### call MyMediaSessionCallback() : onSeekTo")
            super.onSeekTo(pos)
            exoPlayer.seekTo(pos)
            
            mediaSession.setPlaybackState(
                stateBuilder.setState(
                    PlaybackStateCompat.STATE_PLAYING,
                    pos, 1F
                ).build()
            )            
        }

        override fun onSetRating(rating: RatingCompat?) {
            Log.e(TAG, "#### call MyMediaSessionCallback() : onSetRating")
            super.onSetRating(rating)
        }

    }

 

[build.gradle(:app)] 

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 29

    defaultConfig {
        applicationId "edu.kotlin.study"
        minSdkVersion 23
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    // For Java compilers:
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'

    implementation "androidx.media:media:1.2.0"
    implementation "androidx.recyclerview:recyclerview:1.1.0"
    //implementation "com.android.support:support-media-compat:29.+"

    // full exoplayer library
    //implementation 'com.google.android.exoplayer:exoplayer:2.11.5'
    implementation 'com.google.android.exoplayer:exoplayer-core:2.11.5'

    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

}

 

[REFERENCE]

https://developer.android.com/about/versions/10/features#media-notifications

미디어 컨트롤로 알림 만들기

stackoverflow.com/questions/56592386/how-can-we-achieve-seekable-progress-bar-in-android-q-os-programatically

android-developers.googleblog.com/2020/08/playing-nicely-with-media-controls.html

 

 

[연관 글 더보기]

[android : kotlin] MediaStore.Audio.Media로 부터 Uri를 통해 duration (노래 총 재생시간) 가져오는 방법

[android : kotlin] 코틀린 Notification MediaStyle 사용시 SeekBar 설정 및 해제 하는 방법

[android : kotlin] 코틀린 Notification MediaStyle 사용시 앨범 자켓(이미지)

[android : kotlin] 코틀린 RecyclerView 클릭시 미디어 재생 하는 방법 : MediaController ,SimpleExoPlayer

[android : kotlin] 코틀린 Notification setShowActionsInCompactView 사용 예제 : MediaStyle

[프로그래밍/Kotlin] – [android : kotlin] 코틀린 Notification addAction 추가하는 방법 : Notification.Action.Builder

[프로그래밍/Kotlin] – [android : kotlin] 코틀린 Notification 사용 예제

[프로그래밍/Android] – 잠금화면에 알림내용(NotificationCompat) 노출하기 (to show content in lock screen

 

[참고 하면 도움이 될만한 소스 코드]

github.com/AntennaPod/AntennaPod/blob/9df14af284bbaf719c21657c66a75478a874194e/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java#L1080

www.codota.com/web/assistant/code/rs/5c7cb5bfac38dc0001e43499#L106

qiita.com/CAIOS/items/100025926550cca483a5

 

Leave a Reply

error: Content is protected !!