Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,105 @@ async def main():

For a complete working example with green tint processing, see `examples/async_processor_example.py`.

## Decorators: auto-wired handlers

PyTrickle provides a set of decorators that make it easy to implement stream handlers as plain methods on a class. The decorators:

- Mark methods for auto-discovery and wiring into the stream processor
- Bridge sync functions into async (run sync code in a thread pool)
- Normalize return values so your code can stay simple

### Available decorators

- `@video_handler`
- Signature: `(self, frame: VideoFrame) -> Optional[VideoFrame | torch.Tensor | numpy.ndarray | None]`
- Return normalization:
- `None` → pass-through original frame
- `VideoFrame` → used as-is
- `torch.Tensor` / `numpy.ndarray` → replaces the frame's tensor via `frame.replace_tensor(...)`

- `@audio_handler`
- Signature: `(self, frame: AudioFrame) -> Optional[List[AudioFrame] | AudioFrame | torch.Tensor | numpy.ndarray | None]`
- Return normalization:
- `None` → `[original frame]`
- `AudioFrame` → `[that frame]`
- `List[AudioFrame]` → returned as-is
- `torch.Tensor` / `numpy.ndarray` → replaces samples via `frame.replace_samples(...)`, returning `[frame]`

- `@model_loader`
- Signature: any (sync or async), called once during model/resource loading.

- `@param_updater` (optionally `@param_updater(model=MyParamsModel)`)
- Signature: `(self, params)` where `params` is a `dict` or a validated Pydantic model instance if `model=...` is provided.
- If you pass a Pydantic `BaseModel`, incoming params are validated and parsed before your method runs.

- `@on_stream_stop`
- Signature: `() -> None` (sync or async). Invoked when a stream stops for cleanup.

All of the above decorators produce async wrappers internally, so they can be awaited by the framework even if your implementation is synchronous.

### Using decorators with StreamProcessor

```python
from pytrickle.decorators import video_handler, audio_handler, model_loader, param_updater, on_stream_stop
from pytrickle.stream_processor import StreamProcessor
from pytrickle.frames import VideoFrame, AudioFrame
from typing import List, Optional

class MyHandler:
@model_loader
async def load(self):
# Load models/resources here
...

@video_handler
def handle_video(self, frame: VideoFrame):
# Return None to pass through, VideoFrame, or a tensor/ndarray replacement
return None

@audio_handler
def handle_audio(self, frame: AudioFrame):
# Return None/[frame]/AudioFrame/List[AudioFrame] or tensor/ndarray samples
return None

@param_updater
async def update(self, params: dict):
# Update runtime parameters
...

@on_stream_stop
def cleanup(self):
# Release resources
...

# Auto-discover decorated handlers and run
sp = StreamProcessor.from_handlers(MyHandler(), port=8000)
sp.run() # blocking
```

### Parameter validation with Pydantic (optional)

```python
from pydantic import BaseModel
from pytrickle.decorators import param_updater

class Params(BaseModel):
threshold: float = 0.5
enabled: bool = True

class Handler:
@param_updater(model=Params)
async def update(self, params: Params):
# params is a validated model instance
...
```

### Error handling and sync bridging

- Decorators create async wrappers that run sync code in a thread (`asyncio.to_thread`) so the event loop stays responsive.
- If your handler raises, the framework logs the error and falls back to pass-through behavior to keep the stream alive.
- When constructing `StreamProcessor` directly (without decorators), all handlers must be true async callables. Using decorators is recommended because they ensure async wrappers and return normalization.

## HTTP API

PyTrickle automatically provides a REST API for your video processor:
Expand Down
196 changes: 196 additions & 0 deletions README_CLI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# PyTrickle CLI Reference

The PyTrickle CLI provides commands to scaffold and run streaming applications using the PyTrickle framework.

## Installation

Use your preferred Python environment, then install this package:

```bash
pip install -e .
```

After installation, the `pytrickle` command will be available globally.

## Commands Overview

```bash
pytrickle --help # Show main help
pytrickle init --help # Show init command help
pytrickle run --help # Show run command help
```

## `pytrickle init` - Scaffold a New App

Creates a new PyTrickle application with a complete project structure.

### Usage

```bash
pytrickle init [PATH] [OPTIONS]
```

### Arguments

- `PATH` (optional): Target directory for the new app (default: current directory `.`)

### Options

