livepeer / livepeer/livepeer-python-gateway

Encapsulate live-runner session payments in a stateful session/client object

Open
#30 2 comments 0 reactions 1 assignee View on GitHub

@rickstaa is already working on this.

Since Jun 17, 2026.

  • #31 by @rickstaa — closed without merging
enhancement
Dominant language
Python
Stars
1
Forks
7
PR merge metrics
No merged PRs in 30d

Description

## Problem

Streamed live-runner sessions (trickle / websocket) must keep paying for as long as the session is open, because the orchestrator meters the open session by wall-clock/segments and drops it when the balance runs dry. Today the SDK exposes this as a free function, `run_session_payments(session, interval=...)`, which the caller must drive manually:

```python
payment_task = asyncio.create_task(run_session_payments(session, interval=...))
...
finally:
payment_task.cancel()
await payment_task
```

`reserve_session` already mints and attaches the `payment_session` credential to the returned `LiveRunnerSession`, but `LiveRunnerSession` is a frozen dataclass with no lifecycle, so it cannot own the background task. That pushes task creation and (critically) cancellation onto every consumer. Forget the `finally` and you silently stop paying or leak a task. The echo example (`example-apps/echo/client.py`) shows the boilerplate this requires.

## Prior art (already in our codebase)

Two better-encapsulated patterns already exist and prove the approach:

- `lv2v.py`: `Lv2vJob.start_payment_sender()` runs a per-output-segment payment sender, auto-started by `start_lv2v(start_payments=True)` and cancelled by `job.aclose()`. The consumer cannot forget it.
- daydreamlive/scope `ja/runner` (`server/livepeer_client.py`): `LivepeerClient` starts an interval `_payment_loop` right after connect, gated on `payment_session is not None`, and cancels it on teardown. This is effectively `run_session_payments` hand-rolled inside a client object.

## Proposal

Make `reserve_session` return a stateful session/client that owns the payment lifecycle, so payments are automatic for on-chain sessions:

```python
async with await reserve_session(app=..., signer_url=signer) as session:
# publish/consume trickle; payments start and stop automatically
```

- Auto-start the payment loop when `payment_session is not None` (on-chain only; no-op offchain).
- Auto-cancel on context exit / `aclose()`, alongside `stop_runner_session`.
- Keep `run_session_payments` as the internal primitive.
- This mirrors `start_lv2v(start_payments=True)` + `job.aclose()`.

Payments still need a scope anchor (the session must know when the stream ended); the context manager / `aclose()` provides it. The object cannot infer that media stopped on its own.

## Open questions

1. One unified `LiveRunnerSession` object that subsumes `Lv2vJob`, or keep lv2v separate and only wrap the general live-runner path? The former removes the parallel implementations; the latter is the minimal change.
2. Cadence semantics to standardize on: lv2v pays per output segment (only while media flows); `run_session_payments` pays on a fixed interval (pays even when paused-but-open). These differ in over-payment behavior and should be decided before standardizing the interface.

## Context

Follow-up from the echo example migration and the `run_session_payments` work (branch `rs/live-runner-session-payments`). Related: gateway PR #25 (SSE streaming).

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.