android錄屏直播:VLC通過rtsp協(xié)議播放android錄屏實(shí)時(shí)視頻(Java實(shí)現(xiàn))2020-06-19

首先說下為什么要做這樣一個(gè)東西

? ? ? ? ? 在上家公司的時(shí)候,作為客戶端開發(fā),一個(gè)月要給領(lǐng)導(dǎo)演示異常app的開發(fā)成果,當(dāng)時(shí)用的策略是用錄屏類軟件,錄制成mp4,然后通過投影播放mp4文件,來給領(lǐng)導(dǎo)看。這樣做帶來的問題是,要提前準(zhǔn)備mp4需要時(shí)間,領(lǐng)導(dǎo)想要看除了mp4外的內(nèi)容時(shí),體驗(yàn)不好。自己對(duì)流媒體知識(shí)有一些了解,所以就想做一個(gè)直播android屏幕的app,這就是想做這樣一個(gè)東西的原因。

項(xiàng)目地址:GITHUB

https://github.com/sszhangpengfei/AndroidShow

說下為什么選擇rtsp協(xié)議

? ? ? ? ? 本來是想用rtmp來做流媒體協(xié)議的,如果用rtmp,手機(jī)作為推流端,將視頻推給rtmp服務(wù)器,vlc等客戶端可以播放。但是這樣做還需要一個(gè)流媒體服務(wù)器,所以選擇了rtsp協(xié)議。

? ? ? ? ? 思路是:手機(jī)端作為rtsp服務(wù)端,vlc作為客戶端,通過rtp協(xié)議來傳輸視頻流。這樣做就省去了搭建流媒體服務(wù)的工作。這樣做事參考了github上開源項(xiàng)目spydroid來做的。本項(xiàng)目所有功能使用java實(shí)現(xiàn)。

結(jié)構(gòu)簡(jiǎn)述

? ? ? ? ? ? app相當(dāng)于一個(gè)rtsp服務(wù)器,vlc相當(dāng)于客戶端,通過rtsp協(xié)議與app的服務(wù)端交互,rtsp交互成功后,setup成功后服務(wù)端通過rtp協(xié)議開始推流。大概結(jié)構(gòu)如下圖所示

代碼簡(jiǎn)析

android采集屏幕視頻數(shù)據(jù)

這一步內(nèi)容,可以看我之前的一片博客,連接如下:

android通過MediaProjectionManager錄屏關(guān)聯(lián)MediaCodec獲取h264數(shù)據(jù)

rtsp server端的搭建

rtsp協(xié)議的交互過程

rtsp的簡(jiǎn)單交互過,以此app為例,來簡(jiǎn)單說下:

1.vlc發(fā)送OPTIONS報(bào)文到server端,server端根據(jù)計(jì)算結(jié)果,返回200 ok或者其他錯(cuò)誤;

2.vlc發(fā)送DESCRIBE報(bào)文,server返回報(bào)文,報(bào)文中的字段,感興趣的同學(xué)可以自己搜索下;

3.vlc發(fā)送SETUP報(bào)文,server返回,如果setup成功,server端會(huì)進(jìn)行rtp發(fā)送的準(zhǔn)備工作;

這一步還是比較關(guān)鍵的,服務(wù)端會(huì)告訴客戶端Transport用的是rtp/udp還是rtp/tcp,告訴客戶端一些端口的相關(guān)信息。

4.vlc發(fā)送PLAY報(bào)文,server返回;

5.如果播放結(jié)束,客戶端可以發(fā)送TEARDOWN報(bào)文,一次完整的RTSP交互結(jié)束。

代碼實(shí)現(xiàn)簡(jiǎn)述

本項(xiàng)目用的rtsp server端代碼,是從GITHUB開源項(xiàng)目spydroid中摘取的,感情去的同學(xué)可以看下這個(gè)項(xiàng)目,代碼些的很好。

工程中rtsp涉及到的文件如下:

RtspServer集成android Service,是整個(gè)rtsp功能部分的入口文件。

