Android音视频入门(一):音频的录制和播放

Android音视频入门(一):音频的录制和播放

一、前言

当我们使用各种播放器,系统API来完成音视频播放和录制的时候,其实底层已经帮我做了很多看不到的工作。比如如何进行采集、编解码、合成、压缩等一系列工作,各种库其实都已经帮我实现了,我们只需要按规范调用API来完成我们想要的功能。但是在未来的学习过程中我们会一一的深入内部去探讨各个知识点。

下面是整理的关于android音视频开发的学习脑图,分别把采集、渲染、编码、传输相关的知识点拆分了下,通过这个图可以大概的了解目前的学习位置。

知识大概
image

demo截屏

image

image

二、使用系统API完成音视频采集和播放

2.1 播放音视频文件

在android开发中系统的api已经提供了一个叫MediaRecorder的类给开发者来完成音视频的播放,底层的一系列细节都被封装了起来,所以我们现在使用api完成体验一下成功的感觉。

2.1.1 MediaPlayer 概览

MediaPlayer官方介绍

Android 多媒体框架支持播放各种常见媒体类型,以便您轻松地将音频、视频和图片集成到应用中。您可以使用 MediaPlayer API,播放存储在应用资源(原始资源)内的媒体文件、文件系统中的独立文件或者通过网络连接获得的数据流中的音频或视频。

下面我们使用MediaPlayer来完成音视频的播放,播放源支持网络文件和本地文件。

配置清单文件的权限

    <uses-permission android:name="android.permission.INTERNET" />
    
    //唤醒锁定权限 - 如果播放器应用需要防止屏幕变暗或处理器进入休眠状态,或者要使用 MediaPlayer.setScreenOnWhilePlaying() 或 MediaPlayer.setWakeMode() 方法,则您必须申请此权限。
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    
    

部分核心代码

 //播放视频
 var mediaPlayer: MediaPlayer? = MediaPlayer()
    private fun startMedia() {
        if (!mediaPlayer?.isPlaying!!) {
            //设置播放源 支持本地文件和网络文件
            mediaPlayer?.setDataSource("https://rbv01.ku6.com/wifi/o_1dv2knrei18ju12l7l2vgj210oe11kvs")
            //开始异步准备
            mediaPlayer?.prepareAsync()
            //设置监听
            mediaPlayer?.setOnPreparedListener {
                //开始播放
                mediaPlayer?.start()
            }
        }
    }

    //播放音频
    private fun startAudio() {
        if (!mediaPlayer?.isPlaying!!) {
            val path = "$filesDir/record_mp3/vick.aac"
            mediaPlayer?.setDataSource(this, Uri.parse(path))
            mediaPlayer?.prepareAsync()
            mediaPlayer?.setOnPreparedListener {
                mediaPlayer?.start()
            }
        }
    }

2.1.2 AudioTrack 概览

AudioTrack只支持wav格式的音频文件,因为wav格式的音频文件大部分都是PCM流。AudioTrack不创建解码器,所以只能播放不需要解码的wav文件。

MediaPlayer在framework层还是会创建AudioTrack,把解码后的PCM数流传递给AudioTrack,AudioTrack再传递给AudioFlinger进行混音,然后才传递给硬件播放,所以是MediaPlayer包含了AudioTrack。

部分核心代码

class MeAudioTrack : PlayerAudioI {
    private var audioData: ByteArray? = null
    var audioTrack: AudioTrack? = null
    override fun initData() {
        //通过 write 写文件

    }

    override fun start(filePath: String) {
        //读文件
        stop()
//        play1(filePath)
        playInModeStream(filePath)


    }

    var mHandler: Handler = Handler()
    var mRunnable: Runnable = Runnable {
        playInModeStatic()
    }

