Android在应用层提供丰富的音视频多媒体接口,包括MediaPlayer、MediaCodec、AudioTrack、MediaMuxer、MediaExtractor、MediaRecorder、MediaMetadataRetriever、AudioRecord、AudioManager、Camera/Camera2/CameraX等。本文对咱们常用的多媒体API进行介绍,并且结合代码实例,希望能让大家对Android多媒体有深刻认识。

目录

一、MediaPlayer播放器

1、播放状态图

2、初始化播放器

3、监听播放状态

4、获取音视频信息

5、播放操作

二、MediaExtractor解封装器

三、MediaCodec硬编解码

1、MediaCodec状态图

2、创建MediaCodec

3、同步与异步编解码

4、参数配置

5、编解码队列

6、视频解码示例

四、MediaMuxer封装器

五、AudioTrack音频播放


一、MediaPlayer播放器

MediaPlayer是Android提供的多媒体播放器,支持播放音频和视频,可监听播放状态,可获取音视频信息,支持播放常规操作。详情请查看官方文档:MediaPlayer文档

1、播放状态图

MediaPlayer播放状态包括:Idle、Initialized、Preparing、Prepared、Started、Paused、Stopped、PlaybackCompleted、End、Error。有严格时序和状态转换,需要按照时序来调用播放接口,如下图所示:

2、初始化播放器

初始化播放器步骤:创建播放器、设置DataSource、设置显示Surface、设置播放状态监听、准备播放。代码如下:

    fun initPlayer(filePath: String, surface: Surface) {
        try {
            renderFirstFrame = false
            mediaPlayer = MediaPlayer()
            mediaPlayer!!.setDataSource(filePath)
            mediaPlayer!!.setSurface(surface)
            // 监听播放状态
            setListener()
            mediaPlayer!!.prepareAsync()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }

3、监听播放状态

播放状态包括:准备完毕、播放信息、缓冲进度、字幕更新、播放出错、播放完成等。有同步初始化和异步初始化两种方式,一般采用异步初始化,在准备完毕onPrepared时开始播放;监听InfoListener为MEDIA_INFO_VIDEO_RENDERING_START时,说明视频渲染第一帧;监听到播放出错onError时,应该结束播放。相关的播放状态监听如下:

    private fun setListener() {
        mediaPlayer!!.setOnPreparedListener {
            mediaPlayer!!.start()
            playerCallback?.onPrepare()
        }

        mediaPlayer!!.setOnInfoListener { mp: MediaPlayer?, what: Int, extra: Int ->
            (
                    if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
                        if (!renderFirstFrame) {
                            renderFirstFrame = true
                            playerCallback?.onRenderFirstFrame()
                        }
                    })
            return@setOnInfoListener true
        }

        mediaPlayer!!.setOnBufferingUpdateListener { mp, percent ->
            Log.i("MediaPlayer", "buffer percent=$percent")
        }

        mediaPlayer!!.setOnTimedTextListener { mp: MediaPlayer?, text: TimedText? ->
            Log.i("MediaPlayer", "subtitle=" + text?.text)
        }

        mediaPlayer!!.setOnErrorListener { mp: MediaPlayer?, what: Int, extra: Int ->
            return@setOnErrorListener playerCallback?.onError(what, extra)!!
        }

        mediaPlayer!!.setOnCompletionListener {
            playerCallback?.onCompleteListener()
        }
    }

4、获取音视频信息

在onPrepared回调后,可以获取视频宽高、时长等信息。在播放过程中,可以获取当前播放位置。相关代码如下:

    // 当前播放位置
    fun currentPosition(): Int {
        if (mediaPlayer == null)
            return 0
        return mediaPlayer!!.currentPosition
    }
    // 播放时长
    fun duration(): Int {
        if (mediaPlayer == null)
            return 0
        return mediaPlayer!!.duration
    }
    // 视频宽
    fun getVideoWidth(): Int {
        return mediaPlayer!!.videoWidth
    }
    // 视频高
    fun getVideoHeight(): Int {
        return mediaPlayer!!.videoHeight
    }

5、播放操作

