Android的簡(jiǎn)易投屏(不包含音頻的數(shù)據(jù)傳遞)

一姐呐、簡(jiǎn)述

  1. 投屏技術(shù)主要就是將自己手機(jī)所展示的內(nèi)容通過傳輸媒介傳遞到另一個(gè)設(shè)備
  2. 所要傳輸?shù)膬?nèi)容需要經(jīng)過一定的規(guī)范傳到其他設(shè)備才能夠被解析
  3. 傳輸技術(shù)也是多樣化力细,但是投屏是實(shí)時(shí)傳輸?shù)模砸话愣际腔趕ocket

二雀哨、技術(shù)選取

  1. 內(nèi)容獲取 主要采用的是錄屏 MediaProjection來錄屏
  2. 數(shù)據(jù)規(guī)范 因?yàn)榫W(wǎng)絡(luò)傳輸?shù)难訒r(shí)和不可靠性磕谅,需要數(shù)據(jù)在容量上要小一些,才能更加容易的傳輸震束,在此我主要選取的H265壓縮怜庸,搭配MediaCode進(jìn)行硬編解碼。
  3. 網(wǎng)絡(luò)層 網(wǎng)絡(luò)層我們主要選取的是WebSocket進(jìn)行網(wǎng)絡(luò)數(shù)據(jù)的傳輸

三垢村、處理步驟(手機(jī)A 投屏端)

  1. 權(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)
···
  1. 獲取錄屏結(jié)果(onActivityResult)
val mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data)
  1. 建立鏈接
webSocketServer=MyWebSocket(this,InetSocketAddress(socketPort))
webSocketServer.start()
  1. 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


image.png

在工作線程中啟動(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)嘉栓?

  1. 學(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ù)

  1. 接收數(shù)據(jù)
val url = URI("ws://192.168.x.xxx:$port") // 投屏手機(jī)的ip地址  端口號(hào)自己定義
myWebSocketClient = MyWebSocketClient(url)
myWebSocketClient.connect()
  1. 注冊(cè)數(shù)據(jù)回調(diào)接收 WebSocketClient
override fun onMessage(bytes: ByteBuffer) {
   val buf = ByteArray(bytes.remaining())
   bytes.get(buf)
   callback?.callBack(buf)
}
  1. 解碼并顯示視頻+初始化解碼流程
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)容

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末译隘,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子题篷,更是在濱河造成了極大的恐慌厅目,老刑警劉巖番枚,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異葫笼,居然都是意外死亡拗馒,警方通過查閱死者的電腦和手機(jī)路星,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來诱桂,“玉大人,你說我怎么就攤上這事友绝「尉ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵掷漱,是天一觀的道長。 經(jīng)常有香客問我切威,道長丙号,這世上最難降的妖魔是什么缰冤? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮怀薛,結(jié)果婚禮上迷郑,老公的妹妹穿的比我還像新娘创倔。我一直安慰自己焚碌,他們只是感情好畦攘,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布知押。 她就那樣靜靜地躺著鹃骂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪畏线。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天温亲,我揣著相機(jī)與錄音杯矩,去河邊找鬼栈虚。 笑死魂务,一個(gè)胖子當(dāng)著我的面吹牛泌射,可吹牛的內(nèi)容都是我干的粘姜。 我是一名探鬼主播熔酷,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼拒秘,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了躺酒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤揽碘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后雳刺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡浑此,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年凛俱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了料饥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蒲犬。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡原叮,死狀恐怖巡蘸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情悦荒,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布境氢,位于F島的核電站,受9級(jí)特大地震影響萍聊,放射性物質(zhì)發(fā)生泄漏悦析。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一亭螟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧酌泰,春花似錦匕累、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽狸剃。三九已至,卻和暖如春钞馁,著一層夾襖步出監(jiān)牢的瞬間匿刮,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國打工训措, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人绩鸣。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓纱兑,卻偏偏與公主長得像,于是被迫代替她去往敵國和親总珠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子勘纯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容