numpy.ndarray thô qua Socket?Một khung hình độ phân giải $1280 \times 720$ hệ màu BGR có dung lượng thô:
Nếu muốn truyền với tốc độ $30 \text{ FPS}$:
Băng thông này quá lớn, mạng Wifi thông thường sẽ bị nghẽn (lag, giật) ngay lập tức!
cv2.imencodeTrước khi đưa vào luồng Socket, ta nén ma trận ảnh thô thành một chuỗi byte định dạng JPEG (tương tự như lưu file ảnh .jpg nhưng nằm hoàn toàn trên RAM):
# Nén frame thành chuỗi byte JPEG với chất lượng 80%
_, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 80])
data = buffer.tobytes()
Dung lượng một khung hình sau khi nén chỉ còn khoảng $30 - 60 \text{ KB}$ (giảm hơn $50$ lần), giúp video truyền qua Wifi mượt mà không độ trễ.
Giao thức TCP truyền dữ liệu dưới dạng dòng byte liên tục (Stream), không có khái niệm "ranh giới bức ảnh".
Nếu Client gửi 2 bức ảnh liên tiếp, Server có thể đọc dính nửa bức ảnh trước với nửa bức ảnh sau, gây lỗi giải mã ảnh.
Quy ước đóng gói chuẩn (Protocol): Mỗi bức ảnh gửi đi sẽ được đóng gói theo cấu trúc:
[ 8 bytes: Độ dài bức ảnh (Kích thước N) ] + [ N bytes: Dữ liệu ảnh JPEG ]
Phía Server luôn đọc đúng 8 bytes đầu tiên để biết kích thước bức ảnh sắp tới là bao nhiêu, sau đó đọc đúng đủ $N$ bytes rồi mới giải mã bằng cv2.imdecode().
Hai bố con có thể chạy 2 file này trên cùng một máy tính (dùng địa chỉ 127.0.0.1) để kiểm tra trước, hoặc chạy trên 2 máy tính khác nhau trong cùng mạng Wifi (dùng địa chỉ IP LAN của máy Server, ví dụ: 192.168.1.x).
video_server.py (Bên nhận và hiển thị video)Server sẽ mở cổng lắng nghe kết nối từ Client.
import cv2
import socket
import struct
import numpy as np
class VideoStreamServer:
def __init__(self, host='0.0.0.0', port=8485):
self.host = host
self.port = port
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Cho phép tái sử dụng cổng ngay sau khi tắt server
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
def start(self):
self.server_socket.bind((self.host, self.port))
self.server_socket.listen(1)
print(f"📡 Server đang lắng nghe tại cổng {self.port}...")
print(" -> Đang chờ Client kết nối...")
conn, addr = self.server_socket.accept()
print(f" Đã kết nối với Client: {addr}")
data = b""
payload_size = struct.calcsize(">Q") # 8 bytes chứa kích thước ảnh (unsigned long long)
try:
while True:
# 1. Đọc đủ 8 bytes đầu tiên để lấy độ dài bức ảnh
while len(data) < payload_size:
packet = conn.recv(4096)
if not packet:
break
data += packet
if not data:
break
packed_msg_size = data[:payload_size]
data = data[payload_size:]
msg_size = struct.unpack(">Q", packed_msg_size)[0]
# 2. Đọc đủ msg_size bytes của bức ảnh JPEG
while len(data) < msg_size:
data += conn.recv(4096)
frame_data = data[:msg_size]
data = data[msg_size:]
# 3. Giải nén chuỗi byte thành ma trận ảnh NumPy
frame_arr = np.frombuffer(frame_data, dtype=np.uint8)
frame = cv2.imdecode(frame_arr, cv2.IMREAD_COLOR)
if frame is not None:
cv2.putText(frame, "Stream qua TCP Socket", (20, 40),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
cv2.imshow("Server: Receiver Screen", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
except ConnectionResetError:
print(" Client đã ngắt kết nối đột ngột.")
finally:
conn.close()
self.server_socket.close()
cv2.destroyAllWindows()
print(" Đã đóng Server an toàn.")
if __name__ == "__main__":
server = VideoStreamServer()
server.start()
video_client.py (Bên quay Camera và gửi dữ liệu)Client mở camera, nén từng khung hình thành JPEG và gửi tới Server.
import cv2
import socket
import struct
class VideoStreamClient:
def __init__(self, server_ip='127.0.0.1', port=8485, camera_index=0):
self.server_ip = server_ip
self.port = port
self.camera_index = camera_index
self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.cap = cv2.VideoCapture(self.camera_index)
# Hạ độ phân giải về 640x480 để đường truyền nhẹ nhàng
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
def start(self):
if not self.cap.isOpened():
print("❌ Không mở được camera.")
return
print(f" Đang kết nối tới Server {self.server_ip}:{self.port}...")
try:
self.client_socket.connect((self.server_ip, self.port))
print(" Kết nối thành công! Đang stream video...")
except Exception as e:
print(f"❌ Kết nối thất bại: {e}")
return
try:
while True:
ret, frame = self.cap.read()
if not ret:
break
# Lật gương cho tự nhiên
frame = cv2.flip(frame, 1)
# 1. Nén ảnh thành JPEG (chất lượng 75% để tối ưu mạng)
_, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 75])
byte_data = buffer.tobytes()
# 2. Đóng gói kích thước (8 bytes, '>Q') + dữ liệu ảnh
message = struct.pack(">Q", len(byte_data)) + byte_data
# 3. Gửi toàn bộ gói tin qua Socket
self.client_socket.sendall(message)
# Hiển thị màn hình xem trước ở Client
cv2.imshow("Client: Sender Screen (Preview)", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
except (ConnectionResetError, BrokenPipeError):
print("❌ Mất kết nối với Server.")
finally:
self.cap.release()
self.client_socket.close()
cv2.destroyAllWindows()
print(" Đã tắt Client.")
if __name__ == "__main__":
# Đổi '127.0.0.1' thành địa chỉ IP thật của máy Server nếu chạy trên 2 máy tính khác nhau
client = VideoStreamClient(server_ip='127.0.0.1', port=8485)
client.start()
Mở Terminal 1: Chạy python video_server.py trước (Server sẽ chuyển sang trạng thái chờ).
Mở Terminal 2: Chạy python video_client.py.
Hai cửa sổ sẽ xuất hiện đồng thời: một bên quay camera gửi đi, một bên nhận dữ liệu qua mạng và giải mã mượt mà.
Thử thách 1 (Khám phá mô đun struct):
Trong dòng lệnh struct.pack(">Q", len(byte_data)): Ký tự > và chữ Q nghĩa là gì trong lập trình nhị phân (Big-endian và Unsigned Long Long 64-bit)?
Thử thách 2 (Tối ưu băng thông nâng cao):
Nếu đường truyền mạng bị chậm, ta nên ưu tiên giảm thông số nào trước: FPS, Độ phân giải (Resolution) hay Chất lượng nén JPEG (Quality)?