-
Notifications
You must be signed in to change notification settings - Fork 6
Live Runner + Scope support #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 65 commits
8058120
4305580
a712cf0
2b1b00c
be92279
d7bb4ca
db20786
9401537
0c87e75
400b4f3
e25dda2
dac0296
93c05a6
78dc085
7b04ba3
65a7e2b
0772b32
c2b3b6f
a99d188
d9b983d
d00951e
f48707b
43d3c28
92d9517
a14d749
97e4fc5
9f4fe33
d3ecb91
a2ec6e1
8ec09b3
5a7e832
1b1b766
2eda0ff
d363e5e
795d402
b72d72d
ca10a3a
e60c298
34cae50
4dd69cb
a23dfe9
29c1783
30ea2ec
34aee7c
04404a1
fe1d86a
ce7a0a3
2dbc865
a242302
77a0945
e267285
db9c461
a01d574
37844c2
0fa8963
3700a70
f45cb26
7b4bdb2
62f4bd4
0233659
f474038
b97ab53
a7054f7
2f29404
995f93f
7253350
a560fb5
45830bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| name: Tests | ||
|
|
||
| on: | ||
| pull_request: | ||
| push: | ||
| branches: | ||
| - main | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| ruff: | ||
| # disabled until after a repo-wide ruff pass | ||
| if: ${{ false }} | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
| - name: Install uv | ||
| uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 | ||
| with: | ||
| enable-cache: true | ||
| python-version: "3.12" | ||
| - name: Install pinned lint dependencies | ||
| run: uv sync --locked --group lint | ||
| - name: Run Ruff | ||
| run: uv run --frozen --group lint ruff check . | ||
|
|
||
| pytest: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
| - name: Install uv | ||
| uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 | ||
| with: | ||
| enable-cache: true | ||
| python-version: "3.12" | ||
| - name: Install locked test dependencies | ||
| run: uv sync --locked --group test | ||
| - name: Run pytest with coverage | ||
| run: uv run --frozen --group test pytest --cov=livepeer_gateway --cov-branch --cov-report=term-missing | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| # Echo Live Runner Demo | ||
|
|
||
| This example demonstrates: | ||
|
|
||
| * Runner registration | ||
| * Video input - taken from a local file | ||
| * Video output - echoed to output with blur applied | ||
| * Parameter updates - adjust the amount of blur | ||
|
|
||
| Start go-livepeer: | ||
|
|
||
| ```sh | ||
| ./livepeer -orchestrator -useLiveRunners -serviceAddr localhost:8935 -v 99 -orchSecret abcdef | ||
| ``` | ||
|
|
||
| Start the runner: | ||
|
|
||
| ```sh | ||
| uv run examples/echo/runner.py --orchestrator https://localhost:8935 --orchSecret abcdef | ||
| ``` | ||
|
Comment on lines
+18
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Use the configured HTTP orchestrator URL. The command starts go-livepeer without TLS but invokes the runner with 🤖 Prompt for AI Agents |
||
|
|
||
| Run the client with a local sample input (`~/samples/bbb_720p.mp4`): | ||
|
|
||
| ```sh | ||
| uv run examples/echo/client.py --blur ~/samples/bbb_720p.mp4 | ||
| ``` | ||
|
|
||
| The resulting file is stored at echo-out.ts. To use a different file | ||
| or redirect to stdout for live playback: | ||
|
|
||
| ```sh | ||
| uv run client.py --blur --output - ~/samples/bbb_720p.mp4 | ffplay - | ||
| ``` | ||
|
|
||
| The client discovers the `livepeer-sample/echo` runner automatically. To use a | ||
| different orchestrator or discovery endpoint: | ||
|
|
||
| ```sh | ||
| uv run examples/echo/client.py --discovery http://localhost:8935/discovery --blur ~/samples/bbb_720p.mp4 | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| #!/usr/bin/env python3 | ||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import asyncio | ||
| import sys | ||
| import time | ||
| from contextlib import nullcontext, suppress | ||
| from pathlib import Path | ||
|
|
||
| import av | ||
|
|
||
| from livepeer_gateway.errors import LivepeerGatewayError | ||
| from livepeer_gateway.live_runner import stop_runner_session | ||
| from livepeer_gateway.media_output import MediaOutput | ||
| from livepeer_gateway.media_publish import MediaPublish | ||
| from livepeer_gateway.http import post_json | ||
| from livepeer_gateway.selection import reserve_session | ||
|
|
||
| DEFAULT_DISCOVERY = "http://localhost:8935/discovery" | ||
| ECHO_APP_ID = "livepeer-sample/echo" | ||
| DEFAULT_OUTPUT = "echo-out.ts" | ||
| BLUR_UPDATE_INTERVAL_S = 0.01 | ||
| MAX_BLUR_RADIUS = 100 | ||
|
|
||
|
|
||
| def _log(*args: object) -> None: | ||
| print(*args, file=sys.stderr) | ||
|
|
||
|
|
||
| def _parse_args() -> argparse.Namespace: | ||
| parser = argparse.ArgumentParser(description="Run the proxied echo Live Runner demo.") | ||
| parser.add_argument("input") | ||
| parser.add_argument("--discovery", default=DEFAULT_DISCOVERY) | ||
| parser.add_argument("--output", default=DEFAULT_OUTPUT) | ||
| parser.add_argument("--radius", type=int, default=75) | ||
| parser.add_argument("--max-frames", type=int, default=0, help="Stop after this many input video frames (0 = full file).") | ||
| parser.add_argument("--blur", action="store_true", help="Sweep blur radius while publishing the sample.") | ||
| return parser.parse_args() | ||
|
|
||
|
|
||
| def _channel_url(echo_response: dict[str, object], name: str) -> str: | ||
| url = echo_response.get(name) | ||
| if not isinstance(url, str) or not url: | ||
| raise LivepeerGatewayError(f"echo response missing {name!r} url") | ||
| return url | ||
|
|
||
|
|
||
| async def _publish_video( | ||
| input_path: Path, | ||
| publish_url: str, | ||
| *, | ||
| max_frames: int = 0, | ||
| app_url: str = "", | ||
| blur: bool = False, | ||
| ) -> None: | ||
| input_ = av.open(str(input_path)) | ||
| try: | ||
| if not input_.streams.video: | ||
| raise LivepeerGatewayError(f"No video stream found in input file: {input_path}") | ||
| publisher = MediaPublish(publish_url) | ||
| prev_pts_time: float | None = None | ||
| prev_wall: float | None = None | ||
| next_update_pts_time: float | None = None | ||
| blur_radius = 0 | ||
| blur_direction = 1 | ||
|
|
||
| try: | ||
| for index, frame in enumerate(input_.decode(video=0), start=1): | ||
| if max_frames > 0 and index > max_frames: | ||
| break | ||
| current_pts_time = None | ||
| if frame.pts is not None and frame.time_base is not None: | ||
| current_pts_time = float(frame.pts * frame.time_base) | ||
| if next_update_pts_time is None: | ||
| next_update_pts_time = current_pts_time | ||
|
|
||
| while ( | ||
| blur | ||
| and app_url | ||
| and current_pts_time is not None | ||
| and next_update_pts_time is not None | ||
| and current_pts_time >= next_update_pts_time | ||
| ): | ||
| await post_json(f"{app_url.rstrip('/')}/update", {"mode": "blur", "radius": blur_radius}) | ||
| if blur_radius == MAX_BLUR_RADIUS: | ||
| blur_direction = -1 | ||
| elif blur_radius == 0: | ||
| blur_direction = 1 | ||
| blur_radius += blur_direction | ||
| next_update_pts_time += BLUR_UPDATE_INTERVAL_S | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| if ( | ||
| prev_pts_time is not None | ||
| and prev_wall is not None | ||
| and current_pts_time is not None | ||
| ): | ||
| delta_s = current_pts_time - prev_pts_time | ||
| elapsed_s = time.monotonic() - prev_wall | ||
| sleep_s = max(0.0, delta_s - elapsed_s) | ||
| if sleep_s > 0: | ||
| await asyncio.sleep(sleep_s) | ||
|
|
||
| if current_pts_time is not None: | ||
| prev_pts_time = current_pts_time | ||
| prev_wall = time.monotonic() | ||
|
|
||
| await publisher.write_frame(frame) | ||
| finally: | ||
| await publisher.close() | ||
| finally: | ||
| input_.close() | ||
|
|
||
|
|
||
| async def main() -> None: | ||
| args = _parse_args() | ||
| input_path = Path(args.input).expanduser() | ||
| output_stdout = args.output.strip().lower() in {"-", "stdout"} | ||
| output_path = None if output_stdout else Path(args.output).expanduser() | ||
| if not input_path.exists(): | ||
| raise SystemExit(f"input file does not exist: {input_path}") | ||
|
|
||
| session = None | ||
|
|
||
| try: | ||
| session = await reserve_session(discovery_url=args.discovery, app=ECHO_APP_ID) | ||
| _log("runner_url:", session.runner.url if session.runner is not None else session.runner_url) | ||
| _log("session_id:", session.session_id) | ||
| _log("app_url:", session.app_url) | ||
|
|
||
| echo = await post_json(f"{session.app_url.rstrip('/')}/echo", {"radius": args.radius}) | ||
| in_url = _channel_url(echo, "in") | ||
| out_url = _channel_url(echo, "out") | ||
| _log("in:", in_url) | ||
| _log("out:", out_url) | ||
|
|
||
| with nullcontext(sys.stdout.buffer) if output_stdout else output_path.open("wb") as fh: | ||
| def _write_chunk(chunk: bytes) -> None: | ||
| fh.write(chunk) | ||
| if output_stdout: | ||
| fh.flush() | ||
|
|
||
| async with MediaOutput(out_url, on_bytes=_write_chunk): | ||
| await _publish_video( | ||
| input_path, | ||
| in_url, | ||
| max_frames=max(0, args.max_frames), | ||
| app_url=session.app_url, | ||
| blur=args.blur, | ||
| ) | ||
| _log("publish complete; waiting for output to drain...") | ||
| fh.flush() | ||
|
Comment on lines
+143
to
+152
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Wait for output consumption before closing it. Line 151 claims to drain output, but the context exits immediately afterward and closes 🤖 Prompt for AI Agents |
||
| except LivepeerGatewayError as exc: | ||
| raise SystemExit(f"ERROR: {exc}") from exc | ||
| finally: | ||
| if session is not None: | ||
| with suppress(Exception): | ||
| await stop_runner_session(session) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| asyncio.run(main()) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: livepeer/livepeer-python-gateway
Length of output: 1465
🌐 Web query:
actions/checkout default persist-credentials true README💡 Result:
In the actions/checkout GitHub Action, the persist-credentials input controls whether the authentication token or SSH key is configured in the local git configuration [1][2][3]. When set to true (which remains the default), the credentials are made available to subsequent steps in the job to allow authenticated git operations [1][2]. The action automatically removes these credentials during the post-job cleanup phase [1][2]. Starting with version 6.0.0, the action improved security by storing these credentials in a separate file under the $RUNNER_TEMP directory instead of writing them directly into the.git/config file [2][4]. While there has been community discussion and proposals to change the default value of persist-credentials to false to minimize security risks [5][6], the default value remains true in the current official documentation and action configuration [2][3]. Users who wish to disable this behavior and prevent credentials from being available to other steps in their workflow can explicitly set persist-credentials: false [1][2].
Citations:
persist-credentialsor change the default tofalseactions/checkout#485Disable checkout credential persistence
This workflow runs pull-request code via pytest and does not need authenticated Git operations after checkout. Set
persist-credentials: falseso the checkout token is not made available to later test steps.Proposed fix
📝 Committable suggestion
🧰 Tools
🪛 zizmor (1.28.0)
[warning] 16-16: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
🤖 Prompt for AI Agents
Source: Linters/SAST tools