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.
Files changed (26) hide show
  1. {cv_frames-0.0.1 → cv_frames-0.0.2}/.github/workflows/tests.yml +6 -0
  2. {cv_frames-0.0.1 → cv_frames-0.0.2}/.gitignore +1 -0
  3. cv_frames-0.0.2/PKG-INFO +74 -0
  4. cv_frames-0.0.2/README.rst +63 -0
  5. cv_frames-0.0.2/cv_frames.egg-info/PKG-INFO +74 -0
  6. {cv_frames-0.0.1 → cv_frames-0.0.2}/cv_frames.egg-info/SOURCES.txt +5 -1
  7. {cv_frames-0.0.1 → cv_frames-0.0.2}/cv_frames.egg-info/requires.txt +3 -0
  8. {cv_frames-0.0.1 → cv_frames-0.0.2}/cvframes/cli.py +13 -0
  9. cv_frames-0.0.2/cvframes/web/__init__.py +0 -0
  10. cv_frames-0.0.2/cvframes/web/client.py +50 -0
  11. cv_frames-0.0.2/cvframes/web/server.py +184 -0
  12. cv_frames-0.0.2/cvframes/web/static/index.html +100 -0
  13. {cv_frames-0.0.1 → cv_frames-0.0.2}/pyproject.toml +11 -1
  14. cv_frames-0.0.1/PKG-INFO +0 -7
  15. cv_frames-0.0.1/README.md +0 -1
  16. cv_frames-0.0.1/cv_frames.egg-info/PKG-INFO +0 -7
  17. {cv_frames-0.0.1 → cv_frames-0.0.2}/.github/workflows/publish.yml +0 -0
  18. {cv_frames-0.0.1 → cv_frames-0.0.2}/.pre-commit-config.yaml +0 -0
  19. {cv_frames-0.0.1 → cv_frames-0.0.2}/cv_frames.egg-info/dependency_links.txt +0 -0
  20. {cv_frames-0.0.1 → cv_frames-0.0.2}/cv_frames.egg-info/entry_points.txt +0 -0
  21. {cv_frames-0.0.1 → cv_frames-0.0.2}/cv_frames.egg-info/top_level.txt +0 -0
  22. {cv_frames-0.0.1 → cv_frames-0.0.2}/cvframes/__init__.py +0 -0
  23. {cv_frames-0.0.1 → cv_frames-0.0.2}/cvframes/iterate.py +0 -0
  24. {cv_frames-0.0.1 → cv_frames-0.0.2}/requirements.txt +0 -0
  25. {cv_frames-0.0.1 → cv_frames-0.0.2}/setup.cfg +0 -0
  26. {cv_frames-0.0.1 → cv_frames-0.0.2}/tests/test_iterate.py +0 -0
@@ -32,3 +32,9 @@ jobs:
32
32
 
33
33
  - name: Run pre-commit checks
34
34
  uses: pre-commit/action@v3.0.1
35
+
36
+ - name: Check package metadata rendering (twine check)
37
+ run: |
38
+ pip install build twine
39
+ python -m build --sdist --wheel
40
+ twine check dist/*
@@ -3,3 +3,4 @@ __pycache__/
3
3
  .python-version
4
4
  .coverage
5
5
  *.mp4
6
+ .vscode
@@ -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.md
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,6 @@
1
1
  opencv-python
2
2
  numpy
3
3
  click
4
+
5
+ [web]
6
+ aiohttp
@@ -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 = ["cvframes"]
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
@@ -1,7 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: cv-frames
3
- Version: 0.0.1
4
- Summary: Read frames from OpenCV like humans
5
- Requires-Dist: opencv-python
6
- Requires-Dist: numpy
7
- Requires-Dist: click
cv_frames-0.0.1/README.md DELETED
@@ -1 +0,0 @@
1
- # Read frames from OpenCV like humans
@@ -1,7 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: cv-frames
3
- Version: 0.0.1
4
- Summary: Read frames from OpenCV like humans
5
- Requires-Dist: opencv-python
6
- Requires-Dist: numpy
7
- Requires-Dist: click
File without changes
File without changes
File without changes