Deep Dive: MediaPlayer Thực tiễn tốt nhất

Ảnh của Marcela Laskoski trên Bapt

MediaPlayer có vẻ đơn giản để sử dụng, nhưng sự phức tạp sống ngay bên dưới bề mặt. Ví dụ, có thể hấp dẫn để viết một cái gì đó như thế này:

MediaPlayer.create (bối cảnh, R.raw.cowbell) .start ()

Điều này hoạt động tốt lần thứ nhất và có thể là lần thứ hai, thứ ba hoặc thậm chí nhiều lần hơn. Tuy nhiên, mỗi MediaPlayer mới tiêu thụ tài nguyên hệ thống, chẳng hạn như bộ nhớ và codec. Điều này có thể làm giảm hiệu suất của ứng dụng của bạn và có thể là toàn bộ thiết bị.

May mắn thay, nó có thể sử dụng MediaPlayer theo cách vừa đơn giản vừa an toàn bằng cách tuân theo một vài quy tắc đơn giản.

Trường hợp đơn giản

Trường hợp cơ bản nhất là chúng tôi có một tệp âm thanh, có lẽ là tài nguyên thô, mà chúng tôi chỉ muốn chơi. Trong trường hợp này, chúng tôi sẽ tạo một người chơi sử dụng lại mỗi lần chúng tôi cần phát âm thanh. Người chơi nên được tạo bằng một cái gì đó như thế này:

private val mediaPlayer = MediaPlayer (). áp dụng {
    setOnPreparedListener {start ()}
    setOnCompletionListener {reset ()}
}

Người chơi được tạo ra với hai người nghe:

  • OnPreparedListener, sẽ tự động bắt đầu phát lại sau khi trình phát đã được chuẩn bị.
  • OnCompletionListener tự động dọn sạch tài nguyên khi phát lại xong.

Với trình phát được tạo, bước tiếp theo là tạo một hàm lấy ID tài nguyên và sử dụng MediaPlayer đó để chơi:

ghi đè lên playSound vui vẻ (@RawRes rawResId: Int) {
    val propertyFileDescriptor = bối cảnh.resource.openRawResourceFd (rawResId) ?: return
    mediaPlayer.run {
        cài lại()
        setDataSource (propertyFileDescriptor.fileDescriptor, propertyFileDescriptor.start Offerset, propertyFileDescriptor.declaredLpm)
        chuẩn bịAsync ()
    }
}

Có một chút xảy ra trong phương pháp ngắn này:

  • ID tài nguyên phải được chuyển đổi thành AssetFileDescriptor vì đây là những gì MediaPlayer sử dụng để phát tài nguyên thô. Kiểm tra null đảm bảo tài nguyên tồn tại.
  • Gọi thiết lập lại () đảm bảo trình phát ở trạng thái Khởi tạo. Điều này hoạt động cho dù người chơi đang ở trạng thái nào.
  • Đặt nguồn dữ liệu cho trình phát.
  • readyAsync chuẩn bị cho người chơi chơi và quay lại ngay lập tức, giữ cho giao diện người dùng phản ứng nhanh. Điều này hoạt động vì OnPreparedListener đính kèm bắt đầu phát sau khi nguồn đã được chuẩn bị.

Điều quan trọng cần lưu ý là chúng tôi không phát hành cuộc gọi () trên trình phát của mình hoặc đặt thành không. Chúng tôi muốn sử dụng lại nó! Vì vậy, thay vì chúng ta gọi reset (), giải phóng bộ nhớ và codec mà nó đang sử dụng.

Phát một âm thanh đơn giản như gọi:

playSound (R.raw.cowbell)

Đơn giản!

Nhiều tiếng chuông

Phát một âm thanh tại một thời điểm rất dễ dàng, nhưng nếu bạn muốn bắt đầu một âm thanh khác trong khi âm đầu tiên vẫn phát thì sao? Gọi playSound () nhiều lần như thế này đã giành được công việc:

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

Trong trường hợp này, R.raw.big_cowbell bắt đầu chuẩn bị, nhưng cuộc gọi thứ hai sẽ đặt lại trình phát trước khi mọi thứ có thể xảy ra, vì vậy chỉ có bạn chỉ nghe thấy R.raw.small_cowbell.

Và nếu chúng ta muốn chơi nhiều âm thanh cùng một lúc thì sao? Chúng tôi cần phải tạo một MediaPlayer cho mỗi người. Cách đơn giản nhất để làm điều này là có một danh sách những người chơi tích cực. Có lẽ một cái gì đó như thế này:

