前言
音頻或者視頻,是今天互聯(lián)網(wǎng)上被使用得最廣泛也最受歡迎的信息媒介露泊,可以肯定這個趨勢為未來很長一段時間都不會改變散址,因此對于開發(fā)者而言乖阵,深入的了解這塊內(nèi)容是很有必要的。
音視頻的技術分割
無論是音頻或者視頻预麸,他們在互聯(lián)網(wǎng)上被使用的主要是:創(chuàng)造瞪浸,存儲,播放吏祸。而這里面所涉及的技術就是音視頻數(shù)據(jù)的創(chuàng)造和獲取对蒲,數(shù)據(jù)的編碼和存儲,數(shù)據(jù)的解碼和播放贡翘,以及音視頻本身的協(xié)議格式
視音頻的協(xié)議
音視頻的協(xié)議非常重要蹈矮,這是他們能在互聯(lián)網(wǎng)上廣泛傳播的基礎。
當然其實協(xié)議可能是封裝協(xié)議比如mp4,flac,mp3,mkv,flv等鸣驱,封裝協(xié)議指的是將一個音頻泛鸟,一個視頻,以及其他信息統(tǒng)一封裝成一個文件的規(guī)則協(xié)議踊东,假設我們下載了一部電影《東京不熱》 mp4文件北滥,里面就至少包含了視頻,音頻這兩種數(shù)據(jù)闸翅,MP4可以算作是一種把他們封裝的協(xié)議再芋。
除了封裝協(xié)議,最重要的其實是編解碼協(xié)議缎脾,也是音視頻的一個重點祝闻,它指的是把一幀(音頻/視頻)數(shù)據(jù)按照既定的規(guī)則進行編碼或者解碼,從而達到壓縮數(shù)據(jù)便于存儲傳播的目的。比如我們大家都熟悉的哈夫曼編碼联喘,就是一種很典型的壓縮數(shù)據(jù)的編碼方法华蜒。最常見的有視頻類:h264,h265;音頻類:aac,ac3,mp3(MP3也可以被作為編碼協(xié)議的稱呼)。
之所以會出現(xiàn)編碼協(xié)議豁遭,是因為視頻的原始數(shù)據(jù)往往過于龐大叭喜,大家可以計算一下一個長度為一分鐘,幀率為30幀蓖谢,單幀圖像像素格式為RGB8888,1080x1920的視頻大概有多大捂蕴。如果原封不動的保存或者傳播。代價是非常昂貴的闪幽,因此對視頻的內(nèi)容進行編碼啥辨,去除冗余數(shù)據(jù),可以包存儲大小降低至少80%以上盯腌,甚至更多溉知。因此,音視頻的編碼協(xié)議對于他們自身是至關重要的腕够。
音視頻創(chuàng)建
音視頻的創(chuàng)建往往需要依賴攝像頭或者麥克風這些設備级乍,因此就涉及到依賴攝像頭和麥克風的驅(qū)動設備對其進行操作(實際上驅(qū)動之上肯定有多層的封裝來簡化使用的復雜性)。
編碼和存儲
對于已經(jīng)獲得了音視頻的原始數(shù)據(jù)后帚湘,往往需要對他們進行編碼玫荣,然后保存。目前市面上已有的非常成熟的編碼方法h264(視頻)大诸,aac(音頻)捅厂,是由不同的技術組織提供,并由各種硬件軟件廠商提供了支持资柔,因此對于普通開發(fā)者而言恒傻,我們面對的編碼是一個黑盒子,把原始數(shù)據(jù)輸入進編碼器建邓,等待輸出編碼后的數(shù)據(jù)即可。
對于存儲而言睁枕,核心內(nèi)容是把一個視頻內(nèi)容進行封裝成不同的封裝格式(比如mp4,flv),這里就涉及到了使用復用的功能,media muxer曲稼。它指的就是把音視頻等不同內(nèi)容封裝成某種格式的能力女阀。
解碼和播放
在視頻編碼保存之后,最終還是會被播放觀看的跳仿,這里其實涉及到了三個方面的內(nèi)容诡渴,解復用(demuxer),解碼(decoder),播放(player)菲语。
因為媒體文件是被封裝保存的妄辩,因此播放它的第一步惑灵,應該是解復用,就是把封裝格式(mp4,flv)里的內(nèi)容提取出來眼耀,獲得單獨的視頻英支,音頻以及其他內(nèi)容。
獲得了單獨的視頻音頻之后哮伟,無法直接播放干花,因為他們是被編碼過的看起來有些雜亂的數(shù)據(jù),而不是一幀幀圖像(音頻)的集合楞黄。因此我們需要把數(shù)據(jù)送入對應的解碼器對它們進行解碼池凄,然后才能獲得可以播放的一幀幀視頻(音頻)數(shù)據(jù),此時把他們送入播放器才可以正常播放鬼廓。
而播放器并不是提供播放能力即可肿仑,因為對于視頻的播放最重要的首先是音視頻的同步,就是要保證每一幀畫面大概要和某一組音頻數(shù)據(jù)同時播放桑阶,這是最基礎但是最重要的能力柏副。其次,則是視頻進度條的拖動蚣录,這個涉及 到視頻時間尋址的能力割择。雖然往往系統(tǒng)會提供一個播放器,里面會具備相應的能力萎河,但是如果需要獨立開發(fā)也不算太難(要做好還是挺難的)荔泳。
小結(jié)
以上就是對于音視頻的技術領域的簡單拆分,這個并不涉及到平臺虐杯,無論是windows,linux玛歌,ios,android大體上都是一樣的。系統(tǒng)往往提供了更加豐富的API支持擎椰,操作相對更簡單支子,考慮到平臺的差異,大家往往也會考慮使用跨平臺的框架(ffmpeg)达舒。
Android的音視頻框架
參照前面講的音視頻框架值朋,我們可以來講講Android平臺的對應的音視頻框架,以及他們的使用方法巩搏。
對于Android視頻而言昨登,或許我們應該首先重點熟悉一下Android 的Surface,它是Android系統(tǒng)中的一個重要結(jié)構贯底,不僅僅音視頻框架的各個流程中隨處可見丰辣,在我們?nèi)粘5腣iew的體系中他也是核心內(nèi)容,討論Android的顯示原理也無法繞開它。
關于surface的更多描述見《Android顯示系統(tǒng)的基本原理》.篇幅所限在此不做展開笙什,大家可以把surface理解為一個渲染數(shù)據(jù)的匯集地飘哨,surface可以作為camera圖像數(shù)據(jù)輸出的出口匯集地,也可以作為編碼器的輸入輸入的入口得湘。
音視頻的創(chuàng)建
對于移動設備來講杖玲,創(chuàng)建音視頻的方式是使用攝像頭和麥克風這兩個硬件設備來獲取媒體內(nèi)容,在Android平臺而言淘正,就是使用Camera2或者CameraX相關的API即可獲取攝像頭的圖像數(shù)據(jù)摆马。而要獲取音頻數(shù)據(jù),則一般通過MediaRecoder或者AudioRecod來實現(xiàn)鸿吆。
視頻的創(chuàng)建
對于視頻而言囤采,我們說到一般使用Camera2或者CameraX來實現(xiàn)對圖像數(shù)據(jù)的獲取。但是這兩個模塊也有所區(qū)別:
- camera2
- 優(yōu)勢 可以實現(xiàn)更精細的配置惩淳,更好的自定義蕉毯。
- 缺陷 接口的操作相對繁瑣。
- camerax
- 優(yōu)勢 對camera的相關操作有進一步的封裝思犁,可以快速完成對camera的配置使用代虾,已經(jīng)加入Jetpack中,算是官方推薦優(yōu)先使用
- 缺陷 較難實現(xiàn)精細配置激蹲,對高度自定義需求支持不如前者
由于camerax的對于預覽view,camera配置棉磨,生命周期管理等的高度封裝,因此理論上按照官方教程看30分鐘就可以基本掌握cameraX的用法:CameraX使用入門学辱。
我來簡單介紹一下camera2的基本用法乘瓤,對于camera2,最重要的類有三個:
- CameraManager
- 相機的系統(tǒng)服務管理策泣,可以用來獲取一個相機頭衙傀,以及相機相關的參數(shù)
- CameraDevice
- 代表單個攝像頭設備
- CameraCaptureSession
- 針對單個攝像頭獲取連接,對該攝像頭采集的數(shù)據(jù)進行捕獲(主要是通過給攝像頭設置output surface)
camera2的使用主要圍繞著對三個類的對象的獲取萨咕,而且是依次獲取的统抬,得到前一個對象可以獲取后一個對象,最終通過CameraCaptureSession來啟動預覽
首先在布局文件中添加用于預覽的View危队,我們選擇SurfaceView.它可以把camera傳來的渲染數(shù)據(jù)直接展示出來(見《Android顯示系統(tǒng)的基本原理》)蓄喇。假設我們在主界面中已經(jīng)完成了SurfaceView配置,正常獲取到了SurfaceHolder的情況下交掏,大致需要實現(xiàn)這樣的操作:
// 1, 獲得CameraManager
private val cameraManager:CameraManager by lazy{
applicationContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager
}
// 2 獲取camera
private val cameraThread = HandlerThread("CameraThread").apply { start() }
private val cameraHandler = Handler(cameraThread.looper)
private var theCamera:CameraDevice? = null
// cameraid:攝像頭id,通過cameraManager獲得所有的攝像頭數(shù)據(jù)(包含一些配置參數(shù))
val cameraId = getCameraIds().get(0)
// 通過openCamera,然后回調(diào)獲得結(jié)果
cameraManager.openCamera(cameraId,object :CameraDevice.StateCallback(){
override fun onOpened(camera: CameraDevice) {
// 正常獲取到了CameraDevice
theCamera = camera
}
...
...
},cameraHandler)
// 3,獲取cameraSession
// 輸出Surface列表(camera 可以輸出到多個surface)
var outConfigs = mutableListOf<OutputConfiguration>()
var cameraSession:CameraCaptureSession? = null
// 注意,后面setRequest時輸入的surface必須在此處的configuration里的surface范圍內(nèi)
var outConfig = OutputConfiguration(dataBinding.surfaceViewId.holder.surface)
outConfigs.add(outConfig)
var mStateCallback = object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
// 在回調(diào)中正常獲取到session
cameraSession = session
}
...
}
var config = SessionConfiguration(SESSION_REGULAR,outConfigs,cameraExecutor,mStateCallback)
// 通過createCaptureSession嘗試創(chuàng)建CameraCaptureSession
camera.createCaptureSession(config)
// 4刃鳄,啟動預覽
// 創(chuàng)建一個捕獲請求
var request = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
addTarget(dataBinding.surfaceViewId.holder.surface) // 添加預覽view的surface盅弛,表明希望camera的數(shù)據(jù)輸出到這個surface中
}.build()
// 設置捕獲圖像數(shù)據(jù)的請求
cameraSession.setRepeatingRequest(request,null,cameraHandler)
上述代碼并不是完整代碼,而是主要邏輯。開發(fā)者只要需要記住主要步驟即可:
- 通過SystemService獲取CameraManager挪鹏。
- 通過CameraManager獲得CameraDevice见秽。
- 通過CameraDevice獲得CameraCaptureSession。
- CameraCaptureSession設置好surface開始獲取攝像頭數(shù)據(jù)讨盒。
具體的實現(xiàn)細節(jié)和對camera的參數(shù)的精細控制可自行實現(xiàn)解取。
音頻的創(chuàng)建
對于Android設備而言,創(chuàng)建音頻主要通過MediaRecorder,這是對音頻錄制的高度封裝返顺,而且可以實現(xiàn)音頻視頻的同時錄制禀苦,而且實現(xiàn)編碼并保存,正常而言如果對音視頻沒有更多要求遂鹊,大家都應該使用這樣高度封裝的類振乏,可以避免很多錯誤。而它的實現(xiàn)也非常簡單秉扑,只要按照一定的順序設置好配置即可慧邮。
// A common case of using MediaRecorder to record audio works as follows:
MediaRecorder recorder = new MediaRecorder();
recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
recorder.setOutputFile(PATH_NAME);
recorder.prepare();
recorder.start(); // Recording is now started
...
recorder.stop();
recorder.reset(); // You can reuse the object by going back to setAudioSource() step
recorder.release(); // Now the object cannot be reused
但是對于技術探討必然不能追求省事,因此我們可以講講AudioRecod,這是Android系統(tǒng)中舟陆,用于管理音頻資源的Java層實現(xiàn)類误澳,通過它可以實現(xiàn)對音頻的錄制。
雖然AudioRecod自身的API結(jié)構同樣簡單秦躯,但是因為它剝離了音視頻編碼等內(nèi)容忆谓,功能更加聚焦。對于AudioRecod,我們只需要創(chuàng)建對象宦赠,開始錄制陪毡,讀取數(shù)據(jù),結(jié)束錄制即可勾扭。
// MediaRecorder.AudioSource.MIC 音頻來源 麥克風
// 剩余是 采樣率,聲道配置妙色,采樣深度,緩沖區(qū)大小身辨,這些根據(jù)自身需要來進行配置
var audioRecoder = AudioRecord(MediaRecorder.AudioSource.MIC,SAMPLE_RATE,CHANNEL_CONFIG,
AudioFormat.ENCODING_PCM_16BIT,miniBufferSize)
// 開始錄制
audioRecoder?.startRecording()
// 從AudioRecod讀取音頻數(shù)據(jù)到buffer
val readSize = audioRecord?.read(buffer, 0, buffer.size) ?: 0
// 停止錄制
audioRecord?.stop()
我們可以看到聲音的采集操作是不復雜的,不過我們也到知道聲音的的各項配置是比較復雜的煌珊,需要注意号俐,建議閱讀關于音頻的基本概念
關于Android編解碼
前面獲取到的音視頻數(shù)據(jù)往往是原始數(shù)據(jù),我們前面說過定庵,原始數(shù)據(jù)直接保存的話太大了吏饿,所以往往需要經(jīng)過編碼,把數(shù)據(jù)縮小贞远,才能正常保存。
Android系統(tǒng)針對編碼和解碼提供了一個統(tǒng)一的操作API笨忌,在Java層面我們無法對編解碼細節(jié)做任何的窺探蓝仲,但這并不意味著Java層面的編解碼器只是高級API的擺設,實際上他們?nèi)匀粚儆谳^底層的API官疲,因為通過它們可以接觸到編碼前后的數(shù)據(jù)袱结,那么假如我們需要對音頻或者視頻做修改的話袁余,這是一個很好的契機。而MediaRecoder的完全沒有這樣的可能性颖榜。
對于編解碼器,核心類是MediaCodec掩完。
聲明周期
關于編解碼器的生命周期如下:
我們需要首先完成configure過程,然后開始進行編解碼的工作欣硼,等編解碼完成之后,再stop或者直接release.
工作流程
running狀態(tài)的編解碼的基本工作流程是這樣的:
在輸入端诈胜,不斷的從編解碼器中獲取空的緩存空間,并填入待處理的數(shù)據(jù)焦匈。
在輸出端昵仅,從對應的緩存空間中獲取已經(jīng)編解碼完成的數(shù)據(jù)。
在連接輸入和輸出的編解碼器則是一個黑盒子摔笤。
Android音視頻編碼
前面已經(jīng)分別獲取了音頻和視頻的數(shù)據(jù),那么接下來就是對這些原始數(shù)據(jù)進行編碼工作吕世,注意對于視頻和音頻這兩種完全不同的數(shù)據(jù),是需要不同的編碼器來實現(xiàn)編碼過程的命辖。
配置階段
視頻編碼器的配置
// 獲取視頻的編碼器
private val videoEncoder: MediaCodec by lazy {
MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
}
// 配置一些視頻相關的參數(shù)晚伙。比如幀率俭茧,顏色格式等
val format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC,
videoWidth,videoHeight
)
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
)
format.setInteger(MediaFormat.KEY_BIT_RATE, video_bit_rate) // bit rate
format.setInteger(MediaFormat.KEY_FRAME_RATE, video_fps) // 幀率
...
videoEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
// 開始進行編碼
videoEncoder.start()
音頻編碼器的配置(大差不差)
//獲取音頻的編碼器
private val audioEncoder: MediaCodec by lazy {
MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)
}
format = MediaFormat.createAudioFormat(
MediaFormat.MIMETYPE_AUDIO_AAC,
SAMPLE_RATE,
1
)
format.setInteger(MediaFormat.KEY_BIT_RATE, AUDIO_BIT_RATE)
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
...
audioEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
// 開始進行編碼
audioEncoder.start()
調(diào)用了configure方法之后漓帚,編碼器就進入了configured的狀態(tài),然后調(diào)用start之后尝抖,就進入了編碼狀態(tài),此時就需要往編碼器里輸入待編碼的數(shù)據(jù)了衙熔。
編碼
而目前,編解碼的方式有兩種红氯,一種是同步模式咕痛,一種是異步模式痢甘,我們先展示下同步模式下茉贡,編碼工作是怎樣進行的:
// 注意塞栅,編碼工作應該在子線程中進行
...
while (true){
/************ 編碼的輸入部分 *************/
// 嘗試從緩存隊列中獲取一個可用空間的下標
val index = audioEncoder.dequeueInputBuffer(0)
if (index >= 0){
// 獲取到緩存空間
val inputBuffer = audioEncoder.getInputBuffer(index)
var buffer = ByteArray(BUFFER_SIZE)
// 從音頻錄制器中讀取錄制的音頻數(shù)據(jù)腔丧,不一定能讀滿 BUFFER_SIZE
val readSize = audioRecord?.read(buffer, 0, buffer.size) ?: 0
var inputBuffer :ByteBuffer? = null
if (readSize > 0) {
inputBuffer?.let {
it.clear()
it.put(buffer)
it.position(0)
it.limit(readSize)
// 當前時間戳
val time = getPresentationTimeUs()
audioEncoder.queueInputBuffer(index, 0, readSize,time , 0) // 把數(shù)據(jù)輸入隊列中
}
}
}
/************ 編碼的輸出部分 *************/
var encoderStatus = mediaCodec.dequeueOutputBuffer(bufferInfo,0)
if(encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER){
break;
}else if(encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
outputFormat = mediaCodec.getOutputFormat()
Log.i(Constant.LOG_TAG,"獲取輸出的媒體格式 ===============")
}else if (encoderStatus<0){
Log.w(Constant.LOG_TAG,"dequeueInputBuffer error $encoderStatus")
}else{ // 大于等于0,表示成功
// 此時encoderStatus 就是輸出緩沖隊列的緩存區(qū)下標 index
var encodedData : ByteBuffer?= mediaCodec.getOutputBuffer(encoderStatus)?:return encodedFrame
Log.i(Constant.LOG_TAG,"獲取編碼的數(shù)據(jù) ===============")
if(bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0){
bufferInfo.size = 0
}
if (bufferInfo.size!=0){
encodedData?.position(bufferInfo.offset)
encodedData?.limit(bufferInfo.size+bufferInfo.offset)
// 接下來把數(shù)據(jù)encodedData寫入文件即可
}
// 緩存空間被使用完之后釋放空間砾医,把內(nèi)存空間返回給編解碼器
mediaCodec.releaseOutputBuffer(encoderStatus,false)
if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM)!=0){
break
}
}
}
以上就是基于同步模式下科汗,處理音頻數(shù)據(jù)編碼的大體過程,需要注意的有以下幾點:
- 編解碼都應該在子線程中進行头滔,也要考慮線程通信的問題。
- 對于輸出緩存區(qū)使用完之后需要手動釋放掉坤检。
- 很多API調(diào)用都可能出現(xiàn)throw Exception,需要進行catch
個人認為相對于同步模式而言早歇,異步模式在大多數(shù)情況下可能更加有利于開發(fā)讨勤,因此后面的需要演示的代碼都使用異步模式來展示晨另。
異常模式的啟用方式,就是在encoder進行configure之后借尿,且start之前,設置setCallback回調(diào)即可路翻。
private val audioHandlerThread = HandlerThread("async-audio-encode").apply { start() }
private val asyncAudioEncodeHandler: Handler = Handler(audioHandlerThread.looper)
...
audioEncoder.configure(...)
audioEncoder.setCallback(object : MediaCodec.Callback() {
private var audioTrack = -1
// 這是輸入端
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
if (!recoding){
return
}
var buffer = ByteArray(BUFFER_SIZE)
// 讀取音頻原始數(shù)據(jù)
val readSize = audioRecord?.read(buffer, 0, buffer.size) ?: 0
var inputBuffer :ByteBuffer? = null
inputBuffer = codec.getInputBuffer(index)
if (readSize > 0) {
inputBuffer?.let {
it.clear()
it.put(buffer)
it.position(0)
it.limit(readSize)
// 記錄當前的時間戳
val time = getPresentationTimeUs()
codec.queueInputBuffer(index, 0, readSize,time , 0)
}
}
}
// 這是輸出端
override fun onOutputBufferAvailable(
codec: MediaCodec,
index: Int,
info: MediaCodec.BufferInfo
) {
if(isEnd) return
val outputBuffer = codec.getOutputBuffer(index)
val outputFormat = codec.getOutputFormat(index)
if ((info.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
info.size = 0
}
outputBuffer?.let {
it.position(info.offset) // offset一般就是0
it.limit(info.offset + info.size)
...
// 把編碼后的數(shù)據(jù)輸出到文件
}
codec.releaseOutputBuffer(index, false)
var isEnd = (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0
}
override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
...
}
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
//輸出格式變化
}
// 保證異步回調(diào)在這個線程中進行
}, asyncAudioEncodeHandler)
// 開始進行編碼
audioEncoder.start()
以上就是音頻編碼的異步模式的使用方式茂契。
對于視頻而言,如果讀取視頻的原始數(shù)據(jù)的話掉冶,操作方式是基本一致的,但是編解碼器也提供了Surface作為輸入或者輸出的接口郭蕉,比如在編碼時,編碼器可以提供一個Surface用作輸入的入口召锈,而不需要我們手動把數(shù)據(jù)讀入到編碼器,這就大大的降低了我們的工作量拐袜。正好,camera也可以接受surface作為攝像頭數(shù)據(jù)的輸入地蹬铺,因此秉撇,我們其實只需要把encoder提供的Surface添加到操作camera時需要填入的surface列表中即可甜攀。
...
// 注意琐馆,后面setRequest時輸入的surface必須在此處的configuration里的surface范圍內(nèi)
var outConfig = OutputConfiguration(
dataBinding.surfaceViewId.holder.surface,
encoder.createInputSurface()) // createInputSurface()就是編碼器提供的輸入接口
...
其實如果我們想要使用原始數(shù)據(jù)也是可行的,利用ImageReader對象設置一個數(shù)據(jù)回調(diào)瘦麸,ImageReader.surface同樣添加到camera的輸出列表中即可(沒錯,同樣也是依賴surface)厉碟。
一般我們就直接通過encoder的surface來承接camera的數(shù)據(jù)喊巍,可以節(jié)省很多工作箍鼓,而且使用了inputSurface之后,異步模式的輸入回調(diào)就不會調(diào)用了款咖。
private val handlerThread = HandlerThread("async-video-encode").apply { start() }
private val asyncEncodeHandler: Handler = Handler(handlerThread.looper)
...
videoEncoder.setCallback(object : MediaCodec.Callback() {
var outputFormat: MediaFormat? = null
var mVideoTrack: Int = -1
var isEnd = false
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
// 使用了surface,我們不會受到回調(diào)
}
override fun onOutputBufferAvailable(
codec: MediaCodec,
index: Int,
bufferInfo: MediaCodec.BufferInfo
) {
if (isEnd) {
return
}
val outputFormat = encoder.getOutputFormat(index)
var encodedData: ByteBuffer? = encoder.getOutputBuffer(index) ?: return
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
bufferInfo.size = 0
}
if (bufferInfo.size != 0) {
encodedData?.position(bufferInfo.offset)
encodedData?.limit(bufferInfo.size + bufferInfo.offset)
encodedData?.let {
...
// 輸出的編碼數(shù)據(jù)可以寫入文件
}
}
// 緩存空間被使用完之后釋放空間,把內(nèi)存空間返回給編解碼器
encoder.releaseOutputBuffer(index, false)
isEnd = (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0
}
override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
Log.i(LOG_TAG, "onError : ${e.errorCode} ${e.message}")
}
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
Log.i(LOG_TAG, "onOutputFormatChanged : ${format.toString()}")
}
}, asyncEncodeHandler)
可以看到砍聊,代碼大大減少。
這只是音視頻的編碼的基本邏輯蟹肘,有大量需要關注的問題并沒有體現(xiàn)出來:
- MediaCodec的API會拋出大量的異常,需要catch
- 編碼器的啟動和關閉的控制時機
- 可能的多線程問題
- 編解碼輸入過程中的PTS設置
- 如何寫入文件
以上幾個問題帘腹,前面三個其實還好许饿,如果仔細看文檔和需要很容易掌握阳欲。而PTS設置可能對大家會存在一些困擾陋率,所以有必要專門用一個小節(jié)來解釋一下,寫入文件則是另一個問題瓦糟,在下一個節(jié)中進行講述。
編解碼的PTS
PTS是presentation timestamp(顯示時間戳)的意思菩浙,一般使用presentationTimeUs變量來表示。
大概就是告訴編解碼器當前數(shù)據(jù)應該在哪個時間點來顯示陆淀。需要在queueInputBuffer時設置,而且無論是音頻或者視頻都需要pts這個數(shù)據(jù)(單位微秒)倔约,但是如何得到這個數(shù)據(jù)呢坝初?
官方文檔對于這個PTS的解釋并不多浸剩,但是根據(jù)我的觀察,這時間戳的對于時間的起點并沒有限制吏恭,重要的是能正確表示在時間軸中每一幀數(shù)據(jù)應該處在哪個時間點。
我看網(wǎng)絡上許多的demo大家使用的是
System.nanoTime()/1000L
也就是每次queueInputBuffer之前樱哼,都通過上面的代碼獲取一次當前時間戳剿配。這種方式似乎沒有太大的問題搅幅,而且播放顯示基本正常呼胚。
但是官方的測試代碼卻不是這么寫的,而是根據(jù)音頻或者視頻的格式配置來計算出當前時間戳,比如我摘抄的這段對音頻數(shù)據(jù)的PTS的計算:
// numBytesSubmitted 就是累計提交給編碼器的數(shù)據(jù)量蝇更,1000000是單位轉(zhuǎn)換,把秒轉(zhuǎn)換為微秒
// 而根據(jù)音頻的采樣率蚁廓,采樣精度,聲道數(shù)我們可以計算出一秒鐘內(nèi)的音頻數(shù)據(jù)量
// channelCount就是聲道數(shù)相嵌,sampleRate是采樣率克胳,
//而2代表的應該就是16/8的意思平绩,16是一般音頻采樣精度漠另,8則是把bit轉(zhuǎn)換為byte
long timeUs = (long)numBytesSubmitted * 1000000 / (2 * channelCount * sampleRate);
上面的公式就是根據(jù)已經(jīng)提交的音頻數(shù)據(jù)量除以一秒鐘來獲得當前提交的數(shù)據(jù)所處的時間戳。我們舉個例子笆搓,假設音頻數(shù)據(jù)每一秒的數(shù)據(jù)量是1024kb,而numBytesSubmitted此時是1536kb,那么就表示當前提交的數(shù)據(jù)所處的時間戳是1.5*1000_000L微秒這個時間點(默認時間起點是0)
我個人認為官方的計算方法應該是最合理的满败,但是根據(jù)我的實際測試,假如使用官方的計算方式來設置PTS,得到的視頻文件缺失可以正常播放宵荒,但是顯示的視頻時長信息卻不正常(無法預覽到視頻時長,播放時無進度條顯示)报咳。
我猜測可能是時間起點為0導致的,因此在這個基礎上把時間起點改為某一個系統(tǒng)時間暑刃,
if (startPresentationTimeUs == -1L) { // 初始化
startPresentationTimeUs = System.nanoTime()/1000L
}
var timeUs = numBytesSubmitted * 1000000L / (2 * channelCount * sampleRate)+startPresentationTimeUs
通過這樣修改后,顯示和播放都變得正常了岩臣。對于視頻其實也是類似的方法,我們一般會提前知道想要錄制的視頻的幀率架谎,通過記錄幀數(shù)就可以得到每一幀應該在哪個時間點顯示....
封裝復用
我們知道音頻和視頻是單獨的數(shù)據(jù),必然不能直接通過文件流寫入到文件焙压,必然需要按照所封裝的格式進行寫入(如果是mp4就需要按照mp4的格式要求寫入)。這就是我們要講的媒體文件的封裝復用(muxer)。
封裝復用的核心類是MediaMuxer野哭,其實它的API并不復雜:
// 根據(jù)媒體文件路徑,格式來創(chuàng)建對象
var mediaMuxer = MediaMuxer(filePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
var outputFormat = codec.outputFormat
// 根據(jù)編碼器輸出的編碼數(shù)據(jù)所屬的格式(視頻或音頻)拨黔,來添加一個軌道
// 這個軌道后面就專門存儲這個格式的媒體數(shù)據(jù)
audioTrack = mediaMuxer.addTrack(outputFormat)
//start之后就可以了開始寫入了(但是start只能調(diào)用一次)
mediaMuxer.start()
// 寫入數(shù)據(jù)
mediaMuxer.writeSampleData(audioTrack,encodedData,bufferInfo)
// 釋放資源
mediaMuxer.stop()
mediaMuxer.release()
以上基本就是我們使用MediaMuxer所用到的所有API了,但是問題并沒有那么簡單,因為mediaMuxer.start只能調(diào)用一次贺待,但是音視頻編碼時在不同的編碼器不同的線程中進行的零截,所以我們就需要一些線程同步操作麸塞,保證在音頻和視頻的軌道都被添加上了之后再調(diào)用start,開始寫入數(shù)據(jù)涧衙。
只要選好時機,線程同步就不會太復雜弧哎,編碼器在正式輸出編碼數(shù)據(jù)之后,會提前回調(diào)一次輸出數(shù)據(jù)的格式onOutputFormatChanged偎捎,因此我們在這個回調(diào)中添加軌道即可,然后等待兩個編碼器都完成了這個回調(diào)以后再start即可茴她。
一個思考題寻拂,MediaMuxer的writeSampleData是否需要進行線程同步败京?
總結(jié)
目前分析了Android音視頻創(chuàng)建,編碼赡麦,存儲,可以說我們粗略的講完了音視頻的上半程泛粹;還剩下讀取,解碼扒接,播放,由于篇幅所限決定分為兩篇敘述較好钾怔。
文中的代碼刪減極大蒙挑,主要用于介紹基本框架宗侦,網(wǎng)絡上也有很多類似的demo可供大家觀摩學習忆蚀,個人推薦如果大家要學習多媒體/編解碼的代碼實現(xiàn)的話,推薦學習Android系統(tǒng)的源碼里的針對Media模塊的Test代碼(從詳盡程度上看馋袜,比大多數(shù)的demo更詳細),除此之外我暫時還沒找到更加詳盡的官方demo.代碼點我