首先說下為什么要做這樣一個(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