播放操作包括:seek拖动、播放/暂停、静音播放、倍速播放、设置音频、切换音轨等。其中,切换音轨或字幕轨,前提是存在多音轨或多字幕轨。相关代码如下:

    // seek拖动
    fun seekTo(position: Int) {
        mediaPlayer?.seekTo(position)
    }
    // 播放/暂停
    fun togglePlay() {
        if (mediaPlayer!!.isPlaying) {
            mediaPlayer!!.pause()
        } else {
            mediaPlayer!!.start()
        }
    }
    // 静音播放
    fun mute() {
        mediaPlayer?.setVolume(0.0f, 0.0f)
    }
    // 播放音量
    fun setVolume(volume: Float) {
        if (volume < 0 || volume > 1)
            return
        mediaPlayer?.setVolume(volume, volume)
    }

    // 倍速播放
    fun setSpeed(speed: Float) {
        if (speed <= 0 || speed > 8)
            return
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
            val params = PlaybackParams()
            params.speed = speed
            mediaPlayer?.playbackParams = params
        }
    }
    // 切换音轨或字幕轨
    fun selectTrack(trackId: Int) {
        mediaPlayer?.selectTrack(trackId)
    }

二、MediaExtractor解封装器

MediaExtractor用于媒体文件的解封装,解析媒体信息、获取音视频流。比如,播放视频流程:解封装——>解码——>渲染播放。示例代码如下:

  MediaExtractor extractor = new MediaExtractor();
  extractor.setDataSource(...);
  int numTracks = extractor.getTrackCount();
  for (int i = 0; i < numTracks; ++i) {
    MediaFormat format = extractor.getTrackFormat(i);
    String mime = format.getString(MediaFormat.KEY_MIME);
    if (weAreInterestedInThisTrack) {
      extractor.selectTrack(i);
    }
  }
  ByteBuffer inputBuffer = ByteBuffer.allocate(...)
  while (extractor.readSampleData(inputBuffer, ...) >= 0) {
    int trackIndex = extractor.getSampleTrackIndex();
    long presentationTimeUs = extractor.getSampleTime();
    ...
    extractor.advance();
  }
 
  extractor.release();
  extractor = null;

由此可见,MediaExtractor使用步骤如下:

  1. 创建MediaExtractor解封装器;
  2. 设置DataSource数据源;
  3. 遍历所有媒体轨道,根据MediaFormat的mimetype选择轨道;
  4. 调用readSampleData读取音视频的数据包,调用advance更新;
  5. 调用release释放资源;

三、MediaCodec硬编解码

Android提供MediaCodec进行硬编码、硬解码,包括硬件芯片厂商的硬编解码、系统内置的软编解码。MediaCodec的效率比FFmpeg软解效率高,速度快,占用CPU少。但是FFmpeg软解的兼容性好,支持更广泛的格式和参数。详情请看文档:MediaCodec

1、MediaCodec状态图

MediaCodec的先后顺序状态包括:Unitialized、Configured、Flushed、Running、End of Stream、(Error)、Released。其中,如果编解码过程中出错,会转移到Error状态。具体的状态迁移如下图所示:

2、创建MediaCodec

Android提供两种方式创建MediaCodec,根据name和type。示例代码如下:

    // 根据名字创建编解码
    MediaCodec.createByCodecName(name)
    // 根据mimetype创建编码器
    MediaCodec.createEncoderByType(mimeType)
    // 根据mimetype创建解码器
    MediaCodec.createDecoderByType(mimeType)

3、同步与异步编解码

MediaCodec以前使用同步方式进行编解码。虽然同步调用比较方便,但是会导致阻塞。示例代码如下:

int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
  if (inputBufferId >= 0) {
    ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
    // 填充待编码/解码数据
    ......
    codec.queueInputBuffer(inputBufferId, ...);
  }
  int outputBufferId = codec.dequeueOutputBuffer(...);
  if (outputBufferId >= 0) {
    ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
    codec.releaseOutputBuffer(outputBufferId, ...);
  }

在Android5.0后,提供异步方式进行编解码,通过设置回调监听实现。示例代码如下:

