netprotomvds 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- netprotomvds/__init__.py +11 -0
- netprotomvds/client.py +76 -0
- netprotomvds/packet.py +56 -0
- netprotomvds/server.py +46 -0
- netprotomvds/test_stream.py +35 -0
- netprotomvds/version.py +1 -0
- netprotomvds-0.1.0.dist-info/METADATA +38 -0
- netprotomvds-0.1.0.dist-info/RECORD +10 -0
- netprotomvds-0.1.0.dist-info/WHEEL +5 -0
- netprotomvds-0.1.0.dist-info/top_level.txt +1 -0
netprotomvds/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""netprotomvds — быстрая передача видео по UDP."""
|
|
2
|
+
from .version import __version__
|
|
3
|
+
from .packet import split_frame, reassemble_frame
|
|
4
|
+
from .server import VideoServer
|
|
5
|
+
from .client import VideoClient
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"__version__",
|
|
9
|
+
"split_frame", "reassemble_frame",
|
|
10
|
+
"VideoServer", "VideoClient",
|
|
11
|
+
]
|
netprotomvds/client.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Клиентская часть (Контроллер). Принимает кадры."""
|
|
2
|
+
import socket
|
|
3
|
+
import struct
|
|
4
|
+
import threading
|
|
5
|
+
import queue
|
|
6
|
+
import time
|
|
7
|
+
from .packet import reassemble_frame, HEADER_FORMAT, HEADER_SIZE
|
|
8
|
+
|
|
9
|
+
class VideoClient:
|
|
10
|
+
def __init__(self, server_ip, server_port=8081, buffer_size=3):
|
|
11
|
+
self.server_addr = (server_ip, server_port)
|
|
12
|
+
self.buffer_size = buffer_size
|
|
13
|
+
|
|
14
|
+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
15
|
+
self.sock.bind(("0.0.0.0", 0)) # Слушаем на случайном порту
|
|
16
|
+
|
|
17
|
+
self._running = False
|
|
18
|
+
self._thread = None
|
|
19
|
+
self._recv_queue = queue.Queue()
|
|
20
|
+
self._frame_buffer = []
|
|
21
|
+
self._lock = threading.Lock()
|
|
22
|
+
|
|
23
|
+
def start(self):
|
|
24
|
+
self._running = True
|
|
25
|
+
self._thread = threading.Thread(target=self._listen_loop, daemon=True)
|
|
26
|
+
self._thread.start()
|
|
27
|
+
print(f"[VideoClient] Слушаю {self.server_addr}...")
|
|
28
|
+
|
|
29
|
+
def stop(self):
|
|
30
|
+
self._running = False
|
|
31
|
+
if self._thread:
|
|
32
|
+
self._thread.join(timeout=1.0)
|
|
33
|
+
self.sock.close()
|
|
34
|
+
|
|
35
|
+
def _listen_loop(self):
|
|
36
|
+
current_frame_id = None
|
|
37
|
+
current_packets = []
|
|
38
|
+
expected_chunks = 0
|
|
39
|
+
|
|
40
|
+
while self._running:
|
|
41
|
+
try:
|
|
42
|
+
data, addr = self.sock.recvfrom(2048)
|
|
43
|
+
if len(data) < HEADER_SIZE:
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
frame_id, total_chunks, chunk_idx = struct.unpack(HEADER_FORMAT, data[:HEADER_SIZE])
|
|
47
|
+
|
|
48
|
+
# Если пришел новый кадр — сбрасываем старый (потеряли кусок)
|
|
49
|
+
if current_frame_id != frame_id:
|
|
50
|
+
current_frame_id = frame_id
|
|
51
|
+
current_packets = [None] * total_chunks
|
|
52
|
+
expected_chunks = total_chunks
|
|
53
|
+
|
|
54
|
+
current_packets[chunk_idx] = data
|
|
55
|
+
|
|
56
|
+
# Если собрали все чанки
|
|
57
|
+
if all(p is not None for p in current_packets):
|
|
58
|
+
frame_data = reassemble_frame(current_packets)
|
|
59
|
+
|
|
60
|
+
with self._lock:
|
|
61
|
+
self._frame_buffer.append(frame_data)
|
|
62
|
+
if len(self._frame_buffer) > self.buffer_size:
|
|
63
|
+
self._frame_buffer.pop(0) # Удаляем старый (Jitter Buffer)
|
|
64
|
+
|
|
65
|
+
current_frame_id = None
|
|
66
|
+
current_packets = []
|
|
67
|
+
except Exception as e:
|
|
68
|
+
if self._running:
|
|
69
|
+
print(f"[VideoClient] Ошибка приема: {e}")
|
|
70
|
+
|
|
71
|
+
def get_latest_frame(self):
|
|
72
|
+
"""Возвращает самый свежий собранный кадр (bytes)."""
|
|
73
|
+
with self._lock:
|
|
74
|
+
if self._frame_buffer:
|
|
75
|
+
return self._frame_buffer[-1]
|
|
76
|
+
return None
|
netprotomvds/packet.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Нарезка и сборка пакетов для UDP-видеопотока."""
|
|
2
|
+
import struct
|
|
3
|
+
import zlib
|
|
4
|
+
|
|
5
|
+
HEADER_FORMAT = "<IHH" # packet_id (4), total_chunks (2), chunk_index (2)
|
|
6
|
+
HEADER_SIZE = struct.calcsize(HEADER_FORMAT)
|
|
7
|
+
MAX_PAYLOAD_SIZE = 1400 # Безопасный размер для прохождения через NAT
|
|
8
|
+
|
|
9
|
+
def compress_data(data: bytes) -> bytes:
|
|
10
|
+
"""Сжимает данные, чтобы уменьшить трафик."""
|
|
11
|
+
return zlib.compress(data, level=1) # level=1 самый быстрый
|
|
12
|
+
|
|
13
|
+
def decompress_data(data: bytes) -> bytes:
|
|
14
|
+
"""Распаковывает данные."""
|
|
15
|
+
return zlib.decompress(data)
|
|
16
|
+
|
|
17
|
+
def split_frame(frame_id: int, data: bytes, compressed: bool = True) -> list:
|
|
18
|
+
"""Нарезает кадр на список пакетов."""
|
|
19
|
+
if compressed:
|
|
20
|
+
payload = compress_data(data)
|
|
21
|
+
else:
|
|
22
|
+
payload = data
|
|
23
|
+
|
|
24
|
+
total_size = len(payload)
|
|
25
|
+
total_chunks = (total_size + MAX_PAYLOAD_SIZE - 1) // MAX_PAYLOAD_SIZE
|
|
26
|
+
if total_chunks > 65535:
|
|
27
|
+
raise ValueError("Кадр слишком большой (больше 65535 чанков)")
|
|
28
|
+
|
|
29
|
+
packets = []
|
|
30
|
+
for i in range(total_chunks):
|
|
31
|
+
start = i * MAX_PAYLOAD_SIZE
|
|
32
|
+
end = start + MAX_PAYLOAD_SIZE
|
|
33
|
+
chunk = payload[start:end]
|
|
34
|
+
|
|
35
|
+
# Header: frame_id, total_chunks, chunk_index
|
|
36
|
+
header = struct.pack(HEADER_FORMAT, frame_id, total_chunks, i)
|
|
37
|
+
packets.append(header + chunk)
|
|
38
|
+
|
|
39
|
+
return packets
|
|
40
|
+
|
|
41
|
+
def reassemble_frame(packets: list) -> bytes:
|
|
42
|
+
"""Собирает кадр из списка пакетов.
|
|
43
|
+
Ожидает, что список уже отсортирован по chunk_index и полный.
|
|
44
|
+
"""
|
|
45
|
+
if not packets:
|
|
46
|
+
return b""
|
|
47
|
+
|
|
48
|
+
# Извлекаем заголовок из первого пакета, чтобы узнать, сжат ли он
|
|
49
|
+
header = packets[0][:HEADER_SIZE]
|
|
50
|
+
frame_id, total_chunks, _ = struct.unpack(HEADER_FORMAT, header)
|
|
51
|
+
|
|
52
|
+
payload = bytearray()
|
|
53
|
+
for pkt in packets:
|
|
54
|
+
payload.extend(pkt[HEADER_SIZE:])
|
|
55
|
+
|
|
56
|
+
return decompress_data(bytes(payload))
|
netprotomvds/server.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Серверная часть (Агент). Отправляет кадры."""
|
|
2
|
+
import socket
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from .packet import split_frame
|
|
6
|
+
|
|
7
|
+
class VideoServer:
|
|
8
|
+
def __init__(self, host="0.0.0.0", port=8081):
|
|
9
|
+
self.host = host
|
|
10
|
+
self.port = port
|
|
11
|
+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
12
|
+
self.sock.bind((self.host, self.port))
|
|
13
|
+
|
|
14
|
+
self.target_addr = None
|
|
15
|
+
self._running = False
|
|
16
|
+
self._frame_id = 0
|
|
17
|
+
self._lock = threading.Lock()
|
|
18
|
+
|
|
19
|
+
def set_target(self, ip, port):
|
|
20
|
+
"""Устанавливает адрес клиента (Controller)."""
|
|
21
|
+
self.target_addr = (ip, port)
|
|
22
|
+
|
|
23
|
+
def start(self):
|
|
24
|
+
self._running = True
|
|
25
|
+
print(f"[VideoServer] UDP-сервер запущен на {self.host}:{self.port}")
|
|
26
|
+
|
|
27
|
+
def stop(self):
|
|
28
|
+
self._running = False
|
|
29
|
+
self.sock.close()
|
|
30
|
+
print("[VideoServer] Остановлен.")
|
|
31
|
+
|
|
32
|
+
def send_frame(self, image_bytes):
|
|
33
|
+
"""Нарезает и отправляет один кадр."""
|
|
34
|
+
if not self.target_addr or not self._running:
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
with self._lock:
|
|
38
|
+
self._frame_id = (self._frame_id + 1) & 0xFFFFFFFF
|
|
39
|
+
|
|
40
|
+
packets = split_frame(self._frame_id, image_bytes, compressed=True)
|
|
41
|
+
for pkt in packets:
|
|
42
|
+
try:
|
|
43
|
+
self.sock.sendto(pkt, self.target_addr)
|
|
44
|
+
except Exception as e:
|
|
45
|
+
print(f"[VideoServer] Ошибка отправки: {e}")
|
|
46
|
+
break
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import threading
|
|
3
|
+
from netprotomvds import VideoServer, VideoClient
|
|
4
|
+
|
|
5
|
+
# 1. Настраиваем сервер
|
|
6
|
+
server = VideoServer(host="127.0.0.1", port=8081)
|
|
7
|
+
server.start()
|
|
8
|
+
|
|
9
|
+
# 2. Настраиваем клиент
|
|
10
|
+
client = VideoClient(server_ip="127.0.0.1", server_port=8081)
|
|
11
|
+
client.start()
|
|
12
|
+
|
|
13
|
+
# Говорим серверу, куда слать видео (в тесте шлем сами себе)
|
|
14
|
+
time.sleep(0.5) # Ждем старта
|
|
15
|
+
# Узнаем порт, на который клиент сел
|
|
16
|
+
client_port = client.sock.getsockname()[1]
|
|
17
|
+
server.set_target("127.0.0.1", client_port)
|
|
18
|
+
|
|
19
|
+
print("Начинаем передачу...")
|
|
20
|
+
for i in range(5):
|
|
21
|
+
test_data = f"Это тестовый кадр номер {i}".encode("utf-8") * 100 # ~3 КБ
|
|
22
|
+
server.send_frame(test_data)
|
|
23
|
+
time.sleep(0.2)
|
|
24
|
+
|
|
25
|
+
time.sleep(0.5) # Ждем, пока долетит
|
|
26
|
+
frame = client.get_latest_frame()
|
|
27
|
+
|
|
28
|
+
if frame:
|
|
29
|
+
print(f"Успех! Получен кадр размером {len(frame)} байт.")
|
|
30
|
+
print(f"Начало данных: {frame[:50].decode('utf-8')}...")
|
|
31
|
+
else:
|
|
32
|
+
print("Ошибка: кадр не получен.")
|
|
33
|
+
|
|
34
|
+
server.stop()
|
|
35
|
+
client.stop()
|
netprotomvds/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: netprotomvds
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Network Protocol Multithreaded Video Data Share: быстрая передача видео по UDP.
|
|
5
|
+
Author-email: CharmLxt <reception.mailspypi@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/CharmLxt/netprotomvds
|
|
8
|
+
Project-URL: Source, https://github.com/CharmLxt/netprotomvds
|
|
9
|
+
Project-URL: Tracker, https://github.com/CharmLxt/netprotomvds/issues
|
|
10
|
+
Keywords: udp,video,streaming,networking,multithreading
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Multimedia :: Video
|
|
15
|
+
Classifier: Topic :: System :: Networking
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: numpy>=1.21.0
|
|
19
|
+
Requires-Dist: 9was9>=0.2.0
|
|
20
|
+
|
|
21
|
+
netprotomvds. Версия 0.1.0. Автор CharmLxt (PyPI: CharmLxt, email: reception.mailspypi@gmail.com). Ссылка на GitHub: https://github.com/CharmLxt/netprotomvds.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
netprotomvds (Network Protocol Multithreaded Video Data Share) — это специализированная библиотека для сверхбыстрой передачи видеопотока или изображений по сети с использованием протокола UDP. Она создавалась специально для проекта Monogrammer, но может использоваться в любых задачах, где нужна трансляция экрана или видеозвонки (например, удаленный рабочий стол, стриминг игр или камеры).
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
В основе библиотеки лежит идея, что для видео надежность TCP не так важна, как скорость. Если один пакет с частью картинки потерялся в сети, мы просто отбрасываем весь этот кадр и показываем следующий. Из-за этого нет задержек на повторную отправку (как в TCP), и картинка движется максимально плавно. Библиотека автоматически нарезает большие кадры на маленькие пакеты, подходящие под размер MTU, отправляет их и собирает обратно на другом конце.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
В версии 0.1.0 реализован базовый каркас. Модуль packet отвечает за нарезку и склейку данных с порядковыми номерами. VideoServer и VideoClient используют многопоточность (модуль threading) для фоновой отправки и приема, чтобы не блокировать основной интерфейс программы. Для сжатия данных используется стандартный zlib (чтобы не тащить тяжелые зависимости, но в будущем мы добавим JPEG через C#-мост Sowanna). Также встроен Jitter Buffer, который сглаживает неровности сети, храня пару последних кадров.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
Установка: pip install netprotomvds. Зависимости: numpy и 9was9 (для быстрой математики, если понадобится обработка изображений). Библиотека кроссплатформенная, работает везде, где есть сокеты и Python 3.8+. Связь с автором: reception.mailspypi@gmail.com.
|
|
38
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
netprotomvds/__init__.py,sha256=pT_B_LkDm8qT9Kh3_MdgFI8c8ZLE8UO7m20C9Wttz3s,339
|
|
2
|
+
netprotomvds/client.py,sha256=yGXcfZrGc5SmDeekjBGNFIvv5TckOFVrueg1MCtM43I,3009
|
|
3
|
+
netprotomvds/packet.py,sha256=gjRF4_gs2DwQ1u8zXePQnR9n5wxLJxM7ef7TRaf4f30,2208
|
|
4
|
+
netprotomvds/server.py,sha256=uh42XWT3Ge_BuqcUDzJQB68Z_1u-d9PFDmT3sqroRGI,1603
|
|
5
|
+
netprotomvds/test_stream.py,sha256=rBY_wy9y0e4xA9rfic8LhLuDybrOwuJODfWERwpL9Mw,1209
|
|
6
|
+
netprotomvds/version.py,sha256=Pru0BlFBASFCFo7McHdohtKkUtgMPDwbGfyUZlE2_Vw,21
|
|
7
|
+
netprotomvds-0.1.0.dist-info/METADATA,sha256=HC-aKJW5pumtOMFICNfdrtxpzfs5nx6aZotTdviwv7I,3785
|
|
8
|
+
netprotomvds-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
9
|
+
netprotomvds-0.1.0.dist-info/top_level.txt,sha256=v-A9bkucctQKxfVvLG4zuhogfnClkAHFPa5s4KkXcZc,13
|
|
10
|
+
netprotomvds-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
netprotomvds
|