音視頻開發採集部分需要的知識前三章中已經寫明,這章是對前面三章的整理簡化,首先上實現類,代碼很簡單,文末爲基類代碼
class LiveBroadcastActivity : BaseVideoActivity() {
//預覽CaptureRequest.Builder
private lateinit var previewCaptureRequest: CaptureRequest.Builder
override fun getLayoutId(): Int {
return R.layout.activity_live_broadcast;
}
override fun init() {
//開啓surfaceTextureListener監聽
textureView.surfaceTextureListener = surfaceTextureListener
//切換相機
switchCamera.setOnClickListener {
switchCamera()
}
}
/**
* 創建一個Session
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun createCaptureSession(camera: CameraDevice):List<Surface>{
//創建一個預覽的CaptureRequest
previewCaptureRequest = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
// 設置預覽輸出的 Surface
previewCaptureRequest.addTarget(surface)
val outputs = ArrayList<Surface>()
outputs.add(surface)
return outputs
}
/**
* 返回預覽的CaptureRequest
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun setRepeatingRequest(session: CameraCaptureSession): CaptureRequest{
//開始錄音
startRecording()
return previewCaptureRequest.build()
}
/**
* 音頻編碼,注意這裏是在子線程中的
*/
override fun audioCoding(buffer: ByteArray, len: Int) {
}
/**
* 老相機API數據回調接口
*/
override fun oldSendVideoData(data: ByteArray) {
}
/**
* 自動修改textureView寬高以適應不同預覽比例
*/
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val width = textureView.width
val height = textureView.height
val proportion1 = size.width.toFloat() / size.height
val proportion2 = height.toFloat() / width
if (proportion1 > proportion2) {
val layoutParams = textureView.layoutParams
layoutParams.width = (height * proportion1 + .5).toInt()
textureView.layoutParams = layoutParams
} else if (proportion1 < proportion2) {
val layoutParams = textureView.layoutParams
layoutParams.height = (width * proportion1 + .5).toInt()
textureView.layoutParams = layoutParams
}
}
}
}
是不是感覺瞬間解脫,再也不用去寫那些繁瑣的代碼了,接下來總結一下是如何實現的,開啓定義好的surfaceTextureListener便會自動採集音視頻
textureView.surfaceTextureListener = surfaceTextureListener
使用是不是很簡單,然後來看一下基類是怎麼實現的
/**
* 獲取Surface的回調
*/
val surfaceTextureListener = object : TextureView.SurfaceTextureListener {
//SurfaceTexture大小發生變化時調用
@SuppressLint("Recycle")
override fun onSurfaceTextureSizeChanged(
surfaceTexture: SurfaceTexture,
width: Int,
height: Int
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//獲取相機屬性類
val cameraCharacteristics = getCameraCharacteristics()
//設置預覽尺寸
size = setPreviewSize(surfaceTexture, cameraCharacteristics)
surface = Surface(surfaceTexture)
}
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) {
}
override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture?): Boolean {
//關閉老相機API預覽
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
stopPreview()
}
surface.release()
return true
}
override fun onSurfaceTextureAvailable(
surfaceTexture: SurfaceTexture,
width: Int,
height: Int
) {
//初始化AudioRecord
initAudioRecord()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//初始化Camera2
initCamera2()
//獲取相機屬性類
val cameraCharacteristics = getCameraCharacteristics()
//設置預覽尺寸
size = setPreviewSize(surfaceTexture, cameraCharacteristics)
surface = Surface(surfaceTexture)
//開啓攝像頭
openCamera()
}else{
//開啓老相機API預覽
startPreview(surfaceTexture)
}
}
}
在onSurfaceTextureAvailable方法中初始化AudioRecord和Camera,初始化完成後便會開啓攝像頭
/**
* 打開攝像頭
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun openCamera() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
cameraManager.openCamera(cameraId, callback, getBackgroundHandler())
} else {
val dialog = AlertDialog.Builder(this)
dialog.setTitle("開啓相機失敗").setMessage("缺少開啓相機的權限").setCancelable(false)
dialog.setNegativeButton("取消") { _, _ ->
}
dialog.setPositiveButton("授權") { _, _ ->
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.parse("package:$packageName")
startActivity(intent)
}
dialog.show()
}
}
/**
* 打開攝像頭的回調
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private val callback = object : CameraDevice.StateCallback() {
//成功打開時的回調,可以得到一個 CameraDevice 實例
override fun onOpened(camera: CameraDevice) {
cameraDevice = camera
outputs = createCaptureSession(camera)
//創建一個Session
camera.createCaptureSession(
outputs,
mSessionCallback,
getBackgroundHandler()
)
}
//當 camera 不再可用時的回調,通常在該方法中進行資源釋放的操作
override fun onDisconnected(camera: CameraDevice) {
showToast("camera不再可用")
}
// 當 camera 打開失敗時的回調,error 爲具體錯誤原因,通常在該方法中也要進行資源釋放的操作
override fun onError(camera: CameraDevice, error: Int) {
camera.close()
showError(error)
releaseBackgroundThread()
}
//相機關閉時回調
override fun onClosed(camera: CameraDevice) {
super.onClosed(camera)
cameraCaptureSession?.close()
}
}
/**
* 創建一個Session
*/
abstract fun createCaptureSession(camera: CameraDevice): List<Surface>
在開啓攝像頭的回調中可以看到使用了一個抽象方法,繼承這個基類需要重寫這個方法,便可以得到CameraDevice,接下來你想怎麼玩就是你的事了
/**
* 創建一個Session
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun createCaptureSession(camera: CameraDevice):List<Surface>{
//創建一個預覽的CaptureRequest
previewCaptureRequest = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
// 設置預覽輸出的 Surface
previewCaptureRequest.addTarget(surface)
val outputs = ArrayList<Surface>()
outputs.add(surface)
return outputs
}
接下來是創建預覽Session的回調
/**
* 創建預覽Session的回調
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
val mSessionCallback = object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
cameraCaptureSession = session
val captureRequest = setRepeatingRequest(session)
session.setRepeatingRequest(
captureRequest,
captureCallback,
getBackgroundHandler()
)
}
//創建失敗
override fun onConfigureFailed(session: CameraCaptureSession) {
showToast("創建Session失敗")
}
}
/**
* 開始預覽,即設置反覆請求
*/
abstract fun setRepeatingRequest(session: CameraCaptureSession):CaptureRequest
同樣使用了一個抽象方法,定義這個抽象方法主要是靈活性考慮,當然如果你不考慮可以寫在基類中
/**
* 返回預覽的CaptureRequest
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun setRepeatingRequest(session: CameraCaptureSession): CaptureRequest{
//開始錄音
startRecording()
return previewCaptureRequest.build()
}
爲了兼容5.0以下的手機,定義了一個老相機API數據回調接口,只有5.0以下的手機這個方法纔會生效
/**
* 老相機API數據回調接口
*/
override fun oldSendVideoData(data: ByteArray) {
}
最後是音頻的抽象方法,可以直接在這裏得到音頻數據
/**
* 音頻編碼,注意這裏是在子線程中的
*/
override fun audioCoding(buffer: ByteArray, len: Int) {
}
爲了防止內存泄露做了一些處理
override fun onDestroy() {
super.onDestroy()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cameraDevice?.close()
if(outputs != null){
for (surface in outputs!!) {
surface.release()
}
}
endRecording()
releaseBackgroundThread()
}else{
stopPreview()
}
}
最後附上一個自適應比例
/**
* 自動修改textureView寬高以適應不同預覽比例
*/
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val width = textureView.width
val height = textureView.height
val proportion1 = size.width.toFloat() / size.height
val proportion2 = height.toFloat() / width
if (proportion1 > proportion2) {
val layoutParams = textureView.layoutParams
layoutParams.width = (height * proportion1 + .5).toInt()
textureView.layoutParams = layoutParams
} else if (proportion1 < proportion2) {
val layoutParams = textureView.layoutParams
layoutParams.height = (width * proportion1 + .5).toInt()
textureView.layoutParams = layoutParams
}
}
}
基類
abstract class BaseVideoActivity() : BaseActivity() {
private lateinit var mBackgroundThread: HandlerThread
private var mBackgroundHandler: Handler? = null
//攝像頭管理類
lateinit var cameraManager: CameraManager
//攝像頭id列表
lateinit var cameraIdList: Array<String>
//第幾個攝像頭
var index = 0
//當前攝像頭id
lateinit var cameraId: String
//Surface集合
private var outputs: List<Surface>? = null
//當前攝像頭
private var cameraDevice: CameraDevice? = null
//Session
private var cameraCaptureSession: CameraCaptureSession? = null
//預覽Surface
lateinit var surface: Surface
//相機預覽分辨率
lateinit var size: Size
/**
* 獲取Surface的回調
*/
val surfaceTextureListener = object : TextureView.SurfaceTextureListener {
//SurfaceTexture大小發生變化時調用
@SuppressLint("Recycle")
override fun onSurfaceTextureSizeChanged(
surfaceTexture: SurfaceTexture,
width: Int,
height: Int
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//獲取相機屬性類
val cameraCharacteristics = getCameraCharacteristics()
//設置預覽尺寸
size = setPreviewSize(surfaceTexture, cameraCharacteristics)
surface = Surface(surfaceTexture)
}
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) {
}
override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture?): Boolean {
//關閉老相機API預覽
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
stopPreview()
}
surface.release()
return true
}
override fun onSurfaceTextureAvailable(
surfaceTexture: SurfaceTexture,
width: Int,
height: Int
) {
//初始化AudioRecord
initAudioRecord()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//初始化Camera2
initCamera2()
//獲取相機屬性類
val cameraCharacteristics = getCameraCharacteristics()
//設置預覽尺寸
size = setPreviewSize(surfaceTexture, cameraCharacteristics)
surface = Surface(surfaceTexture)
//開啓攝像頭
openCamera()
}else{
//開啓老相機API預覽
startPreview(surfaceTexture)
}
}
}
/**
* 初始化Camera2
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun initCamera2() {
cameraManager = application.getSystemService(Context.CAMERA_SERVICE) as CameraManager
cameraIdList = cameraManager.cameraIdList
cameraId = cameraIdList[index]
}
/**
* 獲取CameraCharacteristics相機屬性類
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun getCameraCharacteristics(): CameraCharacteristics {
return cameraManager.getCameraCharacteristics(cameraId)
}
/**
* 設置預設的預覽尺寸
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun setPreviewSize(@NotNull surfaceTexture: SurfaceTexture, cameraCharacteristics: CameraCharacteristics): Size {
val aspectRatios = ArrayList<Float>()
aspectRatios.add(16.toFloat() / 9)
aspectRatios.add(4.toFloat() / 3)
aspectRatios.add(18.toFloat() / 9)
val size = getPreviewSize(cameraCharacteristics, aspectRatios)
surfaceTexture.setDefaultBufferSize(size.width, size.height)
return size
}
/**
* 獲取預覽尺寸
* 參數2:預覽尺寸比例的集合,按加入順序尋找預覽尺寸並返回
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun getPreviewSize(@NotNull cameraCharacteristics: CameraCharacteristics, aspectRatios: ArrayList<Float>): Size {
for (aspectRatio in aspectRatios) {
val size = getPreviewSize(cameraCharacteristics, aspectRatio)
if (size != null) {
return size
}
}
return Size(1280, 720)
}
/**
* 獲取預覽尺寸
* 參數2:預覽尺寸比例,如4:3,16:9
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun getPreviewSize(@NotNull cameraCharacteristics: CameraCharacteristics, aspectRatio: Float): Size? {
val streamConfigurationMap =
cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
val supportedSizes = streamConfigurationMap.getOutputSizes(SurfaceTexture::class.java)
for (size in supportedSizes) {
if (size.width.toFloat() / size.height == aspectRatio) {
return size
}
}
return null
}
/**
* 打開攝像頭
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun openCamera() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
cameraManager.openCamera(cameraId, callback, getBackgroundHandler())
} else {
val dialog = AlertDialog.Builder(this)
dialog.setTitle("開啓相機失敗").setMessage("缺少開啓相機的權限").setCancelable(false)
dialog.setNegativeButton("取消") { _, _ ->
}
dialog.setPositiveButton("授權") { _, _ ->
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.parse("package:$packageName")
startActivity(intent)
}
dialog.show()
}
}
/**
* 打開攝像頭的回調
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private val callback = object : CameraDevice.StateCallback() {
//成功打開時的回調,可以得到一個 CameraDevice 實例
override fun onOpened(camera: CameraDevice) {
cameraDevice = camera
outputs = createCaptureSession(camera)
//創建一個Session
camera.createCaptureSession(
outputs!!,
mSessionCallback,
getBackgroundHandler()
)
}
//當 camera 不再可用時的回調,通常在該方法中進行資源釋放的操作
override fun onDisconnected(camera: CameraDevice) {
}
// 當 camera 打開失敗時的回調,error 爲具體錯誤原因,通常在該方法中也要進行資源釋放的操作
override fun onError(camera: CameraDevice, error: Int) {
camera.close()
showError(error)
releaseBackgroundThread()
}
//相機關閉時回調
override fun onClosed(camera: CameraDevice) {
super.onClosed(camera)
cameraCaptureSession?.close()
}
}
/**
* 創建一個Session
*/
abstract fun createCaptureSession(camera: CameraDevice): List<Surface>
/**
* 創建預覽Session的回調
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
val mSessionCallback = object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
cameraCaptureSession = session
val captureRequest = setRepeatingRequest(session)
session.setRepeatingRequest(
captureRequest,
captureCallback,
getBackgroundHandler()
)
}
//創建失敗
override fun onConfigureFailed(session: CameraCaptureSession) {
showToast("創建Session失敗")
}
}
/**
* 開始預覽,即設置反覆請求
*/
abstract fun setRepeatingRequest(session: CameraCaptureSession):CaptureRequest
/**
* Session進度的回調
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
val captureCallback = object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult
) {
super.onCaptureCompleted(session, request, result)
}
override fun onCaptureFailed(
session: CameraCaptureSession,
request: CaptureRequest,
failure: CaptureFailure
) {
super.onCaptureFailed(session, request, failure)
}
}
/**
* 切換攝像頭
*/
fun switchCamera() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (cameraDevice != null) {
if (index < cameraIdList.size - 1) {
index++
} else {
index = 0
}
cameraId = cameraIdList[index]
cameraDevice?.close()
openCamera()
} else {
showToast("請先開啓攝像頭")
}
}else{
oldSwitchCamera()
}
}
/**
* 獲取BackgroundHandler
*/
fun getBackgroundHandler(): Handler {
if (mBackgroundHandler == null) {
//設置攝像頭線程
mBackgroundThread = HandlerThread("CameraBackground")
mBackgroundThread.start()
mBackgroundHandler = Handler(mBackgroundThread.looper)
}
return mBackgroundHandler as Handler
}
/**
* 釋放線程資源
*/
fun releaseBackgroundThread() {
mBackgroundHandler?.removeCallbacksAndMessages(null)
mBackgroundHandler = null
mBackgroundThread.quitSafely()
mBackgroundThread.join()
}
/**
* 開啓攝像頭錯誤提示
*/
fun showError(error: Int) {
when (error) {
CameraDevice.StateCallback.ERROR_CAMERA_IN_USE -> {
showToast("當前相機設備已經在一個更高優先級的地方打開了")
}
CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE -> {
showToast("已打開相機數量到上限了,無法再打開新的相機了")
}
CameraDevice.StateCallback.ERROR_CAMERA_DISABLED -> {
showToast("由於相關設備策略該相機設備無法打開")
}
CameraDevice.StateCallback.ERROR_CAMERA_DEVICE -> {
showToast("相機設備發生了一個致命錯誤")
}
CameraDevice.StateCallback.ERROR_CAMERA_SERVICE -> {
showToast("相機服務發生了一個致命錯誤")
}
}
}
override fun onDestroy() {
super.onDestroy()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cameraDevice?.close()
if(outputs != null){
for (surface in outputs!!) {
surface.release()
}
}
endRecording()
releaseBackgroundThread()
}else{
stopPreview()
}
}
// 採樣率
private val sampleRateInHz = 44100
// 音頻通道 立體聲:
val stereo = AudioFormat.CHANNEL_IN_STEREO
lateinit var audioRecord: AudioRecord
//audioRecord能接受的最小的buffer大小
private var bufferSizeInBytes: Int = 0
//錄音線程
private var recordingJob: Job? = null
/**
* 初始化AudioRecord
*/
fun initAudioRecord(channelConfig: Int = AudioFormat.CHANNEL_IN_MONO) {
//audioRecord能接受的最小的buffer大小
bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, AudioFormat.ENCODING_PCM_16BIT)
audioRecord = AudioRecord(MediaRecorder.AudioSource.MIC, sampleRateInHz, channelConfig, AudioFormat.ENCODING_PCM_16BIT, bufferSizeInBytes)
}
/**
* 開始錄音
*/
fun startRecording() {
recordingJob = GlobalScope.launch(Dispatchers.IO) {
if (bufferSizeInBytes > 0) {
audioRecord.startRecording()
while (isActive) {
val buffer = ByteArray(bufferSizeInBytes)
val len = audioRecord.read(buffer, 0, buffer.size)
if (len > 0) {
//音頻編碼
audioCoding(buffer, len)
}
}
} else {
launch(Dispatchers.Main) {
showToast("請先初始化AudioRecord類")
}
}
}
}
/**
* 音頻編碼
*/
abstract fun audioCoding(buffer: ByteArray, len: Int)
/**
* 結束錄音
*/
fun endRecording() {
if (audioRecord.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
audioRecord.stop()
}
if (audioRecord.state == AudioRecord.STATE_INITIALIZED) {
audioRecord.release()
}
recordingJob?.cancel()
}
//老相機id
private var oldCameraId = Camera.CameraInfo.CAMERA_FACING_BACK
//老相機SurfaceTexture
private var oldSurfaceTexture:SurfaceTexture? = null
//老相機數據存儲數組
private var oldBuffers:ByteArray? = null
//老相機
private var oldCamera:Camera? = null
//老相機預覽尺寸
lateinit var oldSize:Camera.Size
/**
* 老相機API開始預覽
*/
fun startPreview(surfaceTexture:SurfaceTexture){
if (oldSurfaceTexture == null){
oldSurfaceTexture = surfaceTexture;
}
// 打開攝像頭並將展示方向旋轉90度
oldCamera = Camera.open(oldCameraId)
oldCamera!!.setDisplayOrientation(90)
val parameters = oldCamera!!.parameters
// 選擇合適的預覽尺寸
val sizeList = parameters.supportedPreviewSizes
oldSize = getOldSize(sizeList)
parameters.previewFormat = ImageFormat.NV21
//設置預覽圖像參數
parameters.setPictureSize(oldSize.width,oldSize.height)
parameters.setPreviewSize(oldSize.width,oldSize.height)
oldCamera!!.parameters = parameters
oldCamera!!.setPreviewTexture(oldSurfaceTexture)
//獲取預覽數據
oldBuffers = ByteArray(oldSize.width * oldSize.height * 4)
oldCamera!!.addCallbackBuffer(oldBuffers)
oldCamera!!.setPreviewCallbackWithBuffer(previewCallback)
oldCamera!!.startPreview()
}
/**
* 獲取老相機預覽數據回調
*/
private val previewCallback = Camera.PreviewCallback { data, camera ->
camera?.addCallbackBuffer(oldBuffers)
oldSendVideoData(data)
}
/**
* 老相機API預覽數據回調
*/
abstract fun oldSendVideoData(data:ByteArray)
/**
* 停止預覽
*/
fun stopPreview(){
if (oldCamera != null){
oldCamera!!.stopPreview()
oldCamera!!.release()
oldCamera = null
}
}
/**
* 老camera切換攝像頭
*/
private fun oldSwitchCamera(){
if (oldSurfaceTexture != null){
oldCameraId = if (oldCameraId == Camera.CameraInfo.CAMERA_FACING_BACK){
Camera.CameraInfo.CAMERA_FACING_FRONT
}else{
Camera.CameraInfo.CAMERA_FACING_BACK
}
stopPreview()
startPreview(oldSurfaceTexture!!)
}
}
/**
* 獲取老相機API的預覽大小
*/
private fun getOldSize(sizeList:List<Camera.Size>):Camera.Size{
val aspectRatios = ArrayList<Float>()
aspectRatios.add(16.toFloat() / 9)
aspectRatios.add(4.toFloat() / 3)
aspectRatios.add(18.toFloat() / 9)
for (aspectRatio in aspectRatios){
for (size in sizeList){
if (size.width.toFloat() / size.height == aspectRatio) {
return size
}
}
}
return sizeList[0]
}
}