codec.setCallback(new MediaCodec.Callback() {
  @Override
  void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
    ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
    // 填充待编码/解码数据
    ......
    codec.queueInputBuffer(inputBufferId, ...);
  }
 
  @Override
  void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, ...) {
    ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
    codec.releaseOutputBuffer(outputBufferId, ...);
  }
 
  @Override
  void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {

  }
 
  @Override
  void onError(...) {

  }
 });

4、参数配置

MediaCodec提供一系列参数配置,包括:码率、码率模式、GOP间隔等等。而MediaFormat的参数包括:width、height、frameRate、duration、bitrate、sampleRate、channelCount等。另外,在比较新的API版本有新增参数。

(1) HDR10_PLUS_INFO

在Android10(API 29),新增支持HDR10+,可以设置HDR10+的metadata到mediacodec的输入队列,适用于编解码场景。详情请看文档:PARAMETER_KEY_HDR10_PLUS_INFO

(2) LOW_LATENCY

在Android11(API 30),新增支持低延时解码,如果开启,那么解码器不会在内部缓存多余数据。

5、编解码队列

编解码队列包括:编解码前队列和编解码后队列,以生产者和消费者形式存在。队列是环形缓冲区,会循环使用。从输入端角度,客户端作为生产者,为codec提供编解码前的数据,codec作为消费者取出数据进行编解码;从输出端角度,codec作为生产者,为客户端提供编解码后的数据,客户端作为消费者取出数据去渲染。示意图如下:

6、视频解码示例

以视频解码渲染为例,使用MediaExtractor解封装、MediaCodec解码,并且关联到Surface进行渲染。示例代码如下:

    fun decodeVideo() {
        try {
            // 调用MediaExtractor解析得到MediaFormat
            mediaExtractor!!.setDataSource(mFilePath)
            for (i in 0 until mediaExtractor!!.trackCount) {
                mediaFormat = mediaExtractor!!.getTrackFormat(i)
                mimeType = mediaFormat!!.getString(MediaFormat.KEY_MIME)
                if (mimeType != null && mimeType.startsWith("video/")) {
                    mediaExtractor!!.selectTrack(i)
                    break
                }
            }
            // 创建MediaCodec,配置与启动
            mediaCodec = MediaCodec.createDecoderByType(mimeType)
            mediaCodec!!.configure(mediaFormat, mSurface, null, 0)
            mediaCodec!!.start()

            while (!isRunning) {
                val inputIndex = mediaCodec!!.dequeueInputBuffer(DEQUEUE_TIME)
                if (inputIndex >= 0) {
                    val inputBuffer = mediaCodec!!.getInputBuffer(inputIndex)
                    val sampleSize = mediaExtractor!!.readSampleData(inputBuffer!!, 0)
                    // 待解码数据入队列
                    if (sampleSize < 0) {
                        mediaCodec!!.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
                    } else {
                        mediaCodec!!.queueInputBuffer(inputIndex, 0, sampleSize, mediaExtractor!!.sampleTime, 0)
                        mediaExtractor!!.advance()
                    }
                }
                // 解码后数据出队列
                val outputIndex = mediaCodec!!.dequeueOutputBuffer(bufferInfo, DEQUEUE_TIME)
                if (outputIndex != MediaCodec.INFO_OUTPUT_FORMAT_CHANGED 
                        && outputIndex != MediaCodec.INFO_TRY_AGAIN_LATER
                        && outputIndex != MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                        mediaCodec!!.releaseOutputBuffer(outputIndex, true)
                }
            }
        } catch (e: Exception) {
            Log.e(TAG, "decode error=$e")
        }
    }

四、MediaMuxer封装器

