딥 다이브 : 미디어 플레이어 모범 사례

Unsplash에 Marcela Laskoski의 사진

MediaPlayer는 사용하기 매우 간단 해 보이지만 복잡성은 표면 바로 아래에 있습니다. 예를 들어 다음과 같이 작성하고 싶을 수 있습니다.

MediaPlayer.create (문맥, R.raw.cowbell) .start ()

이것은 처음과 아마도 두 번째, 세 번째 또는 더 많은 시간에 잘 작동합니다. 그러나 각각의 새로운 MediaPlayer는 메모리 및 코덱과 같은 시스템 리소스를 사용합니다. 앱의 성능과 전체 장치의 성능이 저하 될 수 있습니다.

다행히 몇 가지 간단한 규칙을 따르면 간단하고 안전한 방식으로 MediaPlayer를 사용할 수 있습니다.

간단한 사례

가장 기본적인 경우는 재생하려는 사운드 파일, 아마도 원시 리소스가 있다는 것입니다. 이 경우 소리를 재생할 때마다 재사용하는 단일 플레이어를 만듭니다. 플레이어는 다음과 같이 만들어야합니다 :

private val mediaPlayer = MediaPlayer (). apply {
    setOnPreparedListener {start ()}
    setOnCompletionListener {reset ()}
}

플레이어는 두 개의 청취자로 만들어집니다.

  • 플레이어가 준비된 후 자동으로 재생을 시작하는 OnPreparedListener.
  • 재생이 완료되면 자동으로 리소스를 정리하는 OnCompletionListener입니다.

플레이어가 생성되면 다음 단계는 리소스 ID를 가져와 해당 미디어 플레이어를 사용하여 재생하는 기능을 만드는 것입니다.

funSound (@RawRes rawResId : Int) 재정의 {
    val assetFileDescriptor = context.resources.openRawResourceFd (rawResId)? : 반환
    mediaPlayer.run {
        다시 놓기()
        setDataSource (assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset, assetFileDescriptor.declaredLength)
        PreparingAsync ()
    }
}

이 짧은 방법에는 약간의 일이 있습니다.

  • MediaPlayer가 원시 리소스를 재생하는 데 사용하기 때문에 리소스 ID를 AssetFileDescriptor로 변환해야합니다. 널 검사는 자원이 존재하는지 확인합니다.
  • reset ()을 호출하면 플레이어가 초기화 상태에있게됩니다. 이것은 플레이어의 상태에 관계없이 작동합니다.
  • 플레이어의 데이터 소스를 설정하십시오.
  • PreparingAsync는 플레이어의 재생 준비를하고 UI를 계속 반응하도록 유지합니다. 소스가 준비된 후 연결된 OnPreparedListener가 재생을 시작하기 때문에 작동합니다.

플레이어에서 release ()를 호출하거나 null로 설정하지 않아야합니다. 재사용하고 싶다! 대신 reset ()을 호출하여 사용중인 메모리와 코덱을 비 웁니다.

소리 재생은 다음과 같이 간단합니다.

playSound (R.raw.cowbell)

단순한!

더 많은 Cowbells

한 번에 하나의 사운드를 재생하는 것은 쉽지만 첫 번째 사운드가 계속 재생되는 동안 다른 사운드를 시작하려면 어떻게해야합니까? 이와 같이 playSound ()를 여러 번 호출하면 작동하지 않습니다.

playSound (R.raw.big_cowbell)
playSound (R.raw.small_cowbell)

이 경우 R.raw.big_cowbell이 준비되기 시작하지만 두 번째 호출은 문제가 발생하기 전에 플레이어를 재설정하므로 R.raw.small_cowbell 만들을 수 있습니다.

여러 사운드를 동시에 재생하려면 어떻게해야합니까? 각각에 대해 MediaPlayer를 만들어야합니다. 가장 간단한 방법은 활성 플레이어 목록을 만드는 것입니다. 아마도 이런 식으로 뭔가 :

MediaPlayers 클래스 (컨텍스트 : 컨텍스트) {
    개인 값 컨텍스트 : 컨텍스트 = context.applicationContext
    private val playersInUse = mutableListOf  ()

    private fun buildPlayer () = MediaPlayer (). apply {
        setOnPreparedListener {start ()}
        setOnCompletionListener {
            it.release ()
            playersInUse-= 그것
        }
    }

    funSound (@RawRes rawResId : Int) 재정의 {
        val assetFileDescriptor = context.resources.openRawResourceFd (rawResId)? : 반환
        val mediaPlayer = buildPlayer ()

        mediaPlayer.run {
            playersInUse + = it
            setDataSource (assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset,
                    assetFileDescriptor.declaredLength)
            PreparingAsync ()
        }
    }
}

