在我們的生活和工作當中,不管是智能家居系統還是智能硬件與穿戴,為了在基本功能的基礎上增強系統的用戶的體驗和功能擴充,往往會有視頻流采集的強大功能得以實現,視頻采集的方式大同小異,這里我們以視頻流服務器mjpg-streamer作為視頻采集的基礎展開。視頻流服務器可能有些技術人員不是很陌生,它是一款輕量級的視頻流服務器軟件,基本的功能是從攝像頭硬件采集圖形圖像并且上傳到客戶端,我們就是利用這一點通過Android客戶端應用程序獲得底層的視頻圖形圖像的。Android應用上位機程序通過網絡編碼實現視頻流數據的獲取和解析,完成如下圖片的效果,從而流暢的實現視頻的實時采集工作。
這里視頻流服務器是底層系統的一個軟件支持,發揮著視頻的實時傳輸的基本功能,系統大多是linux或openwrt的移植版本, 對于有一定的嵌入式基礎的技術工程師是比較容易上手的,其間可以用wifi也可以是有線以太網傳輸,但是根據體驗的方便系數我們通常選擇wifi作為傳輸媒介。視頻傳輸的流程圖如下:
終端的app需要通過Android網絡編程基本原理實現數據的傳輸,這里使用URL作為網絡資源的統一資源定位符,是指向互聯網“資源”的指針。資源可以是簡單的目錄或文件,也可以是對更復雜的對象引用,例如對數據庫或搜索引擎的查詢,通常情況而言,URL由協議名,主機,端口號和資源組成,其格式為Protocol://host:port/resourceName,而如果想通過視頻流服務器實現數據的采集,假設網卡的ip是192.168.1.1,則URL格式為://192.168.1.1:8080/?action=stream,這樣一個URL就會請求視頻流服務器的圖像數據, 為后續的Android數據獲取與解析做準備。
Android網絡編程以及圖像解碼方面核心代碼步驟如下:
1.我們需要綁定上述的URL(//192.168.1.1:8080/?action=stream),并且建立URL對象:
URL url = new URL("//192.168.1.1:8080/?action=stream")。
2.Android客戶端通過URL對象和遠程的服務器建立連接對象,通過客戶端的wifi模塊獲得視頻流服務器的視頻圖像,即通過Http把數據反饋給端口號為8080的客戶端,實現操作如下:
HttpURLConnection urlConn = (HttpURLConnection)url.openConnection();
3.在配置好連接對象的前提下,底層的服務器發送的數據是經過封裝的內容,需要我們具體細化的解析和處理,這里我們使用的是io的輸入流讀操作,通過輸入流的read()方法把數據放到buffer緩沖區,同時獲得數據的大小read,如下代碼實現數據內容和數據內容大小的獲得:
int read = urlConn.getInputStream().read(buffer, 0, readSize);
4.數據內容已經獲得存儲在緩沖區buffer里面,需要對數據進行解析,視頻流數據返回的內容的開始是“Content-Length:”,其后是網絡傳輸的圖像的特殊標志,一幀圖像的特殊標記由2個字節組成,圖像的開始標記(包頭)是0xFF+0xD8, 圖像的結束標記(包尾)是0xFF+0xD9,在0xFFD8和0xFFD9之間的就是我們需要的圖像(通常是jpeg圖像),如果循環獲取逐幀圖像就形成了完整的視頻流,大家就可以真實的獲得攝像頭的實時采集信息了。
宏觀數據流圖如下:
解碼代碼如下:
switch(status){//采集狀態記錄
case 0:
if (buffer[i] == (byte) 'C')//接收字節'C'
status++;//繼續采集狀態累加
else
status = 0;//如果不是符合要求的數據封裝就丟棄當前的幀,進行下一次數據采集
break;
case 1:
if (buffer[i] == (byte) 'o')//接收字節'o'
status++;//繼續采集狀態累加
else
status = 0;//如果不是符合要求的數據封裝就丟棄當前的幀,進行下一次數據采集
break;
case 2:
if (buffer[i] == (byte) 'n')//接收字節'n'
status++;//繼續采集狀態累加
else
status = 0;//如果不是符合要求的數據封裝就丟棄當前的幀,進行下一次數據采集
break;
case 3:
if (buffer[i] == (byte) 't')//接收字節't'
status++;//繼續采集狀態累加
else
status = 0;//如果不是符合要求的數據封裝就丟棄當前的幀,進行下一次數據采集
break;
case 4:
if (buffer[i] == (byte) 'e')//接收字節'e'
status++;//繼續采集狀態累加
else
status = 0;//如果不是符合要求的數據封裝就丟棄當前的幀,進行下一次數據采集
break;
case 5:
if (buffer[i] == (byte) 'n')//接收字節'n'
status++;//繼續采集狀態累加
else
status = 0;//如果不是符合要求的數據封裝就丟棄當前的幀,進行下一次數據采集
break;
case 6:
if (buffer[i] == (byte) 't')//接收字節't'
status++;//繼續采集狀態累加
else
status = 0;//如果不是符合要求的數據封裝就丟棄當前的幀,進行下一次數據采集
break;
case 7:
if (buffer[i] == (byte) '-')//接收字節'-'
status++;//繼續采集狀態累加
else
status = 0;//如果不是符合要求的數據封裝就丟棄當前的幀,進行下一次數據采集
break;
case 8:
if (buffer[i] == (byte) 'L')//接收字節'L'
status++;//繼續采集狀態累加
else
status = 0;//如果不是符合要求的數據封裝就丟棄當前的幀,進行下一次數據采集
break;
case 9:
if (buffer[i] == (byte) 'e')//接收字節'e'
status++;//繼續采集狀態累加
else
status = 0;//如果不是符合要求的數據封裝就丟棄當前的幀,進行下一次數據采集
break;
case 10:
if (buffer[i] == (byte) 'n')//接收字節'n'
status++;//繼續采集狀態累加
else
status = 0;//如果不是符合要求的數據封裝就丟棄當前的幀,進行下一次數據采集
break;
case 11:
if (buffer[i] == (byte) 'g')//接收字節'g'
status++;//繼續采集狀態累加
else
status = 0;//如果不是符合要求的數據封裝就丟棄當前的幀,進行下一次數據采集
break;
case 12:
if (buffer[i] == (byte) 't')//接收字節't'
status++;//繼續采集狀態累加
else
status = 0;//如果不是符合要求的數據封裝就丟棄當前的幀,進行下一次數據采集
break;
case 13:
if (buffer[i] == (byte) 'h')//接收字節'h'
status++;//繼續采集狀態累加
else
status = 0;//如果不是符合要求的數據封裝就丟棄當前的幀,進行下一次數據采集
break;
case 14:
if (buffer[i] == (byte) ':')//接收字節':'
status++;//繼續采集狀態累加
else
status = 0;//如果不是符合要求的數據封裝就丟棄當前的幀,進行下一次數據采集
break;
case 15:
if (buffer[i] == (byte) 0xFF)//圖像開始標示1:0xFF
status++;//繼續采集狀態累加
jpg_count = 0;//圖像數據數組記錄下標
jpg_buf[jpg_count++] = (byte) buffer[i];//進行圖像信息數據存儲
break;
case 16:
if (buffer[i] == (byte) 0xD8) {//圖像開始標示2:0xD8
status++;//繼續采集狀態累加
jpg_buf[jpg_count++] = (byte) buffer[i];//進行圖像信息數據存儲
} else {
if (buffer[i] != (byte) 0xFF)
status = 15;//更改采集狀態,重新圖片數據內容
}
break;
case 17:
jpg_buf[jpg_count++] = (byte) buffer[i];//進行圖像信息數據存儲
if (buffer[i] == (byte) 0xFF)//圖像結束標示1:0xFF
status++;//繼續采集狀態累加
if (jpg_count >= bufSize)//異常處理,安全操作
status = 0;//如果不是符合要求的數據封裝就丟棄當前的幀,進行下一次數據采集
break;
case 18:
jpg_buf[jpg_count++] = (byte) buffer[i];//進行圖像信息數據存儲
if (buffer[i] == (byte) 0xD9) {//圖像結束標示2:0xD9
status = 0;//圖片接收完畢,進行下一次數據采集
break;
} // 一幀圖像接收完成
5.將獲得的幀圖像數據流數組jgp_buf通過Bitmap類的decodeByteArray()方法轉換為Bitmap圖像對象,為后續的圖像顯示到界面做準備,代碼如下:
Bitmap bmp = BitmapFactory.decodeByteArray(jpg_buf,0,jpg_buf.length);
并且調整圖片的大小適應現實界面,代碼如下:
Bitmap mBitmap = Bitmap.createScaledBitmap(bmp,width, height,false);
6.目前底層的幀圖片已經準備就緒,接下來要進行圖片在界面的繪制工作,由于循環讀取的數據巨大,同時還要保持實時性,所以這里使用一種特殊的視圖稱為SurfaceView,它擁有獨立的繪圖表面,即它不與其宿主窗口共享同一個繪圖表面。由于擁有獨立的繪圖表面,因此SurfaceView的UI就可以在一個獨立的線程中進行繪制。又由于不會占用主線程資源,SurfaceView一方面可以實現復雜而高效的UI設計,另一方面又不會導致用戶輸入得不到及時響應。如果要進行SurfaceView的圖像UI繪制,我們需要進行如下的步驟:
(1)通過SurfaceView的 getHolder()獲得SurfaceHolder對象,代碼如下:
SurfaceHolder sfh = new SurfaceView().getHolder();
(2) 通過SurfaceHolder的lockCanvas()方法在繪圖表面的基礎上建立一塊畫布,即獲得一個Canvas對象,代碼如下:
Canvas canvas = sfh.lockCanvas();
(3)通過Canvas對象的方法繪制UI圖像畫布,代碼如下:
canvas.drawBitmap(mBitmap, 0, 0, null);
(4) 將已經填充好的UI數據的畫布緩沖區通過SurfaceHolder對象的unlockCanvasAndPost()方法提交給SurfaceFlinger服務,以便SurfaceFlinger服務可以將它合成到屏幕上去,代碼如下:
sfh.unlockCanvasAndPost(canvas);
圖像效果如下:
华清图书馆
0元电子书,限时免费申领10本华清图书PDF版
扫码关注华清远见公众号