一姐呐、簡(jiǎn)述
- 投屏技術(shù)主要就是將自己手機(jī)所展示的內(nèi)容通過傳輸媒介傳遞到另一個(gè)設(shè)備
- 所要傳輸?shù)膬?nèi)容需要經(jīng)過一定的規(guī)范傳到其他設(shè)備才能夠被解析
- 傳輸技術(shù)也是多樣化力细,但是投屏是實(shí)時(shí)傳輸?shù)模砸话愣际腔趕ocket
二雀哨、技術(shù)選取
- 內(nèi)容獲取 主要采用的是錄屏 MediaProjection來錄屏
- 數(shù)據(jù)規(guī)范 因?yàn)榫W(wǎng)絡(luò)傳輸?shù)难訒r(shí)和不可靠性磕谅,需要數(shù)據(jù)在容量上要小一些,才能更加容易的傳輸震束,在此我主要選取的H265壓縮怜庸,搭配MediaCode進(jìn)行硬編解碼。
- 網(wǎng)絡(luò)層 網(wǎng)絡(luò)層我們主要選取的是WebSocket進(jìn)行網(wǎng)絡(luò)數(shù)據(jù)的傳輸
三垢村、處理步驟(手機(jī)A 投屏端)
- 權(quán)限處理
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
2.錄屏
mediaProjectionManager =
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
val captureIntent: Intent = mediaProjectionManager.createScreenCaptureIntent()
startActivityForResult(captureIntent, 1)
···
- 獲取錄屏結(jié)果(onActivityResult)
val mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data)
- 建立鏈接
webSocketServer=MyWebSocket(this,InetSocketAddress(socketPort))
webSocketServer.start()
- MeidaCodec+H265 初始化編碼
var format = MediaFormat.createVideoFormat(MIMETYPE_VIDEO_HEVC,width,height).apply {
setInteger(KEY_COLOR_FORMAT,MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
setInteger(KEY_BIT_RATE,width * height)
setInteger(KEY_FRAME_RATE,20)
setInteger(KEY_I_FRAME_INTERVAL,1)
}
mediaCodec= MediaCodec.createEncoderByType("video/hevc")
mediaCodec.configure(format,null,null, CONFIGURE_FLAG_ENCODE)
var surface = mediaCodec.createInputSurface()
mediaProjection.createVirtualDisplay(
"-display",
width,height,1,
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,surface,null,null)
這里可以查看Mediacodec支持的視頻編碼
avc對(duì)應(yīng)得就是h264 hevc對(duì)應(yīng)的是h265
在工作線程中啟動(dòng)編碼,并不斷獲取編碼結(jié)果
mediaCodec.start()
var bufferInfo = MediaCodec.BufferInfo()
while (true){
try {
var outputBufferId = mediaCodec.dequeueOutputBuffer(bufferInfo, 10000)
if (outputBufferId >= 0) {
val byteBuffer = mediaCodec.getOutputBuffer(outputBufferId)
dealFrame(byteBuffer,bufferInfo)//這個(gè)方法比較重要 后面會(huì)講到
mediaCodec.releaseOutputBuffer(outputBufferId,false)
}
} catch (e: Exception) {
e.printStackTrace()
Log.e("我的線程","${e.message}")
break
}
}
6:發(fā)送數(shù)據(jù)
if (webSocket != null && webSocket!!.isOpen) {
webSocket!!.send(bytes)
}
其實(shí)步驟很簡(jiǎn)單割疾,有點(diǎn)難度的就是編碼的處理上dealFrame(byteBuffer,bufferInfo)。接下來我們分析下為啥這個(gè)地方有難點(diǎn)嘉栓?
- 學(xué)習(xí)過h265編碼的我們都知道h265編碼的頭部主要是vps+sps+pps作為一幀宏榕,vps pps sps這些知識(shí)不在這里敘述可自行百度。pps和sps是播放視頻最關(guān)鍵的東西侵佃,里面包含了視頻的參數(shù)信息麻昼,正常的網(wǎng)絡(luò) 視頻或者本地視頻也好,一個(gè)視頻里面必定有一組這些信息 (vps是h265有的)馋辈,但是投屏不一樣抚芦,它是實(shí)時(shí)傳播的加上網(wǎng)絡(luò)的不確定性,在接受端就可以出現(xiàn)pps和sps等信息的丟失迈螟,所以我們必須保證在I幀(關(guān)鍵幀)之前都必須有一組這樣的信息存在叉抡。大家可以想象一下直播,大家在任何時(shí)候進(jìn)入直播間都能看到直播內(nèi)容答毫,如果沒有sps和pps等信息我們就沒法知道編解碼信息和視頻信息褥民,就沒法播放。
所以基于此我們來看下具體的處理步驟
1:先儲(chǔ)存sps等的信息
2:如果編碼出來的是i幀就把sps等信息加入到i幀的前面一起發(fā)送
3:如果編碼出來的是B幀或者P幀我們不用管可以直接發(fā)送
(注:I幀是視頻的關(guān)鍵幀洗搂,P幀和B幀都是根據(jù)算法計(jì)算出來的參考幀消返,屬于幀間壓縮內(nèi)容,B幀和P幀是沒法獨(dú)立播放的,大家可以去查詢下相關(guān)資料)
好了不廢話了上代碼看下處理的步驟
val NAL_I = 19
val NAL_VPS = 32
private lateinit var vps_sps_pps_buf: ByteArray
private fun dealFrame(byteBuffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) {
var offset=4
if (byteBuffer[2] == 0x01.toByte()) {
offset=3
}
var buffer:Byte = byteBuffer.get(offset)
var and = buffer.and(0x7E)
var type=and/2//右移一位
if (type == NAL_VPS) {//vps頭
vps_sps_pps_buf= ByteArray(bufferInfo.size)
byteBuffer.get(vps_sps_pps_buf)//將vps數(shù)據(jù)保存
}else if (type == NAL_I) {//i幀
var bytes = ByteArray(bufferInfo.size)
byteBuffer.get(bytes)//i幀的話先存儲(chǔ)
var newBuf = ByteArray(vps_sps_pps_buf.size + bytes.size)//創(chuàng)建一個(gè)數(shù)組長度為vps+i幀
//先拷貝vps 在拷貝I幀
System.arraycopy(vps_sps_pps_buf, 0, newBuf, 0, vps_sps_pps_buf.size)
System.arraycopy(bytes, 0, newBuf, vps_sps_pps_buf.size, bytes.size);
this.socketLive.sendData(newBuf)//直接發(fā)送出去
} else {
var bytes = ByteArray(bufferInfo.size)
byteBuffer.get(bytes)//B或者P幀的話先存儲(chǔ)
this.socketLive.sendData(bytes)//直接發(fā)送出去
}
}
以上就是投屏端的簡(jiǎn)易版耘拇。
下面介紹接收端
(手機(jī)B 接收端)
接收端比較簡(jiǎn)單些只涉及到接收數(shù)據(jù)和解碼顯示數(shù)據(jù)
- 接收數(shù)據(jù)
val url = URI("ws://192.168.x.xxx:$port") // 投屏手機(jī)的ip地址 端口號(hào)自己定義
myWebSocketClient = MyWebSocketClient(url)
myWebSocketClient.connect()
- 注冊(cè)數(shù)據(jù)回調(diào)接收 WebSocketClient
override fun onMessage(bytes: ByteBuffer) {
val buf = ByteArray(bytes.remaining())
bytes.get(buf)
callback?.callBack(buf)
}
- 解碼并顯示視頻+初始化解碼流程
var mediaCodec: MediaCodec? = null
private fun initDecoder(surface: Surface) {
try {
mediaCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_HEVC)
val format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_HEVC, 720, 1280)
format.setInteger(MediaFormat.KEY_BIT_RATE, 720 * 1280)
format.setInteger(MediaFormat.KEY_FRAME_RATE, 20)
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
mediaCodec!!.configure(
format,
surface,
null, 0
)
mediaCodec!!.start()
} catch (e: IOException) {
e.printStackTrace()
}
}
override fun callBack(data: ByteArray?) {
val index = mediaCodec!!.dequeueInputBuffer(100000)
if (index >= 0) {
val inputBuffer: ByteBuffer? = mediaCodec!!.getInputBuffer(index)
inputBuffer?.clear()
inputBuffer?.put(data, 0, data!!.size)
// 通知dsp芯片幫忙解碼
mediaCodec!!.queueInputBuffer(
index,
0,data!!.size, System.currentTimeMillis(), 0
)
}
// 獲取數(shù)據(jù)
val bufferInfo = MediaCodec.BufferInfo()
var outputBufferIndex = mediaCodec!!.dequeueOutputBuffer(bufferInfo, 100000)
while (outputBufferIndex > 0) {
mediaCodec!!.releaseOutputBuffer(
outputBufferIndex, true
)
outputBufferIndex = mediaCodec!!.dequeueOutputBuffer(bufferInfo, 0)
}
}
mediaCodec!!.configure(
format,
surface,
null, 0
)
這里面在初始化的時(shí)候已經(jīng)設(shè)置了自動(dòng)解碼到surface所以顯示頁是自動(dòng)處理的撵颊。
項(xiàng)目還沒有放到git上去,以后會(huì)更新地址惫叛。以上內(nèi)容都是基于自己的理解倡勇,內(nèi)容如果有出錯(cuò)希望各位不吝賜教,感激不盡挣棕。
下一篇應(yīng)該會(huì)寫關(guān)于音視頻通話的內(nèi)容