모든 사운드에는 고유 한 플레이어가 있으므로 R.raw.big_cowbell과 R.raw.small_cowbell을 함께 연주 할 수 있습니다! 완전한!

… 거의 완벽합니다. 코드에 한 번에 재생할 수있는 사운드 수를 제한하는 것은 없으며 MediaPlayer에는 여전히 작동하는 메모리와 코덱이 필요합니다. 미디어가 부족하면 logcat에서 "E / MediaPlayer : Error (1, -19)"만 기록하여 MediaPlayer가 자동으로 실패합니다.

MediaPlayerPool 입력

한 번에 여러 사운드 재생을 지원하려고하지만 메모리 나 코덱이 부족하지는 않습니다. 이러한 것들을 관리하는 가장 좋은 방법은 플레이어 풀을 가지고 소리를 재생하려고 할 때 사용할 풀을 선택하는 것입니다. 코드를 다음과 같이 업데이트 할 수 있습니다.

MediaPlayerPool 클래스 (컨텍스트 : 컨텍스트, maxStreams : Int) {
    개인 값 컨텍스트 : 컨텍스트 = context.applicationContext

    private val mediaPlayerPool = mutableListOf  (). also {
        (i에서 0..maxStreams의 경우) it + = buildPlayer ()
    }
    private val playersInUse = mutableListOf  ()

    private fun buildPlayer () = MediaPlayer (). apply {
        setOnPreparedListener {start ()}
        setOnCompletionListener {recyclePlayer (it)}
    }

    / **
     * 사용 가능한 경우 [MediaPlayer]를 반환합니다.
     * 그렇지 않은 경우는 null
     * /
    private fun requestPlayer () : MediaPlayer? {
        (! mediaPlayerPool.isEmpty ()) {
            mediaPlayerPool.removeAt (0) .also {
                playersInUse + = it
            }
        } 그렇지 않으면 null
    }

    private fun recyclePlayer (미디어 플레이어 : 미디어 플레이어) {
        mediaPlayer.reset ()
        playersInUse-= mediaPlayer
        mediaPlayerPool + = mediaPlayer
    }

    재미있는 playSound (@RawRes rawResId : Int) {
        val assetFileDescriptor = context.resources.openRawResourceFd (rawResId)? : 반환
        val mediaPlayer = requestPlayer ()? : 반환

        mediaPlayer.run {
            setDataSource (assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset,
                    assetFileDescriptor.declaredLength)
            PreparingAsync ()
        }
    }
}

이제 여러 사운드를 한 번에 재생할 수 있으며 메모리가 너무 많거나 코덱이 너무 많지 않도록 최대 동시 플레이어 수를 제어 할 수 있습니다. 또한 인스턴스를 재활용하고 있으므로 가비지 수집기는 실행이 끝난 이전 인스턴스를 모두 정리하기 위해 실행할 필요가 없습니다.

이 방법에는 몇 가지 단점이 있습니다.

  • maxStreams 사운드가 재생 된 후 플레이어가 해제 될 때까지 playSound에 대한 추가 호출은 무시됩니다. 이미 새로운 사운드를 재생하는 데 사용중인 플레이어를 "스틸 링"하여이 문제를 해결할 수 있습니다.
  • playSound 호출과 실제로 사운드 재생 사이에는 상당한 지연이있을 수 있습니다. MediaPlayer가 재사용되고 있지만 실제로는 JNI를 통해 기본 C ++ 기본 객체를 제어하는 ​​얇은 래퍼입니다. 기본 플레이어는 MediaPlayer.reset ()을 호출 할 때마다 삭제되며 MediaPlayer가 준비 될 때마다 다시 만들어야합니다.

플레이어를 재사용 할 수있는 능력을 유지하면서 대기 시간을 개선하는 것은 더 어렵습니다. 다행히 대기 시간이 짧은 특정 유형의 사운드 및 앱의 경우 다음에 살펴볼 또 다른 옵션 인 SoundPool이 있습니다.