cv-frames 0.0.1__tar.gz → 0.0.2__tar.gz
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.
- {cv_frames-0.0.1 → cv_frames-0.0.2}/.github/workflows/tests.yml +6 -0
- {cv_frames-0.0.1 → cv_frames-0.0.2}/.gitignore +1 -0
- cv_frames-0.0.2/PKG-INFO +74 -0
- cv_frames-0.0.2/README.rst +63 -0
- cv_frames-0.0.2/cv_frames.egg-info/PKG-INFO +74 -0
- {cv_frames-0.0.1 → cv_frames-0.0.2}/cv_frames.egg-info/SOURCES.txt +5 -1
- {cv_frames-0.0.1 → cv_frames-0.0.2}/cv_frames.egg-info/requires.txt +3 -0
- {cv_frames-0.0.1 → cv_frames-0.0.2}/cvframes/cli.py +13 -0
- cv_frames-0.0.2/cvframes/web/__init__.py +0 -0
- cv_frames-0.0.2/cvframes/web/client.py +50 -0
- cv_frames-0.0.2/cvframes/web/server.py +184 -0
- cv_frames-0.0.2/cvframes/web/static/index.html +100 -0
- {cv_frames-0.0.1 → cv_frames-0.0.2}/pyproject.toml +11 -1
- cv_frames-0.0.1/PKG-INFO +0 -7
- cv_frames-0.0.1/README.md +0 -1
- cv_frames-0.0.1/cv_frames.egg-info/PKG-INFO +0 -7
- {cv_frames-0.0.1 → cv_frames-0.0.2}/.github/workflows/publish.yml +0 -0
- {cv_frames-0.0.1 → cv_frames-0.0.2}/.pre-commit-config.yaml +0 -0
- {cv_frames-0.0.1 → cv_frames-0.0.2}/cv_frames.egg-info/dependency_links.txt +0 -0
- {cv_frames-0.0.1 → cv_frames-0.0.2}/cv_frames.egg-info/entry_points.txt +0 -0
- {cv_frames-0.0.1 → cv_frames-0.0.2}/cv_frames.egg-info/top_level.txt +0 -0
- {cv_frames-0.0.1 → cv_frames-0.0.2}/cvframes/__init__.py +0 -0
- {cv_frames-0.0.1 → cv_frames-0.0.2}/cvframes/iterate.py +0 -0
- {cv_frames-0.0.1 → cv_frames-0.0.2}/requirements.txt +0 -0
- {cv_frames-0.0.1 → cv_frames-0.0.2}/setup.cfg +0 -0
- {cv_frames-0.0.1 → cv_frames-0.0.2}/tests/test_iterate.py +0 -0
cv_frames-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cv-frames
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Read frames from OpenCV like humans
|
|
5
|
+
Description-Content-Type: text/x-rst
|
|
6
|
+
Requires-Dist: opencv-python
|
|
7
|
+
Requires-Dist: numpy
|
|
8
|
+
Requires-Dist: click
|
|
9
|
+
Provides-Extra: web
|
|
10
|
+
Requires-Dist: aiohttp; extra == "web"
|
|
11
|
+
|
|
12
|
+
cv-frames
|
|
13
|
+
=========
|
|
14
|
+
|
|
15
|
+
.. image:: https://github.com/kqf/cv-frames/actions/workflows/tests.yml/badge.svg
|
|
16
|
+
:target: https://github.com/kqf/cv-frames/actions
|
|
17
|
+
:alt: Tests
|
|
18
|
+
|
|
19
|
+
.. image:: https://img.shields.io/pypi/dm/cv-frames.svg
|
|
20
|
+
:target: https://pypi.org/project/cv-frames/
|
|
21
|
+
:alt: PyPI Downloads
|
|
22
|
+
|
|
23
|
+
``cv-frames`` is a lightweight utility that simplifies working with video files frame-by-frame using OpenCV.
|
|
24
|
+
It includes both a Python API and a command-line interface (CLI) for interacting with video frames in an intuitive way.
|
|
25
|
+
|
|
26
|
+
----
|
|
27
|
+
|
|
28
|
+
Installation
|
|
29
|
+
------------
|
|
30
|
+
|
|
31
|
+
Install via pip:
|
|
32
|
+
|
|
33
|
+
.. code-block:: bash
|
|
34
|
+
|
|
35
|
+
pip install cv-frames
|
|
36
|
+
|
|
37
|
+
----
|
|
38
|
+
|
|
39
|
+
Command Line Usage
|
|
40
|
+
------------------
|
|
41
|
+
|
|
42
|
+
After installation, you can use the CLI:
|
|
43
|
+
|
|
44
|
+
.. code-block:: bash
|
|
45
|
+
|
|
46
|
+
cv-frames show path/to/video.mp4
|
|
47
|
+
|
|
48
|
+
Navigate through frames with any key. Press **q** to quit.
|
|
49
|
+
|
|
50
|
+
----
|
|
51
|
+
|
|
52
|
+
Python Usage
|
|
53
|
+
------------
|
|
54
|
+
|
|
55
|
+
Example: Read and show frames programmatically
|
|
56
|
+
|
|
57
|
+
.. code-block:: python
|
|
58
|
+
|
|
59
|
+
from pathlib import Path
|
|
60
|
+
from cvframes import iterate
|
|
61
|
+
|
|
62
|
+
for i, (_, frame) in enumerate(iterate(Path("video.mp4"))):
|
|
63
|
+
print(f"Frame {i} shape:", frame.shape)
|
|
64
|
+
|
|
65
|
+
Example: Split side-by-side video into left/right frames
|
|
66
|
+
|
|
67
|
+
.. code-block:: python
|
|
68
|
+
|
|
69
|
+
from cvframes import iterate_sbs
|
|
70
|
+
|
|
71
|
+
# Write the processed frames to disc
|
|
72
|
+
for capture, (left, right) in iterate_sbs("sbs_video.mp4", oname="processed.mp4"):
|
|
73
|
+
processed = do_something(left)
|
|
74
|
+
capture.write(processed)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
cv-frames
|
|
2
|
+
=========
|
|
3
|
+
|
|
4
|
+
.. image:: https://github.com/kqf/cv-frames/actions/workflows/tests.yml/badge.svg
|
|
5
|
+
:target: https://github.com/kqf/cv-frames/actions
|
|
6
|
+
:alt: Tests
|
|
7
|
+
|
|
8
|
+
.. image:: https://img.shields.io/pypi/dm/cv-frames.svg
|
|
9
|
+
:target: https://pypi.org/project/cv-frames/
|
|
10
|
+
:alt: PyPI Downloads
|
|
11
|
+
|
|
12
|
+
``cv-frames`` is a lightweight utility that simplifies working with video files frame-by-frame using OpenCV.
|
|
13
|
+
It includes both a Python API and a command-line interface (CLI) for interacting with video frames in an intuitive way.
|
|
14
|
+
|
|
15
|
+
----
|
|
16
|
+
|
|
17
|
+
Installation
|
|
18
|
+
------------
|
|
19
|
+
|
|
20
|
+
Install via pip:
|
|
21
|
+
|
|
22
|
+
.. code-block:: bash
|
|
23
|
+
|
|
24
|
+
pip install cv-frames
|
|
25
|
+
|
|
26
|
+
----
|
|
27
|
+
|
|
28
|
+
Command Line Usage
|
|
29
|
+
------------------
|
|
30
|
+
|
|
31
|
+
After installation, you can use the CLI:
|
|
32
|
+
|
|
33
|
+
.. code-block:: bash
|
|
34
|
+
|
|
35
|
+
cv-frames show path/to/video.mp4
|
|
36
|
+
|
|
37
|
+
Navigate through frames with any key. Press **q** to quit.
|
|
38
|
+
|
|
39
|
+
----
|
|
40
|
+
|
|
41
|
+
Python Usage
|
|
42
|
+
------------
|
|
43
|
+
|
|
44
|
+
Example: Read and show frames programmatically
|
|
45
|
+
|
|
46
|
+
.. code-block:: python
|
|
47
|
+
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
from cvframes import iterate
|
|
50
|
+
|
|
51
|
+
for i, (_, frame) in enumerate(iterate(Path("video.mp4"))):
|
|
52
|
+
print(f"Frame {i} shape:", frame.shape)
|
|
53
|
+
|
|
54
|
+
Example: Split side-by-side video into left/right frames
|
|
55
|
+
|
|
56
|
+
.. code-block:: python
|
|
57
|
+
|
|
58
|
+
from cvframes import iterate_sbs
|
|
59
|
+
|
|
60
|
+
# Write the processed frames to disc
|
|
61
|
+
for capture, (left, right) in iterate_sbs("sbs_video.mp4", oname="processed.mp4"):
|
|
62
|
+
processed = do_something(left)
|
|
63
|
+
capture.write(processed)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cv-frames
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Read frames from OpenCV like humans
|
|
5
|
+
Description-Content-Type: text/x-rst
|
|
6
|
+
Requires-Dist: opencv-python
|
|
7
|
+
Requires-Dist: numpy
|
|
8
|
+
Requires-Dist: click
|
|
9
|
+
Provides-Extra: web
|
|
10
|
+
Requires-Dist: aiohttp; extra == "web"
|
|
11
|
+
|
|
12
|
+
cv-frames
|
|
13
|
+
=========
|
|
14
|
+
|
|
15
|
+
.. image:: https://github.com/kqf/cv-frames/actions/workflows/tests.yml/badge.svg
|
|
16
|
+
:target: https://github.com/kqf/cv-frames/actions
|
|
17
|
+
:alt: Tests
|
|
18
|
+
|
|
19
|
+
.. image:: https://img.shields.io/pypi/dm/cv-frames.svg
|
|
20
|
+
:target: https://pypi.org/project/cv-frames/
|
|
21
|
+
:alt: PyPI Downloads
|
|
22
|
+
|
|
23
|
+
``cv-frames`` is a lightweight utility that simplifies working with video files frame-by-frame using OpenCV.
|
|
24
|
+
It includes both a Python API and a command-line interface (CLI) for interacting with video frames in an intuitive way.
|
|
25
|
+
|
|
26
|
+
----
|
|
27
|
+
|
|
28
|
+
Installation
|
|
29
|
+
------------
|
|
30
|
+
|
|
31
|
+
Install via pip:
|
|
32
|
+
|
|
33
|
+
.. code-block:: bash
|
|
34
|
+
|
|
35
|
+
pip install cv-frames
|
|
36
|
+
|
|
37
|
+
----
|
|
38
|
+
|
|
39
|
+
Command Line Usage
|
|
40
|
+
------------------
|
|
41
|
+
|
|
42
|
+
After installation, you can use the CLI:
|
|
43
|
+
|
|
44
|
+
.. code-block:: bash
|
|
45
|
+
|
|
46
|
+
cv-frames show path/to/video.mp4
|
|
47
|
+
|
|
48
|
+
Navigate through frames with any key. Press **q** to quit.
|
|
49
|
+
|
|
50
|
+
----
|
|
51
|
+
|
|
52
|
+
Python Usage
|
|
53
|
+
------------
|
|
54
|
+
|
|
55
|
+
Example: Read and show frames programmatically
|
|
56
|
+
|
|
57
|
+
.. code-block:: python
|
|
58
|
+
|
|
59
|
+
from pathlib import Path
|
|
60
|
+
from cvframes import iterate
|
|
61
|
+
|
|
62
|
+
for i, (_, frame) in enumerate(iterate(Path("video.mp4"))):
|
|
63
|
+
print(f"Frame {i} shape:", frame.shape)
|
|
64
|
+
|
|
65
|
+
Example: Split side-by-side video into left/right frames
|
|
66
|
+
|
|
67
|
+
.. code-block:: python
|
|
68
|
+
|
|
69
|
+
from cvframes import iterate_sbs
|
|
70
|
+
|
|
71
|
+
# Write the processed frames to disc
|
|
72
|
+
for capture, (left, right) in iterate_sbs("sbs_video.mp4", oname="processed.mp4"):
|
|
73
|
+
processed = do_something(left)
|
|
74
|
+
capture.write(processed)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
.gitignore
|
|
2
2
|
.pre-commit-config.yaml
|
|
3
|
-
README.
|
|
3
|
+
README.rst
|
|
4
4
|
pyproject.toml
|
|
5
5
|
requirements.txt
|
|
6
6
|
.github/workflows/publish.yml
|
|
@@ -14,4 +14,8 @@ cv_frames.egg-info/top_level.txt
|
|
|
14
14
|
cvframes/__init__.py
|
|
15
15
|
cvframes/cli.py
|
|
16
16
|
cvframes/iterate.py
|
|
17
|
+
cvframes/web/__init__.py
|
|
18
|
+
cvframes/web/client.py
|
|
19
|
+
cvframes/web/server.py
|
|
20
|
+
cvframes/web/static/index.html
|
|
17
21
|
tests/test_iterate.py
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
1
3
|
import click
|
|
2
4
|
import cv2
|
|
3
5
|
|
|
@@ -22,3 +24,14 @@ def show(filepath):
|
|
|
22
24
|
break
|
|
23
25
|
|
|
24
26
|
cv2.destroyAllWindows()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@main.command(name="server")
|
|
30
|
+
@click.option(
|
|
31
|
+
"--http-port", default=8000, help="Port for HTTP/WebSocket server."
|
|
32
|
+
)
|
|
33
|
+
@click.option("--tcp-port", default=9999, help="Port for TCP frame source.")
|
|
34
|
+
def run(http_port, tcp_port):
|
|
35
|
+
from cvframes.web.server import main
|
|
36
|
+
|
|
37
|
+
asyncio.run(main(http_port, tcp_port))
|
|
File without changes
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
importlib.import_module("aiohttp")
|
|
5
|
+
except ImportError as e:
|
|
6
|
+
print(
|
|
7
|
+
"❌ Missing required dependency: aiohttp\n"
|
|
8
|
+
"➡️ Please install it with:\n\n"
|
|
9
|
+
" pip install cv-frames[web]\n"
|
|
10
|
+
)
|
|
11
|
+
raise SystemExit(1) from e
|
|
12
|
+
|
|
13
|
+
import importlib
|
|
14
|
+
import select
|
|
15
|
+
import socket
|
|
16
|
+
from contextlib import contextmanager
|
|
17
|
+
|
|
18
|
+
import cv2
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FrameStreamer:
|
|
22
|
+
def __init__(self, sock):
|
|
23
|
+
self.sock = sock
|
|
24
|
+
|
|
25
|
+
def imshow(self, frame):
|
|
26
|
+
_, buffer = cv2.imencode(".jpg", frame)
|
|
27
|
+
self.sock.sendall(buffer.tobytes())
|
|
28
|
+
|
|
29
|
+
def waitKey(self, timeout_ms=1000):
|
|
30
|
+
self.sock.setblocking(0)
|
|
31
|
+
ready = select.select([self.sock], [], [], timeout_ms / 1000.0)
|
|
32
|
+
if not ready[0]:
|
|
33
|
+
return -1
|
|
34
|
+
|
|
35
|
+
if data := self.sock.recv(1024):
|
|
36
|
+
msg = data.decode().strip()
|
|
37
|
+
return int(msg)
|
|
38
|
+
return -1
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@contextmanager
|
|
42
|
+
def connect_to_server(host="127.0.0.1", port=9999):
|
|
43
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
44
|
+
sock.connect((host, port))
|
|
45
|
+
print("[Client] Connected to server")
|
|
46
|
+
try:
|
|
47
|
+
yield FrameStreamer(sock)
|
|
48
|
+
finally:
|
|
49
|
+
sock.close()
|
|
50
|
+
print("[Client] Disconnected")
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from aiohttp import web
|
|
7
|
+
except ImportError as e:
|
|
8
|
+
missing = e.name if hasattr(e, "name") else "aiohttp"
|
|
9
|
+
print(
|
|
10
|
+
f"❌ Missing required dependency: {missing}\n"
|
|
11
|
+
f"➡️ Please install it with:\n\n"
|
|
12
|
+
f" pip install cv-frames[web]\n"
|
|
13
|
+
)
|
|
14
|
+
raise SystemExit(1) from e
|
|
15
|
+
|
|
16
|
+
import importlib.resources as pkg_resources
|
|
17
|
+
|
|
18
|
+
MAPPING = {
|
|
19
|
+
"ArrowUp": 82,
|
|
20
|
+
"ArrowDown": 84,
|
|
21
|
+
"ArrowLeft": 81,
|
|
22
|
+
"ArrowRight": 83,
|
|
23
|
+
"Escape": 27,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def broadcast_task(
|
|
28
|
+
frame_queue: asyncio.Queue,
|
|
29
|
+
clients: set,
|
|
30
|
+
clients_lock: asyncio.Lock,
|
|
31
|
+
):
|
|
32
|
+
while True:
|
|
33
|
+
frame = await frame_queue.get()
|
|
34
|
+
async with clients_lock:
|
|
35
|
+
to_remove = set()
|
|
36
|
+
for ws in clients:
|
|
37
|
+
if ws.closed:
|
|
38
|
+
to_remove.add(ws)
|
|
39
|
+
continue
|
|
40
|
+
try:
|
|
41
|
+
await ws.send_bytes(frame)
|
|
42
|
+
except Exception:
|
|
43
|
+
to_remove.add(ws)
|
|
44
|
+
for ws in to_remove:
|
|
45
|
+
clients.remove(ws)
|
|
46
|
+
frame_queue.task_done()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def handle_tcp_connection(
|
|
50
|
+
reader,
|
|
51
|
+
writer,
|
|
52
|
+
frame_queue: asyncio.Queue,
|
|
53
|
+
):
|
|
54
|
+
print("[TCP] Frame source connected")
|
|
55
|
+
buffer = b""
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
while True:
|
|
59
|
+
chunk = await reader.read(4096)
|
|
60
|
+
if not chunk:
|
|
61
|
+
print("[TCP] Source disconnected")
|
|
62
|
+
break
|
|
63
|
+
buffer += chunk
|
|
64
|
+
|
|
65
|
+
while b"\xff\xd8" in buffer and b"\xff\xd9" in buffer:
|
|
66
|
+
start = buffer.find(b"\xff\xd8")
|
|
67
|
+
end = buffer.find(b"\xff\xd9") + 2
|
|
68
|
+
if end <= start:
|
|
69
|
+
break
|
|
70
|
+
jpg = buffer[start:end]
|
|
71
|
+
buffer = buffer[end:]
|
|
72
|
+
await frame_queue.put(jpg)
|
|
73
|
+
|
|
74
|
+
except Exception as e:
|
|
75
|
+
print(f"[TCP] Error: {e}")
|
|
76
|
+
finally:
|
|
77
|
+
writer.close()
|
|
78
|
+
await writer.wait_closed()
|
|
79
|
+
print("[TCP] Connection closed")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def websocket_handler(request):
|
|
83
|
+
app = request.app
|
|
84
|
+
clients = app["clients"]
|
|
85
|
+
clients_lock = app["clients_lock"]
|
|
86
|
+
tcp_send_lock = app["tcp_send_lock"]
|
|
87
|
+
tcp_conn_writer = app.get("tcp_conn_writer")
|
|
88
|
+
|
|
89
|
+
ws = web.WebSocketResponse()
|
|
90
|
+
await ws.prepare(request)
|
|
91
|
+
|
|
92
|
+
print(f"[WS] Client connected: {request.remote}")
|
|
93
|
+
async with clients_lock:
|
|
94
|
+
clients.add(ws)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
async for msg in ws:
|
|
98
|
+
if msg.type == web.WSMsgType.TEXT:
|
|
99
|
+
try:
|
|
100
|
+
data = json.loads(msg.data)
|
|
101
|
+
except json.JSONDecodeError:
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
if data.get("type") == "key":
|
|
105
|
+
key = data.get("key")
|
|
106
|
+
print(f"[WS] Key pressed: {key}")
|
|
107
|
+
code = MAPPING.get(
|
|
108
|
+
key, ord(key) if len(key) == 1 else None
|
|
109
|
+
)
|
|
110
|
+
if code is None:
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
print(f"[WS] Sending key code: {code}")
|
|
114
|
+
async with tcp_send_lock:
|
|
115
|
+
if tcp_conn_writer := request.app.get(
|
|
116
|
+
"tcp_conn_writer"
|
|
117
|
+
):
|
|
118
|
+
tcp_conn_writer.write(f"{code}".encode())
|
|
119
|
+
await tcp_conn_writer.drain()
|
|
120
|
+
|
|
121
|
+
elif msg.type == web.WSMsgType.ERROR:
|
|
122
|
+
print(f"[WS] Error: {ws.exception()}")
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
print(f"[WS] Receive error: {e}")
|
|
127
|
+
|
|
128
|
+
finally:
|
|
129
|
+
async with clients_lock:
|
|
130
|
+
clients.discard(ws)
|
|
131
|
+
print(f"[WS] Client disconnected: {request.remote}")
|
|
132
|
+
|
|
133
|
+
return ws
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
async def main(http_port: int, tcp_port: int):
|
|
137
|
+
app = web.Application()
|
|
138
|
+
|
|
139
|
+
frame_queue: asyncio.Queue = asyncio.Queue()
|
|
140
|
+
clients: set[web.WebSocketResponse] = set()
|
|
141
|
+
clients_lock = asyncio.Lock()
|
|
142
|
+
tcp_send_lock = asyncio.Lock()
|
|
143
|
+
tcp_conn_writer = None
|
|
144
|
+
|
|
145
|
+
app["frame_queue"] = frame_queue
|
|
146
|
+
app["clients"] = clients
|
|
147
|
+
app["clients_lock"] = clients_lock
|
|
148
|
+
app["tcp_send_lock"] = tcp_send_lock
|
|
149
|
+
app["tcp_conn_writer"] = tcp_conn_writer
|
|
150
|
+
|
|
151
|
+
# Get static paths relative to package
|
|
152
|
+
index = pkg_resources.files("cvframes.web.static").joinpath("index.html")
|
|
153
|
+
static = pkg_resources.files("cvframes.web.static")
|
|
154
|
+
|
|
155
|
+
app.router.add_get("/", lambda request: web.FileResponse(index))
|
|
156
|
+
app.router.add_get("/ws", websocket_handler)
|
|
157
|
+
app.router.add_static("/static/", path=static, name="static")
|
|
158
|
+
|
|
159
|
+
runner = web.AppRunner(app)
|
|
160
|
+
await runner.setup()
|
|
161
|
+
site = web.TCPSite(runner, "0.0.0.0", http_port)
|
|
162
|
+
await site.start()
|
|
163
|
+
print(f"✅ Server running at: http://localhost:{http_port}")
|
|
164
|
+
|
|
165
|
+
broadcaster = asyncio.create_task(
|
|
166
|
+
broadcast_task(frame_queue, clients, clients_lock)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
async def tcp_server_callback(reader, writer):
|
|
170
|
+
app["tcp_conn_writer"] = writer
|
|
171
|
+
await handle_tcp_connection(reader, writer, frame_queue)
|
|
172
|
+
app["tcp_conn_writer"] = None
|
|
173
|
+
|
|
174
|
+
server = await asyncio.start_server(
|
|
175
|
+
tcp_server_callback, "0.0.0.0", tcp_port
|
|
176
|
+
)
|
|
177
|
+
print(f"[TCP] Waiting for frame source on port {tcp_port}")
|
|
178
|
+
|
|
179
|
+
async with server:
|
|
180
|
+
await server.serve_forever()
|
|
181
|
+
|
|
182
|
+
broadcaster.cancel()
|
|
183
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
184
|
+
await broadcaster
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<title>Live Stream</title>
|
|
6
|
+
<style>
|
|
7
|
+
html, body {
|
|
8
|
+
margin: 0;
|
|
9
|
+
padding: 0;
|
|
10
|
+
height: 100%;
|
|
11
|
+
/* background: #255; */
|
|
12
|
+
display: flex;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
align-items: center;
|
|
15
|
+
font-family: sans-serif;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#stream-container {
|
|
19
|
+
position: relative;
|
|
20
|
+
display: flex;
|
|
21
|
+
justify-content: center;
|
|
22
|
+
align-items: center;
|
|
23
|
+
max-width: 100%;
|
|
24
|
+
max-height: 100%;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#stream {
|
|
28
|
+
max-width: 100%;
|
|
29
|
+
max-height: 100vh;
|
|
30
|
+
height: auto;
|
|
31
|
+
width: auto;
|
|
32
|
+
opacity: 1;
|
|
33
|
+
transition: opacity 0.2s;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#waiting-message {
|
|
37
|
+
position: absolute;
|
|
38
|
+
background: rgba(0, 0, 0, 0.6);
|
|
39
|
+
color: white;
|
|
40
|
+
padding: 10px 20px;
|
|
41
|
+
border-radius: 8px;
|
|
42
|
+
display: none;
|
|
43
|
+
font-size: 1.2rem;
|
|
44
|
+
}
|
|
45
|
+
</style>
|
|
46
|
+
</head>
|
|
47
|
+
|
|
48
|
+
<body>
|
|
49
|
+
<div id="stream-container">
|
|
50
|
+
<img id="stream" />
|
|
51
|
+
<div id="waiting-message">Waiting for video...</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<script>
|
|
55
|
+
const ws = new WebSocket("ws://" + location.host + "/ws");
|
|
56
|
+
const img = document.getElementById("stream");
|
|
57
|
+
const waitingMessage = document.getElementById("waiting-message");
|
|
58
|
+
|
|
59
|
+
ws.binaryType = "arraybuffer";
|
|
60
|
+
|
|
61
|
+
function showPlaceholder() {
|
|
62
|
+
img.style.opacity = "0.3";
|
|
63
|
+
waitingMessage.style.display = "block";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function hidePlaceholder() {
|
|
67
|
+
img.style.opacity = "1";
|
|
68
|
+
waitingMessage.style.display = "none";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
ws.onmessage = (event) => {
|
|
72
|
+
if (typeof event.data === "string") {
|
|
73
|
+
try {
|
|
74
|
+
const msg = JSON.parse(event.data);
|
|
75
|
+
if (msg.type === "waiting") {
|
|
76
|
+
showPlaceholder();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.warn("Invalid message:", event.data);
|
|
81
|
+
}
|
|
82
|
+
} else if (event.data instanceof ArrayBuffer) {
|
|
83
|
+
const blob = new Blob([event.data], { type: "image/jpeg" });
|
|
84
|
+
img.src = URL.createObjectURL(blob);
|
|
85
|
+
hidePlaceholder();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
ws.onopen = () => {
|
|
90
|
+
console.log("Connected to WebSocket");
|
|
91
|
+
|
|
92
|
+
document.addEventListener("keydown", (e) => {
|
|
93
|
+
const msg = JSON.stringify({ type: "key", key: e.key });
|
|
94
|
+
ws.send(msg);
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
</script>
|
|
98
|
+
</body>
|
|
99
|
+
|
|
100
|
+
</html>
|
|
@@ -29,17 +29,27 @@ build-backend = "setuptools.build_meta"
|
|
|
29
29
|
name = "cv-frames"
|
|
30
30
|
dynamic = ["version"]
|
|
31
31
|
description = "Read frames from OpenCV like humans"
|
|
32
|
+
readme = "README.rst"
|
|
33
|
+
|
|
32
34
|
dependencies = [
|
|
33
35
|
"opencv-python",
|
|
34
36
|
"numpy",
|
|
35
37
|
"click",
|
|
36
38
|
]
|
|
37
39
|
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
web = ["aiohttp"]
|
|
42
|
+
|
|
38
43
|
[project.scripts]
|
|
39
44
|
cv-frames = "cvframes.cli:main"
|
|
40
45
|
|
|
41
46
|
[tool.setuptools]
|
|
42
|
-
packages = [
|
|
47
|
+
packages = [
|
|
48
|
+
"cvframes",
|
|
49
|
+
"cvframes.web",
|
|
50
|
+
"cvframes.web.static",
|
|
51
|
+
]
|
|
52
|
+
include-package-data = true
|
|
43
53
|
|
|
44
54
|
[tool.setuptools_scm]
|
|
45
55
|
version_scheme = "post-release" # Uses Git tags for versioning
|
cv_frames-0.0.1/PKG-INFO
DELETED
cv_frames-0.0.1/README.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# Read frames from OpenCV like humans
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|