    /**
     * 播放,使用stream模式
     */
    private fun playInModeStream(path: String) {
        /*
        * SAMPLE_RATE_INHZ 对应pcm音频的采样率
        * channelConfig 对应pcm音频的声道
        * AUDIO_FORMAT 对应pcm音频的格式
        * */
        val channelConfig = AudioFormat.CHANNEL_OUT_MONO
        val minBufferSize =
            AudioTrack.getMinBufferSize(SAMPLE_RATE_INHZ, channelConfig, AUDIO_FORMAT)
        audioTrack = AudioTrack(
            AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_MEDIA)
                .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                .build(),
            AudioFormat.Builder().setSampleRate(SAMPLE_RATE_INHZ)
                .setEncoding(AUDIO_FORMAT)
                .setChannelMask(channelConfig)
                .build(),
            minBufferSize,
            AudioTrack.MODE_STREAM,
            AudioManager.AUDIO_SESSION_ID_GENERATE
        )
        audioTrack?.play()

        val file = File(path)
        try {
            val fileInputStream = FileInputStream(file)
            Thread(Runnable {
                try {
                    val tempBuffer = ByteArray(minBufferSize)
                    while (fileInputStream.available() > 0) {
                        val readCount = fileInputStream.read(tempBuffer)
                        if (readCount == AudioTrack.ERROR_INVALID_OPERATION || readCount == AudioTrack.ERROR_BAD_VALUE) {
                            continue
                        }
                        if (readCount != 0 && readCount != -1) {
                            audioTrack?.write(tempBuffer, 0, readCount)
                        }
                    }
                } catch (e: IOException) {
                    e.printStackTrace()
                }
            }).start()

        } catch (e: IOException) {
            e.printStackTrace()
        }

    }

    private fun playInModeStatic() {
        audioTrack = AudioTrack(
            AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_MEDIA)
                .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                .build(),
            AudioFormat.Builder().setSampleRate(22050)
                .setEncoding(AudioFormat.ENCODING_PCM_8BIT)
                .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
                .build(),
            audioData?.size!!,
            AudioTrack.MODE_STATIC,
            AudioManager.AUDIO_SESSION_ID_GENERATE
        )
        audioTrack?.write(audioData, 0, audioData?.size!!)
        audioTrack?.play()
    }

    private fun play1(path: String) {
        // static模式,需要将音频数据一次性write到AudioTrack的内部缓冲区
        Thread {
            audioData = FileUtils.readFile2Bytes(path)
            mHandler.post(mRunnable)
        }.run { start() }

    }

    override fun pause() {
    }

    override fun stop() {
        audioTrack?.stop()
        audioTrack?.release()
    }

}

2.2 采集音视频

Android提供了两套音频采集的API,MediaRecorder 和 AudioRecord前者是一个更加上层一点的API。

2.2.1 MediaRecorder

MediaRecorder 概览

MediaRecorder类似于mediaPlayer是一个偏向上层的api,支持音频的采集和视频的采集。

MediaRecorder官方介绍

部分核心代码

 private var mediaRecorder: MediaRecorder? = null
    override fun start() {
        mediaRecorder = MediaRecorder()
        //输出目录
        mediaRecorder?.setOutputFile(getPath())
        //设置采集源
        mediaRecorder?.setAudioSource(MediaRecorder.AudioSource.MIC)
        //设置输出格式 3GPP media file format
        mediaRecorder?.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
        //编解码的格式 AMR (Narrowband) audio codec 
        mediaRecorder?.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
        mediaRecorder?.prepare()
        mediaRecorder?.start()
    }

    override fun stop() {
        mediaRecorder?.stop()
        mediaRecorder?.release()
        mediaRecorder = null

    }
    
    private fun getPath(): String {
        val path =
            mContext?.getExternalFilesDir("media").toString() + File.separator
        mFileName = System.currentTimeMillis().toString() + ".aac"
        Log.i("VICK", path + mFileName)
        mFilePath = path + mFileName
        return mFilePath
    }

2.2.2 AudioRecord采集

部分核心代码

class AudioRecorder : AudioRecorderI {


    /**
     * 采样率,现在能够保证在所有设备上使用的采样率是44100Hz, 但是其他的采样率(22050, 16000, 11025)在一些设备上也可以使用。
     */
    val SAMPLE_RATE_INHZ = 44100

    /**
     * 声道数。CHANNEL_IN_MONO and CHANNEL_IN_STEREO. 其中CHANNEL_IN_MONO是可以保证在所有设备能够使用的。
     */
    val CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO
    /**
     * 返回的音频数据的格式。 ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, and ENCODING_PCM_FLOAT.
     */
    val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT

