1、簡介
隨著物聯網(IoT)和智能設備的快速發展,邊緣計算技術已成為高效數據處理和服務交付的重要組成部分。當我們考慮利用邊緣端設備進行實時監控時,一個常見的需求是通過攝像頭捕捉視頻,并在局域網內實現視頻流的傳輸。這種設置不僅適用于家庭和小型企業的安全監控,也能滿足遠程教育、醫療監護等多個領域的需要。
面對局域網內的視頻流傳輸挑戰,有多種方法可以實現從攝像頭到顯示終端的數據傳遞,每種方法都有其特點和適用場景。本文將介紹一種基于TCP/IP協議棧和Socket編程的方法,這種方法因其穩定性和易用性而被廣泛采用。
選擇TCP/IP與Socket編程的理由:
穩定性:TCP(傳輸控制協議)作為面向連接的協議,確保了數據包在網絡中的有序傳遞,并提供了錯誤檢測與糾正機制,這對于視頻流這類對丟包敏感的數據尤為重要。
靈活性:使用Socket API可以在應用層直接操作網絡通信,給予開發者更大的自由度來定制視頻流的傳輸邏輯,比如調整幀率、分辨率或實施自定義的安全措施。
跨平臺支持:無論是Windows、Linux還是macOS,大多數操作系統都內置了對TCP/IP和Socket的支持,這意味著開發的應用程序具有良好的兼容性和移植性。
資源效率:對于邊緣端設備而言,優化后的TCP/IP服務和Socket編程可以幫助節省寶貴的計算資源和帶寬,保證即使在網絡條件有限的情況下也能維持視頻流的流暢性。
接下來,我們將探討如何利用這些技術構建一個簡易的局域網視頻流傳輸系統,包括具體的實現步驟和技術細節。您將看到,通過合理的設計和配置,即使是普通用戶也能夠在自己的環境中輕松搭建起一套實用的實時監控解決方案。如果您對視頻流傳輸有著更高的要求,如低延遲、高清晰度或更復雜的安全特性,后續部分還會提及一些進階技術和最佳實踐建議。
2、相關知識
2.1 TCP/IP協議簡介
TCP/IP(Transmission Control Protocol/Internet Protocol,傳輸控制協議/網際協議)是指能夠在多個不同網絡間實現信息傳輸的協議簇。TCP/IP協議不僅僅指的是TCP 和IP兩個協議,而是指一個由FTP、SMTP、TCP、UDP、IP等協議構成的協議簇, 只是因為在TCP/IP協議中TCP協議和IP協議最具代表性,所以被稱為TCP/IP協議。
TCP/IP傳輸協議是嚴格來說是一個四層的體系結構,應用層、傳輸層、網絡層和數據鏈路層都包含其中。
層級名稱 | 主要協議 | 功能描述 |
---|---|---|
應用層 | Telnet, FTP, SMTP, HTTP, HTTPS, DNS 等 | 接收來自傳輸層的數據或按不同應用要求與方式將數據傳輸至傳輸層;提供用戶接口和應用服務。 |
傳輸層 | TCP (傳輸控制協議), UDP (用戶數據報協議) | 提供端到端的通信服務,確保數據可靠地從一臺機器傳輸到另一臺機器(TCP)或不保證順序和可靠性但更快速的數據傳輸(UDP)。 |
網絡層 | IP (網際協議), ICMP, IGMP | 負責網絡中數據包的傳送,包括路由選擇、數據包轉發等;ICMP用于報告錯誤并交換有限的控制消息;IGMP用于管理組播成員關系。 |
網絡訪問層/鏈路層 | ARP (地址解析協議), RARP, Ethernet, Wi-Fi | 提供鏈路管理和錯誤檢測;處理對不同通信媒介有關的信息細節問題,如物理地址的解析(ARP),以及在局域網中直接傳遞數據幀。 |
2.2 TCP
TCP(傳輸控制協議)是TCP/IP模型傳輸層中最重要的協議之一,它提供了一種面向連接、可靠的字節流服務。這意味著在兩個應用程序之間建立通信之前,必須先通過三次握手過程來建立一個連接;而當數據傳輸完成后,則需要通過四次揮手過程來斷開這個連接。TCP確保了數據包按序到達,并且能夠檢測并重傳丟失或損壞的數據包,從而保證了數據傳輸的可靠性。
TCP的主要特點:
面向連接:在發送數據前,雙方必須建立一個連接。這種連接類似于電話呼叫,在通話開始前需要撥號建立連接。
可靠性:TCP使用確認機制和超時重傳來確保所有發送的數據都被接收方正確接收。如果接收方沒有收到某個數據包或者接收到的是損壞的數據包,它會請求發送方重新發送該數據包。
流量控制:為了防止快速的發送方淹沒慢速的接收方,TCP實現了滑動窗口機制來進行流量控制,根據接收方的能力調整發送速率。
擁塞控制:TCP還包含了一系列算法來避免網絡擁堵,例如慢啟動、擁塞避免、快重傳和快恢復等。
全雙工通信:TCP支持同時雙向的數據傳輸,即可以同時作為客戶端和服務端進行數據交換。
錯誤檢查與糾正:利用校驗和機制對每個數據段進行完整性驗證,以確保數據的準確性。
TCP適用場景:
文件傳輸(如FTP)
電子郵件(如SMTP, POP3, IMAP)
Web瀏覽(如HTTP, HTTPS)
遠程登錄(如SSH)
TCP工作流程概述
三次握手:用于建立連接。客戶端發送SYN(同步序列編號),服務器回應ACK(確認信息)和自己的SYN,然后客戶端再次回應ACK確認。
數據傳輸:一旦連接建立,雙方就可以開始傳輸數據。TCP負責將數據分割成合適大小的數據段,并為每個數據段添加頭部信息,包括序列號以便接收方重組數據。
四次揮手:當一方完成數據發送后,它會發送FIN(結束標志)給另一方表示希望關閉連接。接收方會回應ACK確認收到FIN,并在準備好關閉連接時也發送自己的FIN。發送方再回應ACK,最終完成連接的終止。
綜上所述,TCP因其高可靠性和安全性,廣泛應用于那些對數據完整性和順序有嚴格要求的應用程序中。
2.3 UDP
UDP(用戶數據報協議)是TCP/IP模型傳輸層的一個重要成員,它提供了一種無需建立連接即可發送和接收數據包的通信方式。與TCP不同的是,UDP不保證數據的順序性和可靠性,也不進行流量控制或擁塞控制。這意味著使用UDP時,數據可能會丟失、重復或者亂序到達。
UDP的主要特點:
無連接:在發送數據之前不需要建立連接,因此減少了建立連接所需的時間。
盡力而為的服務:UDP不會對數據包的傳輸做任何保證,也不會重傳丟失的數據包。
低開銷:由于缺少確認機制和其他復雜的功能,UDP具有較低的頭部開銷,這使得它非常適合實時應用。
高效性:因為沒有復雜的握手過程,所以UDP可以更快地發送數據。
適用于廣播或多播:UDP支持向多個目的地同時發送數據的能力,這對于視頻會議等應用場景非常有用。
UDP適用場景:
實時音頻/視頻流媒體服務
在線游戲
DNS查詢
SNMP(簡單網絡管理協議)
其他對延遲敏感的應用程序
2.4 TCP vs UDP
特性 | TCP (Transmission Control Protocol) | UDP (User Datagram Protocol) |
---|---|---|
連接類型 | 面向連接,需要三次握手建立連接 | 無連接,即發即用 |
可靠性 | 可靠的數據傳輸,確保數據按序完整到達 | 不可靠,不保證數據包會到達,可能丟失或亂序 |
速度 | 較慢,因為有握手過程和錯誤檢查 | 更快,因為它沒有這些額外的過程 |
流量控制 | 支持,通過滑動窗口機制 | 不支持 |
擁塞控制 | 支持 | 不支持 |
頭部開銷 | 較大,因為包含更多的控制信息 | 較小,只有必要的控制信息 |
用途 | 適合需要高可靠性的應用程序,如文件傳輸、電子郵件 | 適合對時間敏感但可容忍一定數據損失的應用,如視頻流 |
2.5 Socket
Socket(套接字)是網絡編程中的一個重要概念,它提供了一種跨進程通信的方式,使得不同計算機上的應用程序能夠通過網絡交換數據。在實現TCP/IP服務時,Socket扮演著至關重要的角色,它是程序員用來編寫客戶端和服務器端程序的接口。
Socket的基本特性:
地址家族:定義了通信協議類型,例如IPv4(AF_INET)或IPv6(AF_INET6)。
類型:指定了傳輸層使用的協議,如TCP(SOCK_STREAM,流式套接字)或UDP(SOCK_DGRAM,數據報套接字)。
協議:通常設置為0,表示使用默認協議;對于某些特殊情況可以指定特定協議編號。
使用Socket實現TCP/IP服務的步驟:
創建Socket:在服務器端和客戶端都需要調用socket()函數來創建一個套接字對象,該對象將用于發送和接收數據。
綁定(僅限服務器端):服務器需要調用bind()函數將其套接字與特定的IP地址和端口號關聯起來,以便其他客戶端可以通過這些信息找到并連接到服務器。
監聽(僅限服務器端):服務器調用listen()函數開始監聽來自客戶端的連接請求。這一步驟會將套接字轉換為被動模式,準備接受連接。
接受連接(僅限服務器端):當有客戶端嘗試連接時,服務器調用accept()函數來接受這個連接,并返回一個新的套接字,專門用于與那個特定客戶端之間的通信。
連接服務器(客戶端操作):客戶端調用connect()函數發起對服務器的連接請求,指定要連接的服務器IP地址和端口號。
發送/接收數據:一旦建立了連接,雙方都可以使用send()和recv()函數來進行數據的發送和接收。對于TCP來說,這意味著可以進行可靠的數據流傳輸。
關閉連接:數據交換完成后,雙方應該調用close()函數來關閉各自的套接字,釋放資源。
以上代碼展示了如何使用Python內置的socket庫來實現一個簡單的TCP回顯服務器和客戶端。
3 邊緣端設備實現
我此次使用的設備是凌智視覺模塊,但是該設備是沒有帶WiFi的,如果需要使用WiFi,需要外界一塊WiFi模塊。
3.1 服務端
下面我將搭建一個簡易的服務器,用于通過TCP連接向客戶端發送視頻幀。它使用OpenCV庫捕獲圖像,并將圖像編碼為JPEG格式后通過網絡傳輸給客戶端。此外,它還支持命令控制和視頻流模式,允許客戶端發送命令來控制視頻幀的傳輸。
以下是代碼的主要功能模塊和邏輯:
導入必要的庫:
cv2:來自lockzhiner_vision_module的OpenCV庫,用于視頻捕獲和圖像處理。
Thread, Event:來自threading模塊,用于創建線程和事件對象以同步線程間的操作。
socket:用于網絡通信。
定義輔助函數:
send_image(conn, frame):負責將一幀圖像編碼成JPEG格式并通過TCP連接發送出去。
定義線程處理函數:
handle_client_send(conn, start_event, shutdown_event, confirm_event, streaming_event, cap):此函數在一個獨立線程中運行,負責讀取攝像頭圖像并根據事件狀態決定是否發送圖像。它還會計算和打印幀率。
handle_client_receive(conn, addr, cap, start_event, shutdown_event, confirm_event, streaming_event):此函數也在一個獨立線程中運行,負責接收來自客戶端的命令并對這些命令作出響應,比如開始發送單個圖像、確認接收到圖像、關閉連接或切換視頻流模式等。
主程序邏輯:
設置了服務器的IP地址(HOST)和端口(PORT),并初始化了OpenCV的視頻捕獲對象(cap)。
創建了一個TCP/IP套接字,綁定了指定的主機和端口,并開始監聽傳入的連接請求。
當有新的客戶端連接時,會啟動兩個新線程:一個用于處理發送到客戶端的數據,另一個用于接收來自客戶端的命令。
主循環等待客戶端連接,并在接收到中斷信號(如KeyboardInterrupt)時優雅地關閉所有資源。
事件機制:
使用了多個Event對象來協調不同線程之間的交互,確保了正確的順序和狀態管理。
視頻流模式:
支持一種持續發送視頻幀的“視頻流模式”,這可以通過發送特定命令開啟或關閉。
import lockzhiner_vision_module.cv2as cv2fromthreading import Thread, Eventimport socketimport osimporttimedef send_image(conn, frame): try:
# 使用imencode將圖像編碼為JPEG格式
ret, img_encode = cv2.imencode('.jpg', frame)
if not ret:
print("Failed to encode image")
return # 假設img_encode已經是bytes類型,直接使用
data = img_encode # 發送圖像大小和圖像數據
conn.sendall(len(data).to_bytes(4, byteorder='big'))
conn.sendall(data) print("Image sent.") except (ConnectionResetError, OSError) as e:
print(f"Error sending image: {e}")defhandle_client_send(conn, start_event, shutdown_event, confirm_event, streaming_event, cap): frame_counter =0 start_time = time.time() while not shutdown_event.is_set():
if start_event.is_set() or streaming_event.is_set(): # 檢查開始或流式事件是否被設置 ret, frame = cap.read()
if ret: send_image(conn, frame) frame_counter +=1 if not streaming_event.is_set(): # 如果不是流式傳輸,則等待客戶端確認
confirm_event.wait() # 等待客戶端確認
confirm_event.clear() # 清除確認事件
start_event.clear() # 圖像發送完成并且已確認后清除事件
# 計算幀率 elapsed_time = time.time() - start_time if elapsed_time >=1: # 每一秒打印一次幀率 fps = frame_counter / elapsed_time
print(f"FPS: {fps}") # 重置計數器和時間戳
frame_counter =0
start_time = time.time()# 接收線程處理函數,接收命令并根據命令執行操作defhandle_client_receive(conn, addr, cap, start_event, shutdown_event, confirm_event, streaming_event): print(f"Connected by {addr}") try: while not shutdown_event.is_set(): try:
data = conn.recv(1024) except ConnectionResetError: print("Connection reset by peer.")
break if not data or shutdown_event.is_set(): print("Connection closed.") break command = data.decode().strip() print(f"Received command: {command}") if command =='0': ret, frame = cap.read()
if ret:
if streaming_event.is_set(): # 如果處于流式傳輸模式,則不需要設置start_event
pass else: start_event.set() # 設置開始事件以通知發送線程 elif command =='1': confirm_event.set() # 設置確認事件 elif command =='2': shutdown_event.set()
break elif command =='3': # 開始視頻流模式 streaming_event.set() print("Video stream mode started.") elif command =='q': # 停止視頻流模式 streaming_event.clear() print("Video stream mode stopped.") else: conn.sendall("Unknown command".encode()) finally: conn.close()if __name__ =="__main__": HOST ='172.32.0.144' PORT =6810 cap = cv2.VideoCapture() # if not cap.isOpened(): if cap.open(0) is False: print("Failed to open capture")
exit(1) print("video is all ready") start_event =Event() shutdown_event =Event() confirm_event =Event() streaming_event =Event() # 新增流式傳輸事件 server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.bind((HOST, PORT)) server_socket.listen() print("Server started, waiting for connections...") try:
while not shutdown_event.is_set():
conn, addr = server_socket.accept()
client_send_thread =Thread(target=handle_client_send,
args=(conn, start_event, shutdown_event, confirm_event, streaming_event, cap))
client_receive_thread =Thread(target=handle_client_receive,
args=(conn, addr, cap, start_event, shutdown_event, confirm_event, streaming_event))
client_send_thread.start() client_receive_thread.start() except KeyboardInterrupt: print("Server interrupted.") finally:
cap.release() server_socket.close()
print("Server stopped and resources released.")
3.2 客戶端
客戶端通過TCP連接接收圖像幀,并根據命令執行不同操作的功能。以下是代碼的詳細解釋:
客戶端功能概述
接收圖像:
receive_image(conn, timeout=5)函數用于從服務器接收圖像數據。
它首先接收表示圖像大小的4個字節,然后循環接收直到收到完整的圖像數據。
接收到的數據被轉換為NumPy數組并使用OpenCV解碼成圖像格式返回。
主程序邏輯 (main函數):
'0'請求單張圖像。
'1'不做任何操作(假設用作確認信號)。
'2'發送退出命令給服務器并終止程序。
'3'啟動視頻流模式,允許連續接收圖像幀,并將這些幀顯示出來,同時保存為本地視頻文件。
設置了服務器的IP地址(HOST)和端口(PORT),與服務端保持一致。
創建了一個TCP/IP套接字并嘗試連接到服務器。
根據用戶輸入的不同命令來控制視頻幀的請求和處理:
視頻流模式:
當用戶選擇啟動視頻流模式時,客戶端會創建一個以當前時間戳命名的視頻文件。
它會持續請求圖像幀并將接收到的每一幀添加到視頻文件中,同時在窗口中顯示。
用戶可以按q鍵停止視頻流模式,并保存錄制的視頻文件。
異常處理:
在關鍵位置添加了異常處理邏輯,以確保在發生錯誤時能夠適當響應,例如網絡連接超時或失敗時。
資源管理:
使用with語句確保即使出現異常,套接字也會被正確關閉。
確保視頻寫入器在異常情況下也能夠釋放資源。
注意事項
命令同步:客戶端發送命令后,通常需要等待服務器的響應。這里的實現假設服務器會在接收到命令后立即采取行動,因此客戶端緊接著就會嘗試接收圖像數據。
超時設置:對于圖像接收設置了超時參數,以防止程序在無響應的情況下卡住。
視頻編碼格式:選擇了XVID作為視頻編碼格式,這是一個常見的選擇,但并不是唯一可用的選項。可以根據需要更改。
視頻保存:視頻流模式下,視頻會被保存到本地磁盤。每次開始新的視頻流都會創建一個新的文件名,以避免覆蓋舊文件。
代碼
importsocketimportcv2importnumpyasnpimporttimedef receive_image(conn, timeout=5): conn.settimeout(timeout) # 設置接收超時 try: data_length = int.from_bytes(conn.recv(4), byteorder='big') image_data = b'' received =0 whilereceived < data_length:? ? ? ? ? ? packet = conn.recv(min(1024, data_length - received))? ? ? ? ? ??
ifnot packet:
break image_data += packet received += len(packet) # 將接收到的數據轉換為NumPy數組,并使用OpenCV解碼為圖像
image_np = np.frombuffer(image_data, dtype=np.uint8) frame = cv2.imdecode(image_np, cv2.IMREAD_COLOR) returnframe except socket.timeout: print("Timeout waiting for image from server.") returnNone except Exceptionase: print(f"Error receiving image: {e}") returnNonedef main(): HOST ='172.32.0.144' PORT =6810# 應與服務端使用的端口號相同 with socket.socket(socket.AF_INET, socket.SOCK_STREAM)ass: try:
s.connect((HOST, PORT)) video_writer = None # 初始化視頻寫入器為None whileTrue:
command = input("Enter command (0 to request image, 1 to do nothing, 2 to exit, 3 for video stream): ") s.sendall(command.encode()) ifcommand =='2':
s.sendall(b'2') # 發送退出命令給服務器
break elif command =='3':
print("Starting video stream mode. Press 'q' to stop.")
timestamp = int(time.time() *1000) # 毫秒級時間戳
video_filename = f"video_stream_{timestamp}.avi"
fourcc = cv2.VideoWriter_fourcc(*'XVID') # 視頻編碼格式
video_writer = None # 重置視頻寫入器
# 請求第一幀以獲取尺寸信息
s.sendall(b'0') # 請求圖像幀
frame = receive_image(s, timeout=5) # 增加超時參數 ifframeisNone:
print("Failed to receive first image frame.")
continue
height, width, _ = frame.shape
video_writer = cv2.VideoWriter(video_filename, fourcc,20.0, (width, height)) # 創建視頻寫入器 whileTrue:
ifvideo_writerisNone:
break # 如果視頻寫入器未初始化,則跳出循環
frame = receive_image(s, timeout=1) # 對每一幀都設置較短的超時時間 ifframeisNone: print("No new frame received. Waiting...")
time.sleep(1) # 等待一段時間再嘗試重新請求
continue
cv2.imshow('Video Stream', frame)
video_writer.write(frame) # 將幀寫入視頻文件
key = cv2.waitKey(1) &0xFF
ifkey == ord('q'):
print("Stopping video stream and saving video.")
s.sendall(b'q') # 發送停止信號給服務器 break ifvideo_writerisnot None:
video_writer.release() # 關閉視頻寫入器
print(f"Video saved as {video_filename}")
cv2.destroyAllWindows() elif command =='0':
frame = receive_image(s, timeout=5) # 增加超時參數
ifframeisnot None: cv2.imshow('Received Frame', frame)
cv2.waitKey(3000) # 顯示圖像3秒后自動關閉窗口
cv2.destroyAllWindows() else:
print("No image received.") s.sendall(b'1') # 假設'1'是確認信號 except Exceptionase: print(f"Socket error occurred: {e}") ifvideo_writerisnot None: video_writer.release() # 確保異常情況下也釋放視頻寫入器資源if__name__ =="__main__": main()
3.3 執行結果
-
物聯網
+關注
關注
2909文章
44736瀏覽量
374469 -
局域網
+關注
關注
5文章
757瀏覽量
46312 -
AI
+關注
關注
87文章
31097瀏覽量
269429
發布評論請先 登錄
相關推薦
評論