流處理類的繼承關(guān)系: ScreenStream->VideoStream->MediaStream->Stream,ScreenStream為自己實(shí)現(xiàn),VideoStream,MediaStream為抽象類,定義了關(guān)于流的一些基本的屬性集方法,比如設(shè)置sps pps 端口等。

h264打包rtp的集成關(guān)系: H264Packetizer->AbstractPacketizer,下面這個(gè)核心功能函數(shù)的作用就是講264幀打包成rtp包的作用

? ? private void send() throws IOException, InterruptedException {

? ? int sum = 1, len = 0, type;


? ? if (streamType == 0) {

? ? // NAL units are preceeded by their length, we parse the length

? ? fill(header,0,5);

? ? ts += delay;

? ? naluLength = header[3]&0xFF | (header[2]&0xFF)<<8 | (header[1]&0xFF)<<16 | (header[0]&0xFF)<<24;


? ? if (naluLength>100000 || naluLength<0) resync();

? ? } else if (streamType == 1) {

? ? // NAL units are preceeded with 0x00000001

? ? fill(header,0,5);

? ? ts = ((ScreenInputStream)is). getLastts();

? ? //ts += delay;

? ? naluLength = is.available();

? ? Log.d(TAG,"header is? "+header[0]+" "+header[1]+" "+header[2]+" "+header[3]+" "+header[4]+"? ts = "+ts+" nalu len = "+naluLength+"");

? ? if (!(header[0]==0 && header[1]==0 && header[2]==0)) {

? ? // Turns out, the NAL units are not preceeded with 0x00000001

? ? Log.e(TAG, "NAL units are not preceeded by 0x00000001");

? ? streamType = 2;

? ? return;

? ? }

? ? } else {

? ? // Nothing preceededs the NAL units

? ? fill(header,0,1);

? ? header[4] = header[0];

? ? ts = ((ScreenInputStream)is). getLastts();

? ? //ts += delay;

? ? naluLength = is.available()+1;

? ? }

? ? // Parses the NAL unit type

? ? type = header[4]&0x1F;

? ? Log.d(TAG,"NAL type is "+type+"");

? ? // The stream already contains NAL unit type 7 or 8, we don't need

? ? // to add them to the stream ourselves

? ? if (type == 7 || type == 8) {

? ? Log.v(TAG,"SPS or PPS present in the stream.");

? ? count++;

? ? if (count>4) {

? ? sps = null;

? ? pps = null;

? ? }

? ? }


? ? //Log.d(TAG,"- Nal unit length: " + naluLength + " delay: "+delay/1000000+" type: "+type);


? ? // Small NAL unit => Single NAL unit

? ? if (naluLength<=MAXPACKETSIZE-rtphl-2) {

? ? buffer = socket.requestBuffer();

? ? buffer[rtphl] = header[4];

? ? len = fill(buffer, rtphl+1,? naluLength-1);

? ? socket.updateTimestamp(ts);

? ? socket.markNextPacket();

? ? super.send(naluLength+rtphl);

? ? //Log.d(TAG,"----- Single NAL unit - len:"+len+" delay: "+delay);

? ? }

? ? // Large NAL unit => Split nal unit

? ? else {


? ? // Set FU-A header

? ? header[1] = (byte) (header[4] & 0x1F);? // FU header type

? ? header[1] += 0x80; // Start bit

? ? // Set FU-A indicator

? ? header[0] = (byte) ((header[4] & 0x60) & 0xFF); // FU indicator NRI

? ? header[0] += 28;


? ? while (sum < naluLength) {

? ? buffer = socket.requestBuffer();

? ? buffer[rtphl] = header[0];

? ? buffer[rtphl+1] = header[1];

? ? socket.updateTimestamp(ts);

? ? if ((len = fill(buffer, rtphl+2,? naluLength-sum > MAXPACKETSIZE-rtphl-2 ? MAXPACKETSIZE-rtphl-2 : naluLength-sum? ))<0) return; sum += len;

? ? // Last packet before next NAL

? ? if (sum >= naluLength) {

? ? // End bit on

? ? buffer[rtphl+1] += 0x40;

? ? socket.markNextPacket();

? ? }

? ? super.send(len+rtphl+2);

? ? // Switch start bit

? ? header[1] = (byte) (header[1] & 0x7F);

? ? //Log.d(TAG,"----- FU-A unit, sum:"+sum);

? ? }

? ? }

? ? }