MediaMuxer封装器用于封装音视频流,和MediaExtractor作用刚好相反。比如,录制视频流程:采集音视频流——>编码——>封装。接下来,以MediaExtractor解封装和MediaMuxer封装作为示例,参考代码如下:

   fun muxMediaFile(inputPath: String, outputPath: String): Boolean {
        if (inputPath.isEmpty() || outputPath.isEmpty()) {
            return false
        }
        var happenError = false
        // 1、创建MediaMuxer
        val mediaMuxer = MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
        val mediaExtractor = MediaExtractor()
        try {
            var videoIndex = 0
            var audioIndex = 0
            var audioFormat: MediaFormat? = null
            var videoFormat: MediaFormat? = null
            var finished = false
            val bufferInfo = MediaCodec.BufferInfo()
            val inputBuffer = ByteBuffer.allocate(2 * 1024 * 1024)
            mediaExtractor.setDataSource(inputPath)
            // 遍历所有轨道,根据mimetype选择轨道
            for (i in 0 until mediaExtractor.trackCount) {
                val mediaFormat = mediaExtractor.getTrackFormat(i)
                val mimeType = mediaFormat.getString(MediaFormat.KEY_MIME)
                if (mimeType != null && mimeType.startsWith("video")) {
                    videoIndex = i
                    videoFormat = mediaFormat
                    mediaExtractor.selectTrack(i)
                } else if (mimeType != null && mimeType.startsWith("audio") && audioFormat == null) {
                    audioIndex = i
                    audioFormat = mediaFormat
                    mediaExtractor.selectTrack(i)
                }
            }
            // 2、添加轨道,传入MediaFormat
            if (videoFormat != null) {
                mediaMuxer.addTrack(videoFormat)
            }
            if (audioFormat != null) {
                mediaMuxer.addTrack(audioFormat)
            }
            // 3、开启MediaMuxer
            mediaMuxer.start()

            while (!finished) {
                // 解封装获取音视频流数据
                val sampleSize = mediaExtractor.readSampleData(inputBuffer, 0)
                if (sampleSize > 0) {
                    bufferInfo.size = sampleSize
                    bufferInfo.flags = mediaExtractor.sampleFlags
                    bufferInfo.presentationTimeUs = mediaExtractor.sampleTime
                    // 4、调用MediaMuxer把音视频流重新封装
                    if (mediaExtractor.sampleTrackIndex == videoIndex) {
                        mediaMuxer.writeSampleData(videoIndex, inputBuffer, bufferInfo)
                    } else if (mediaExtractor.sampleTrackIndex == audioIndex) {
                        mediaMuxer.writeSampleData(audioIndex, inputBuffer, bufferInfo)
                    }
                    inputBuffer.flip()
                    mediaExtractor.advance()
                } else if (sampleSize < 0) {
                    finished = true
                }
            }

        } catch (e: Exception) {
            happenError = true
        } finally {
            // 5、释放资源
            mediaMuxer.release()
            mediaExtractor.release()
            return !happenError
        }
    }

由此可见,MediaMuxer的使用步骤如下:

  1. 创建MediaMuxer;
  2. 添加MediaFormat到轨道;
  3. 开启MediaMuxer;
  4. 调用writeSampleData来封装音视频流;
  5. 释放资源;

五、AudioTrack音频播放

AudioTrack是Android在应用层提供的音频播放器。如果对延时有严格要求,可以使用底层提供的OpenSL ES,或者AAudio,而oboe库有对AAudio的封装。其中AAudio通过共享内存,降低延时,提高处理效率。

