1. 程式人生 > >android錄屏直播:VLC通過rtsp協議播放android錄屏實時視訊(Java實現)

android錄屏直播:VLC通過rtsp協議播放android錄屏實時視訊(Java實現)

首先說下為什麼要做這樣一個東西

          在上家公司的時候,作為客戶端開發,一個月要給領導演示異常app的開發成果,當時用的策略是用錄屏類軟體,錄製成mp4,然後通過投影播放mp4檔案,來給領導看。這樣做帶來的問題是,要提前準備mp4需要時間,領導想要看除了mp4外的內容時,體驗不好。自己對流媒體知識有一些瞭解,所以就想做一個直播android螢幕的app,這就是想做這樣一個東西的原因。

說下為什麼選擇rtsp協議

          本來是想用rtmp來做流媒體協議的,如果用rtmp,手機作為推流端,將視訊推給rtmp伺服器,vlc等客戶端可以播放。但是這樣做還需要一個流媒體伺服器,所以選擇了rtsp協議。

          思路是:手機端作為rtsp服務端,vlc作為客戶端,通過rtp協議來傳輸視訊流。這樣做就省去了搭建流媒體服務的工作。這樣做事參考了github上開源專案spydroid來做的。本專案所有功能使用java實現。

結構簡述

            app相當於一個rtsp伺服器,vlc相當於客戶端,通過rtsp協議與app的服務端互動,rtsp互動成功後,setup成功後服務端通過rtp協議開始推流。大概結構如下圖所示


程式碼簡析

android採集螢幕視訊資料

這一步內容,可以看我之前的一片部落格,連線如下:

rtsp server端的搭建

rtsp協議的互動過程

rtsp的簡單互動過,以此app為例,來簡單說下:

1.vlc傳送OPTIONS報文到server端,server端根據計算結果,返回200 ok或者其他錯誤;


2.vlc傳送DESCRIBE報文,server返回報文,報文中的欄位,感興趣的同學可以自己搜尋下;


3.vlc傳送SETUP報文,server返回,如果setup成功,server端會進行rtp傳送的準備工作;


這一步還是比較關鍵的,服務端會告訴客戶端Transport用的是rtp/udp還是rtp/tcp,告訴客戶端一些埠的相關資訊。

4.vlc傳送PLAY報文,server返回;


5.如果播放結束,客戶端可以傳送TEARDOWN報文,一次完整的RTSP互動結束。


程式碼實現簡述

本專案用的rtsp server端程式碼,是從GITHUB開源專案spydroid中摘取的,感情去的同學可以看下這個專案,程式碼些的很好。

工程中rtsp涉及到的檔案如下:


RtspServer整合android Service,是整個rtsp功能部分的入口檔案。

流處理類的繼承關係: ScreenStream->VideoStream->MediaStream->Stream,ScreenStream為自己實現,VideoStream,MediaStream為抽象類,定義了關於流的一些基本的屬性集方法,比如設定sps pps 埠等。

h264打包rtp的整合關係: H264Packetizer->AbstractPacketizer,下面這個核心功能函式的作用就是講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傳送,這個類用的是多播udp傳送的。

該類繼承Runnable介面,在該執行緒中進行資料的傳送,包括rtcp報文