RtpSocket類的作用:將打包好的rtp包通過socket發(fā)送,這個(gè)類用的是多播udp發(fā)送的。

該類繼承Runnable接口,在該線程中進(jìn)行數(shù)據(jù)的發(fā)送,包括rtcp報(bào)文

? ? /** The Thread sends the packets in the FIFO one by one at a constant rate. */

? ? @Override

? ? public void run() {

? ? Statistics stats = new Statistics(50,3000);

? ? try {

? ? // Caches mCacheSize milliseconds of the stream in the FIFO.

? ? Thread.sleep(mCacheSize);

? ? long delta = 0;

? ? while (mBufferCommitted.tryAcquire(4,TimeUnit.SECONDS)) {

? ? if (mOldTimestamp != 0) {

? ? // We use our knowledge of the clock rate of the stream and the difference between two timestamps to

? ? // compute the time lapse that the packet represents.

? ? if ((mTimestamps[mBufferOut]-mOldTimestamp)>0) {

? ? stats.push(mTimestamps[mBufferOut]-mOldTimestamp);

? ? long d = stats.average()/1000000;

? ? //Log.d(TAG,"delay: "+d+" d: "+(mTimestamps[mBufferOut]-mOldTimestamp)/1000000);

? ? // We ensure that packets are sent at a constant and suitable rate no matter how the RtpSocket is used.

? ? if (mCacheSize>0) Thread.sleep(d);

? ? } else if ((mTimestamps[mBufferOut]-mOldTimestamp)<0) {

? ? Log.e(TAG, "TS: "+mTimestamps[mBufferOut]+" OLD: "+mOldTimestamp);

? ? }

? ? delta += mTimestamps[mBufferOut]-mOldTimestamp;

? ? if (delta>500000000 || delta<0) {

? ? //Log.d(TAG,"permits: "+mBufferCommitted.availablePermits());

? ? delta = 0;

? ? }

? ? }

? ? mReport.update(mPackets[mBufferOut].getLength(), System.nanoTime(),(mTimestamps[mBufferOut]/100L)*(mClock/1000L)/10000L);

? ? mOldTimestamp = mTimestamps[mBufferOut];

? ? if (mCount++>30) mSocket.send(mPackets[mBufferOut]);

? ? if (++mBufferOut>=mBufferCount) mBufferOut = 0;

? ? mBufferRequested.release();

? ? }

? ? } catch (Exception e) {

? ? e.printStackTrace();

? ? }

? ? mThread = null;

? ? resetFifo();

? ? }

Session SessionBuilder為Session管理類,每一個(gè)客戶端的連接都是一個(gè)Session對(duì)象

SendReport為發(fā)送RTCP報(bào)文的管理類

目前只實(shí)現(xiàn)了視頻功能,音頻功能暫未實(shí)現(xiàn)

項(xiàng)目地址:GITHUB

run該工程后,app啟動(dòng)后,界面目前很簡(jiǎn)單,頂部會(huì)有rtsp的地址,比如:rtsp://192.168.60.120:8086。中間有倆按鈕,開始錄屏和結(jié)束錄屏,點(diǎn)擊開始錄屏,此時(shí)會(huì)啟動(dòng)rtsp server,MediaCodec會(huì)將屏幕yuv編碼為h264。此時(shí)就可以在vlc中輸入rtsp地址,就可以播放了。

Session SessionBuilder為Session管理類,每一個(gè)客戶端的連接都是一個(gè)Session對(duì)象

SendReport為發(fā)送RTCP報(bào)文的管理類,

————————————————

版權(quán)聲明:本文為CSDN博主「sszpf」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接及本聲明。

原文鏈接:https://blog.csdn.net/ss182172633/article/details/79578372

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容