- `--package NAME`: Package name to use (default: derived from folder name)
- `--force`: Overwrite existing files without prompting

### Examples

```bash
# Create app in current directory
pytrickle init

# Create app in specific directory
pytrickle init my_streaming_app

# Create with custom package name
pytrickle init my_app --package custom_name

# Force overwrite existing files
pytrickle init my_app --force
```

### Generated Structure

```
my_app/
├── my_app/
│ ├── __init__.py # Package initializer
│ ├── __main__.py # Entry point for 'python -m my_app'
│ └── handlers.py # Main handlers with decorators
└── README.md # Basic usage instructions
```

### Handler Template

The generated `handlers.py` includes:

- `@model_loader` - Load your model/resources on startup
- `@video_handler` - Process video frames (returns None for pass-through)
- `@param_updater` - Handle parameter updates during streaming
- `@on_stream_stop` - Cleanup when stream ends

## `pytrickle run` - Run an Existing App

Convenience command to run a Python module/package.

### Usage

```bash
pytrickle run --module MODULE_NAME
```

### Options

- `--module MODULE_NAME` (required): Name of the Python module/package to run

### Examples

```bash
# Run a scaffolded app
pytrickle run --module my_app

# Run any Python module with __main__.py
pytrickle run --module my_existing_package
```

This is equivalent to `python -m MODULE_NAME` but provides consistent CLI experience.

## Quick Start Workflow

1. **Create a new app:**
```bash
pytrickle init video_processor
cd video_processor
```

2. **Customize your handlers:**
Edit `video_processor/handlers.py` to implement your processing logic.

3. **Run the app:**
```bash
python -m video_processor
```
or
```bash
pytrickle run --module video_processor
```

4. **Test the streaming endpoint:**
The server starts on port 8000. Send a POST request to `/api/stream/start`:

```json
{
"subscribe_url": "http://localhost:3389/sample",
"publish_url": "http://localhost:3389/output",
"gateway_request_id": "demo-1",
"params": {"width": 512, "height": 512}
}
```

## Server Endpoints

Once running, your app exposes these endpoints:

- `POST /api/stream/start` - Start streaming with parameters
- `POST /api/stream/stop` - Stop active stream
- `POST /api/stream/params` - Update stream parameters
- `GET /api/stream/status` - Get current stream status
- `GET /health` - Health check
- `GET /version` - App version info

## Development Tips

- **Debugging**: Add logging to your handlers to trace execution
- **Parameters**: Use the `@param_updater` to modify behavior during streaming
- **Frame Processing**: Return `None` from handlers for pass-through behavior
- **Error Handling**: Exceptions in handlers are caught and logged automatically
- **Model Loading**: Use `@model_loader` for one-time initialization on startup

## Advanced Usage

### Custom Port

Modify the `main()` function in your generated `handlers.py`:

```python
processor = StreamProcessor.from_handlers(
handlers,
name="my-app",
port=9000, # Custom port
frame_skip_config=FrameSkipConfig(),
)
```

### Multiple Handler Types

Add more decorators to your handler class:

```python
@audio_handler
async def process_audio(self, frame):
# Process audio frames
return None
```

### Custom Frame Skipping

Configure frame skipping in your main function:

```python
from pytrickle.frame_skipper import FrameSkipConfig

config = FrameSkipConfig(
max_queue_size=10,
skip_threshold=5
)
processor = StreamProcessor.from_handlers(
handlers,
frame_skip_config=config
)
```
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,7 @@ ignore_missing_imports = true
[tool.setuptools_scm]
# Enable setuptools-scm for automatic version management from Git tags
write_to = "pytrickle/_version.py"
fallback_version = "0.0.0"
fallback_version = "0.0.0"

[project.scripts]
pytrickle = "pytrickle.cli:main"
18 changes: 17 additions & 1 deletion pytrickle/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,19 @@
from .fps_meter import FPSMeter
from .frame_skipper import FrameSkipConfig


from .decorators import (
trickle_handler,
video_handler,
audio_handler,
model_loader,
param_updater,
on_stream_stop,
)

from . import api

from .version import __version__
from .version import VERSION as __version__

__all__ = [
"TrickleClient",
Expand All @@ -59,5 +69,11 @@
"FrameProcessor",
"FPSMeter",
"FrameSkipConfig",
"trickle_handler",
"video_handler",
"audio_handler",
"model_loader",
"param_updater",
"on_stream_stop",
"__version__"
]
Loading
Loading