/** 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管理類,每一個客戶端的連線都是一個Session物件

SendReport為傳送RTCP報文的管理類

目前只實現了視訊功能,音訊功能暫未實現

run該工程後,app啟動後,介面目前很簡單,頂部會有rtsp的地址,比如:rtsp://192.168.60.120:8086。中間有倆按鈕,開始錄屏和結束錄屏,點選開始錄屏,此時會啟動rtsp server,MediaCodec會將螢幕yuv編碼為h264。此時就可以在vlc中輸入rtsp地址,就可以播放了。

Session SessionBuilder為Session管理類,每一個客戶端的連線都是一個Session物件

SendReport為傳送RTCP報文的管理類,

相關推薦

android直播VLC通過rtsp協議播放android實時視訊Java實現

首先說下為什麼要做這樣一個東西          在上家公司的時候,作為客戶端開發,一個月要給領導演示異常app的開發成果,當時用的策略是用錄屏類軟體,錄製成mp4,然後通過投影播放mp4檔案,來給領導看。這樣做帶來的問題是,要提前準備mp4需要時間,領導想要看除了mp4外的

二叉樹三種遍歷方式及通過兩種遍歷重構二叉樹java實現

重構方法參考文章【重構二叉樹(Java實現):https://blog.csdn.net/wangbingcsu/article/details/51372695】 文章目錄 二叉樹類 三種遍歷方式 前序遍歷 中序遍歷 後序遍歷

LeetCode第23題合併K個有序連結串列JAVA實現

題目: 我的解答: 思路很簡單,把所有的資料先讀到ArrayList中然後轉到陣列中,然後排序,然後構建新連結串列 程式碼: /** * Definition for singly-linked list. * public class ListNode {

LeetCode第24題兩兩交換連結串列的節點JAVA實現

題目: 我的解答: /** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(i

深度優先遍歷和廣度優先遍歷Java實現

深度優先遍歷 深度優先遍歷,從初始訪問結點出發,我們知道初始訪問結點可能有多個鄰接結點,深度優先遍歷的策略就是首先訪問第一個鄰接結點,然後再以這個被訪問的鄰接結點作為初始結點,訪問它的第一個鄰接結點。總結起來可以這樣說:每次都在訪問完當前結點後首先訪問當前結點的

Leetcode題目訓練日記Java實現#2. Add Two Numbers

一、題目 You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order and each

約瑟夫環問題有n個人排成一列或是一圈,從編號為k的人開始報數,數到m的那個人出列。Java實現

文章目錄1.題目2.解析3.總程式碼 約瑟夫環問題 約瑟夫環描述:約瑟夫環(約瑟夫問題)是一個數學的應用問題:已知n個人(以編號1,2,3…n分別表示)圍坐在一張圓桌周圍。從編號為k的人開始報數,數到m的那個人出列;他的下一個人又從1開始報數,數到m的那個人又

通過呼叫API獲取zabbix監控服務JAVA實現案例

        因為專案保密的原因,實際程式碼無法貼出,但與他人部落格中的程式碼有一定的相似度,現拷貝部分作為參考,再結合我專欄中的技術文件,以便理解對zabbix的部分封裝過程。  Zabbix4jSampleGetHost.java package com.z

劍指offer面試題8二叉樹的下一個節點Java 實現

題目:給定一個二叉樹和其中的一個節點,如何找出中序遍歷序列的下一個節點?樹中的節點除了左右子節點外,還包含父節點。 思路: 節點分為有右子樹和沒有右子樹兩大類: 如果節點有右子樹,那麼它的下一個節點為它右子樹的最左節點 如果節點沒有右子樹,也可以分為兩類:(程

劍指offer面試題7重建二叉樹Java實現

題目:輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。例如輸入前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6},則重建二叉樹並返回。 思路:可以把二叉樹分為左右子樹分別構建,前序

劍指offer面試題6從尾到頭列印連結串列Java實現

題目:輸入一個連結串列的頭結點,從尾到頭反過來打印出每個結點的值。 思路:因為要實現從頭到尾遍歷,然後從尾到頭列印,也就是說第一個遍歷到的最後一個列印,最後遍歷到的第一個列印,這很明顯符合棧 “先進後出” 的特點,所以我們可以利用棧來實現這種順序。 測試用例: 功能測試:

劍指offer面試題5替換空格Java 實現

題目: 請實現一個函式,將一個字串中的空格替換成“%20”。例如,當字串為We Are Happy.則經過替換之後的字串為We%20Are%20Happy。 測試用例: 功能測試:輸入的字串包含空格(最前面、中間、最後面、連續多個空格) 邊界測試:輸入的字串沒有空格。

劍指offer面試題10斐波那契數列Java 實現

題目:大家都知道斐波那契數列,現在要求輸入一個整數n,請你輸出斐波那契數列的第n項。 思路:使用遞迴會重複計算,效率較低,可以用迴圈自下到上計算。 測試用例: 功能測試:輸入3、5、10 等。 邊界測試:輸入0、1、2 效能測試:輸入較大的數(如40、50、

劍指offer面試題12矩陣中的路徑Java實現

題目:請設計一個函式,用來判斷在一個矩陣中是否存在一條包含某字串所有字元的路徑。路徑可以從矩陣中的任意一個格子開始,每一步可以在矩陣中向左,向右,向上,向下移動一個格子。如果一條路徑經過了矩陣中的某一個格子,則該路徑不能再進入該格子。 例如 a b c e s f c s a d e e 矩

資料結構實驗之棧一進位制轉換java實現

資料結構實驗之棧一:進位制轉換 Time Limit: 1000MS Memory Limit: 65536KB Problem Description 輸入一個十進位制整數,將其轉換成對應

《劍指offer》- 面試題3陣列中重複的數字java實現

題目一:         在一個長度為n的數組裡的所有數字都在0到n-1的範圍內。 陣列中某些數字是重複的,但不知道有幾個數字是重複的。也不知道每個數字重複幾次。請找出陣列中任意一個重複的數字。 例如,如果輸入長度為7的陣列{2,3,1,0,2,5,3},那麼對應的輸出是重複

連結串列刪除連結串列中重複的結點java實現

題目描述 在一個排序的連結串列中,存在重複的結點,請刪除該連結串列中重複的結點,重複的結點不保留,返回連結串列頭指標。 例如,連結串列1->2->3->3->4->

算法系列插入排序的兩種改進規避邊界檢測和取消交換Java實現

前言:演算法第四版習題2.1.24插入排序的哨兵和習題2.1.25不需要交換的插入排序 規避邊界檢測: 在插入排序的實現中先找到最小的元素並將其置於陣列的第一個位置,可以省掉內迴圈的判斷條件 j>0 。能夠省略判斷條件的元素稱為哨兵。 public class Ex

LeetCode第18題四數之和JAVA實現

題目: 我的解答: public List<List<Integer>> fourSum(int[] nums, int target) { Arrays.sort(nums); List<List<Integer>&

多個數組間元素排列組合問題求解Java實現 標籤 遞迴排列組合迴圈

所有可以用遞迴實現的操作都可以轉化為用while、for等迴圈實現。 遞迴法 優缺點: 陣列數量不太多時用遞迴法確實使程式比較簡潔,陣列數量太多時遞迴函式棧過大,有可能導致執行時棧溢位