    var isRecord = false
    var mContext: Context? = null
    override fun getFilePath(): String {
        return mFilePath
    }

    var mFileName = ""
    var mFilePath = ""
    override fun getFileName(): String {
        return mFileName
    }

    override fun initData(context: Context) {
        mContext = context
    }

    var audioRecord: AudioRecord? = null
    override fun start() {
        //获得最小缓冲区大小,用于记录音频所用。
        val minBufferSize = getMinBufferSize(
            44100,
            AudioFormat.CHANNEL_IN_MONO,
            AudioFormat.ENCODING_PCM_16BIT
        )
        //构建录音对象
        audioRecord = AudioRecord(
            MediaRecorder.AudioSource.MIC, 44100, AudioFormat.CHANNEL_IN_MONO,
            AudioFormat.ENCODING_PCM_16BIT, minBufferSize
        )
        mFileName = System.currentTimeMillis().toString() + ".acc"
        val byteArray = ByteArray(minBufferSize)
        val file = File(mContext?.getExternalFilesDir("media"), mFileName)
        mFilePath = file.toString()
        audioRecord?.startRecording()
        isRecord = true
        //开启线程写入录音数据
        Thread {
            var outputStream: FileOutputStream? = null
            try {
                outputStream = FileOutputStream(file)
                while (isRecord) {
                    //audioRecord通过  read()   方法来读取数据
                    val read = audioRecord?.read(byteArray, 0, minBufferSize)
                    //不出错就写入文件
                    if (AudioRecord.ERROR_INVALID_OPERATION != read) {
                        outputStream.write(byteArray)
                    }
                }

            } catch (e: Exception) {
                e.printStackTrace()
            } finally {
                outputStream?.close()
            }


        }.start()
    }

    override fun pause() {
    }

    override fun stop() {
        isRecord = false
        audioRecord?.stop()
        audioRecord?.release()
        audioRecord = null
    }

    override fun destroy() {
    }

}

2.2.3 采集相机数据

SurfaceView或TextureView采集相机数据

class CmaraActivity : AppCompatActivity(), SurfaceHolder.Callback {
    override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {


    }

    override fun surfaceDestroyed(holder: SurfaceHolder?) {
    }

    override fun surfaceCreated(holder: SurfaceHolder?) {
        camera?.setPreviewDisplay(holder)
        camera?.startPreview()

    }


    var surfaceView: SurfaceView? = null
    var textureView: TextureView? = null
    var camera: android.hardware.Camera? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.camera_activity)
        surfaceView = findViewById(R.id.surface)
        textureView = findViewById(R.id.texture)
        camera = android.hardware.Camera.open()
        camera?.setDisplayOrientation(90)
        val parameters = camera?.parameters
        parameters?.pictureFormat = ImageFormat.NV21
        camera?.parameters = parameters
        camera?.setPreviewCallback(object : android.hardware.Camera.PreviewCallback {
            override fun onPreviewFrame(data: ByteArray?, camera: android.hardware.Camera?) {
                Log.i("vick", data.toString())
            }

        })

//        surfaceView?.holder?.addCallback(this)
        textureView?.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
            override fun onSurfaceTextureSizeChanged(
                surface: SurfaceTexture?,
                width: Int,
                height: Int
            ) {

            }

            override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) {
            }

            override fun onSurfaceTextureDestroyed(surface: SurfaceTexture?): Boolean {
                camera?.release()
                return false
            }

            override fun onSurfaceTextureAvailable(
                surface: SurfaceTexture?,
                width: Int,
                height: Int
            ) {
                camera?.setPreviewTexture(surface)
                camera?.startPreview()
            }

        }
    }


}

三、总结

播放

目前知道系统提供给开发者的api有MediaPlayer一个高度封装的类,可以播放器本地音视频文件或者网络文件,使用AudioTrack可以播放原始的音频文件。

采集

MediaRecorder和AudioRecord可以采集音频文件,MediaRecorder 底层封装了AudioRecord,比如直播需要传输的话就需要用它来做。

github下载地址

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,968评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,682评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,254评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,074评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,964评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,055评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,484评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,170评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,433评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,512评论 2 308
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,296评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,184评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,545评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,150评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,437评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,630评论 2 335

推荐阅读更多精彩内容