以MediaExtractor解封装、MediaCodec解码、AudioTrack播放三者结合,看看AudioTrack的代码示例。首先是初始化工作:

    // 初始化MediaExtractor
    private fun parseAudioFormat(path: String): MediaFormat? {
        mediaExtractor = MediaExtractor()
        try {
            mediaExtractor?.setDataSource(path)
            for (i in 0 until mediaExtractor!!.trackCount) {
                val mediaFormat = mediaExtractor!!.getTrackFormat(i)
                val mimeType = mediaFormat.getString(MediaFormat.KEY_MIME)
                if (mimeType != null && mimeType.startsWith("audio")) {
                    mediaExtractor!!.selectTrack(i)
                    return mediaFormat
                }
            }
        } catch (e: Exception) {
            Log.e(TAG, "parseAudioFormat err=$e")
        }
        return null
    }
    // 初始化MediaCodec
    private fun initMediaCodec(mediaFormat: MediaFormat): Boolean {
        val mimeType = mediaFormat.getString(MediaFormat.KEY_MIME)
        mediaCodec = MediaCodec.createDecoderByType(mimeType)
        return try {
            mediaCodec!!.configure(mediaFormat, null, null, 0)
            mediaCodec!!.start()
            true
        } catch (e: Exception) {
            Log.e(TAG, "initMediaCodec err=$e")
            false
        }
    }
    // 初始化AudioTrack
    private fun initAudioTrack(mediaFormat: MediaFormat): Boolean {
        val sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)
        val channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
        val channelConfig = if (channelCount == 1) {
            AudioFormat.CHANNEL_OUT_MONO
        } else  {
            AudioFormat.CHANNEL_OUT_STEREO
        }
        val encoding = AudioFormat.ENCODING_PCM_16BIT
        val bufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, encoding)
        Log.e(TAG, "sampleRate=$sampleRate, channelCount=$channelCount, bufferSize=$bufferSize")

        try {
            val audioFormat = AudioFormat.Builder()
                    .setEncoding(encoding)
                    .setSampleRate(sampleRate)
                    .setChannelMask(channelConfig)
                    .build()
            val audioAttributes = AudioAttributes.Builder()
                    .setLegacyStreamType(AudioManager.STREAM_MUSIC)
                    .build()
            audioTrack = AudioTrack(audioAttributes, audioFormat,
                    bufferSize, AudioTrack.MODE_STREAM, AudioManager.AUDIO_SESSION_ID_GENERATE)
            audioTrack!!.play()
        } catch (e: Exception) {
            Log.e(TAG, "initAudioTrack err=$e")
            return false
        }
        return true
    }

接下来,调用MediaExtractor的readSampleData方法解析音视频流,调用MediaCodec的queueInputBuffer和dequeueOutputBuffer方法进行解码,最后调用AudioTrack的write方法进行播放。需要注意的是,解封装、解码、播放应该分为三个线程,这里只是简单演示使用方法。相关代码如下:

    fun playAudio(path: String) {
        var finished = false
        val data = ByteArray(10 * 1024)
        running = AtomicBoolean(true)
        val bufferInfo = MediaCodec.BufferInfo()
        val mediaFormat = parseAudioFormat(path) ?: return release()
        var result = initMediaCodec(mediaFormat)
        if (!result) {
            return release()
        }
        result = initAudioTrack(mediaFormat)
        if (!result) {
            return release()
        }

        while (!finished) {
            if (!running!!.get()) {
                break
            }
            val inputIndex = mediaCodec!!.dequeueInputBuffer(DEQUEUE_TIME)
            if (inputIndex >= 0) {
                val inputBuffer = mediaCodec!!.getInputBuffer(inputIndex)
                // demux
                val sampleSize = mediaExtractor!!.readSampleData(inputBuffer!!, 0)
                // decode
                if (sampleSize < 0) {
                    mediaCodec!!.queueInputBuffer(inputIndex, 0, 0,
                            0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
                    finished = true
                } else {
                    mediaCodec!!.queueInputBuffer(inputIndex, 0, sampleSize,
                            mediaExtractor!!.sampleTime, mediaExtractor!!.sampleFlags)
                    mediaExtractor!!.advance()
                }
            }

            val outputIndex = mediaCodec!!.dequeueOutputBuffer(bufferInfo, DEQUEUE_TIME)
            // play
            if (outputIndex >= 0) {
                val outputBuffer = mediaCodec!!.getOutputBuffer(outputIndex)
                val size = outputBuffer!!.limit()
                outputBuffer.get(data, outputBuffer.position(), size - outputBuffer.position())
                audioTrack!!.write(data, 0, size)
                mediaCodec!!.releaseOutputBuffer(outputIndex, false)
                SystemClock.sleep(SLEEP_TIME)
            }
        }

        release()
    }

至此,关于Android多媒体的视频播放器、音频播放器、MediaCodec编解码、多媒体封装与解封装器介绍完毕。完整代码与学习音视频,可查看GitHub:FFmpegAndroid

Logo

致力于链接即构和开发者,提供实时互动和元宇宙领域的前沿洞察、技术分享和丰富的开发者活动,共建实时互动世界。

更多推荐