lớp MediaPlayers (bối cảnh: Bối cảnh) {
    bối cảnh val riêng: Context = bối cảnh.applicationContext
    private val playerInUse = mutableListOf  ()

    private fun buildPlayer () = MediaPlayer (). áp dụng {
        setOnPreparedListener {start ()}
        setOnCompletionListener {
            it.release ()
            người chơiInse - = nó
        }
    }

    ghi đè lên playSound vui vẻ (@RawRes rawResId: Int) {
        val propertyFileDescriptor = bối cảnh.resource.openRawResourceFd (rawResId) ?: return
        val mediaPlayer = buildPlayer ()

        mediaPlayer.run {
            người chơiInUse + = nó
            setDataSource (propertyFileDescriptor.fileDescriptor, propertyFileDescriptor.start Offerset,
                    propertyFileDescriptor.declaredLpm)
            chuẩn bịAsync ()
        }
    }
}

Bây giờ, mỗi âm thanh đều có trình phát riêng, nó có thể chơi cả R.raw.big_cowbell và R.raw.small_cowbell cùng nhau! Hoàn hảo!

Chà, gần như hoàn hảo. Không có bất cứ điều gì trong mã của chúng tôi giới hạn số lượng âm thanh có thể phát cùng một lúc và MediaPlayer vẫn cần phải có bộ nhớ và codec để hoạt động. Khi chúng hết, MediaPlayer thất bại một cách âm thầm, chỉ lưu ý đến E / MediaPlayer: Error (1, -19), trong logcat.

Nhập MediaPlayerPool

Chúng tôi muốn hỗ trợ phát nhiều âm thanh cùng một lúc, nhưng chúng tôi không muốn hết bộ nhớ hoặc codec. Cách tốt nhất để quản lý những thứ này là có một nhóm người chơi và sau đó chọn một thứ để sử dụng khi chúng ta muốn phát âm thanh. Chúng tôi có thể cập nhật mã của chúng tôi để được như thế này:

lớp MediaPlayerPool (bối cảnh: Bối cảnh, maxStreams: Int) {
    bối cảnh val riêng: Context = bối cảnh.applicationContext

    private val mediaPlayerPool = mutableListOf  (). cũng {
        cho (tôi trong 0..maxStreams) nó + = buildPlayer ()
    }
    private val playerInUse = mutableListOf  ()

    private fun buildPlayer () = MediaPlayer (). áp dụng {
        setOnPreparedListener {start ()}
        setOnCompletionListener {recyclPlayer (nó)}
    }

    / **
     * Trả về [MediaPlayer] nếu có sẵn,
     * nếu không thì không.
     * /
    requestPlayer (): MediaPlayer? {
        trả về nếu (! mediaPlayerPool.isEmpty ()) {
            mediaPlayerPool.removeAt (0) .also {
                người chơiInUse + = nó
            }
        } khác null
    }

    recyclPlayer vui vẻ riêng tư (mediaPlayer: MediaPlayer) {
        mediaPlayer.reset ()
        người chơiInUse - = mediaPlayer
        mediaPlayerPool + = mediaPlayer
    }

    vui chơiSound (@RawRes rawResId: Int) {
        val propertyFileDescriptor = bối cảnh.resource.openRawResourceFd (rawResId) ?: return
        val mediaPlayer = requestPlayer () ?: return

        mediaPlayer.run {
            setDataSource (propertyFileDescriptor.fileDescriptor, propertyFileDescriptor.start Offerset,
                    propertyFileDescriptor.declaredLpm)
            chuẩn bịAsync ()
        }
    }
}

Bây giờ nhiều âm thanh có thể phát cùng một lúc và chúng tôi có thể kiểm soát số lượng người chơi đồng thời tối đa để tránh sử dụng quá nhiều bộ nhớ hoặc quá nhiều codec. Và, vì chúng tôi đã tái chế các cá thể, người thu gom rác đã giành được phải chạy để dọn sạch tất cả các phiên bản cũ đã chơi xong.

Có một vài nhược điểm của phương pháp này:

  • Sau khi âm thanh maxStream đang phát, mọi cuộc gọi bổ sung tới playSound sẽ bị bỏ qua cho đến khi người chơi được giải phóng. Bạn có thể giải quyết vấn đề này bằng cách ăn cắp một người chơi mà LỚN đã sử dụng để phát ra âm thanh mới.
  • Có thể có độ trễ đáng kể giữa việc gọi playSound và thực sự phát âm thanh. Mặc dù MediaPlayer đang được sử dụng lại, nhưng nó thực sự là một trình bao bọc mỏng điều khiển một đối tượng gốc C ++ bên dưới thông qua JNI. Trình phát gốc bị hủy mỗi khi bạn gọi MediaPlayer.reset () và nó phải được tạo lại bất cứ khi nào MediaPlayer được chuẩn bị.

Cải thiện độ trễ trong khi duy trì khả năng tái sử dụng người chơi là khó thực hiện hơn. May mắn thay, đối với một số loại âm thanh và ứng dụng yêu cầu độ trễ thấp, có một tùy chọn khác mà chúng tôi sẽ xem xét trong lần tới: SoundPool.