Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 2
Composition SDK#94
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
Open
Gawor270
wants to merge
7
commits into
mainChoose a base branch
from
fce-3730
base:main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Uh oh!
There was an error while loading. Please reload this page.
Open
Composition SDK #94
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
d28d86e
Add the generated Composition API client
Gawor270 a01e093
Add CompositionClient
Gawor270 6b4fc2f
Forward a room's tracks into a composition
Gawor270 4bae4ee
Add the composition example
Gawor270 958301d
Name both generated clients after the API they wrap
Gawor270 49eebe2
Report Composition API failures as the server states them
Gawor270 ec49f4e
Start the demo's services with the server
Gawor270 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| # Your Fishjam envs, which you can get at https://fishjam.io/app | ||
| FISHJAM_ID="your-fishjam-id" | ||
| FISHJAM_MANAGEMENT_TOKEN="your-management-token" | ||
| # Only needed when running against a deployment other than production | ||
| # COMPOSITION_URL="http://localhost:8000" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| # Composition Demo | ||
| Demo application showing compositions, the real-time video compositing sessions, with | ||
| [Fishjam](https://fishjam.io) and the Python Server SDK. | ||
| Two sources are composed into one picture, laid out like a gaming livestream: a looping | ||
| movie fills the stage with your camera tucked into its top corner, framed by the Fishjam | ||
| logo and a caption bar. The result is sent to a Fishjam livestream that viewers watch. | ||
| ``` | ||
| [camera] ── WHIP ─┐ | ||
| ├─▶ composition ── WHIP ─▶ fishjam livestream ── WHEP ─▶ [viewers] | ||
| [movie mp4] ──────┘ | ||
| ``` | ||
| ## What it shows | ||
| - **Inputs**: a WHIP input the demo prints publishing credentials for, and an MP4 input | ||
| looping a movie from a URL | ||
| - **Renderers**: an SVG logo registered as an image, and the Inter font used by the caption | ||
| - **Scene**: the movie fills the stage with the camera in its top corner, on a cream frame | ||
| with a coral bar along the bottom. Each tile sits on a black backing, so a stream that is | ||
| not publishing yet reads as an empty tile rather than a hole | ||
| - _picture in picture_ — the movie fills the stage, the camera sits in its top corner | ||
| - _spotlight_ — the camera takes the stage with the movie tucked away | ||
| - _side by side_ — the movie and the camera share the stage | ||
| - **Audio**: both inputs mixed, with the movie ducked under the camera | ||
| ## Prerequisites | ||
| - Python 3.10+ | ||
| - [uv](https://docs.astral.sh/uv/) package manager | ||
| - Fishjam credentials ([get them here](https://fishjam.io/app)) | ||
| > [!IMPORTANT] | ||
| > All commands should be run from the `examples/composition` directory | ||
| ## Quick Start | ||
| 1. Install dependencies: | ||
| ```bash | ||
| uv sync | ||
| ``` | ||
| 2. Copy [`.env.example`](./.env.example) to `.env` and populate your environment variables. | ||
| 3. Run the server: | ||
| ```bash | ||
| uv run ./main.py | ||
| ``` | ||
| Starting the server creates the composition and the livestream, then serves two endpoints: | ||
| | Endpoint | Returns | | ||
| | --- | --- | | ||
| | `GET /streamer` | `url` and `token` to publish a camera into the composition over WHIP | | ||
| | `GET /viewer` | `url` and `token` to watch the composed result over WHEP | | ||
| Publish with any WHIP client, such as `useLivestreamStreamer` from the React client SDK, | ||
| and watch with a livestream viewer such as `useLivestreamViewer`. | ||
| Press Ctrl+C to delete the composition and the livestream room. | ||
| To change the scene while the output is running, call `CompositionClient.update_output`. | ||
| An update has to mirror the registration: this output registers both video and audio, so | ||
| an update has to carry both. | ||
| > [!NOTE] | ||
| > A composition holds resources until it is deleted, so let the demo clean up on exit | ||
| > rather than killing it. | ||
| ## Composing a whole room | ||
| This demo composes inputs whose IDs it chooses itself. To compose everyone in a room | ||
| instead, forward the room's tracks with `FishjamClient.forward_room_tracks` and render | ||
| them with a template, since a template decides the layout as peers come and go. Build one | ||
| with `npx @fishjam-cloud/composition-cli build App.tsx --out template.js`, register it | ||
| with `CompositionClient.register_template_output`, and see the | ||
| [JS example](https://github.com/fishjam-cloud/js-server-sdk/tree/main/examples/composition) | ||
| for a template to start from. |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| from contextlib import asynccontextmanager | ||
| from starlette.applications import Starlette | ||
| from starlette.middleware import Middleware | ||
| from starlette.middleware.cors import CORSMiddleware | ||
| from starlette.requests import Request | ||
| from starlette.responses import JSONResponse, Response | ||
| from starlette.routing import Route | ||
| from .composition_service import CompositionService | ||
| from .config import COMPOSITION_URL, FISHJAM_ID, FISHJAM_TOKEN | ||
| from .fishjam_service import FishjamService | ||
| def clean_up(*services) -> None: | ||
| for service in services: | ||
| if service is None: | ||
| continue | ||
| try: | ||
| service.cleanup() | ||
| except Exception as error: | ||
| print(f"cleanup failed: {error}") | ||
| @asynccontextmanager | ||
| async def lifespan(app: Starlette): | ||
| fishjam = composition = None | ||
| try: | ||
| fishjam = FishjamService(FISHJAM_ID, FISHJAM_TOKEN) | ||
| composition = CompositionService(FISHJAM_TOKEN, COMPOSITION_URL) | ||
| composition.register_assets() | ||
| composition.play_movie() | ||
| composition.camera() | ||
| composition.stream_to( | ||
| fishjam.livestream_whip_url(), fishjam.create_streamer_token() | ||
| ) | ||
| except Exception: | ||
| clean_up(composition, fishjam) | ||
| raise | ||
| app.state.fishjam = fishjam | ||
| app.state.composition = composition | ||
| try: | ||
| yield | ||
| finally: | ||
| clean_up(composition, fishjam) | ||
| print("deleted the composition and the livestream room") | ||
| async def streamer(request: Request) -> Response: | ||
| camera = request.app.state.composition.camera() | ||
| return JSONResponse({"url": camera.url, "token": camera.bearer_token}) | ||
| async def viewer(request: Request) -> Response: | ||
| fishjam = request.app.state.fishjam | ||
| return JSONResponse({ | ||
| "url": fishjam.livestream_whep_url(), | ||
| "token": fishjam.create_viewer_token(), | ||
| }) | ||
| app = Starlette( | ||
| lifespan=lifespan, | ||
| routes=[ | ||
| Route("/streamer", streamer, methods=["GET"]), | ||
| Route("/viewer", viewer, methods=["GET"]), | ||
| ], | ||
| middleware=[ | ||
| Middleware( | ||
| CORSMiddleware, | ||
| allow_origins=["*"], | ||
| allow_credentials=True, | ||
| allow_methods=["*"], | ||
| allow_headers=["*"], | ||
| ) | ||
| ], | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import httpx | ||
| from fishjam import CompositionClient, WhipInputTarget | ||
| from fishjam.composition import ( | ||
| AudioScene, | ||
| AudioSceneInput, | ||
| ImageSpecSvg, | ||
| ImageSpecSvgAssetType, | ||
| OutputWhipAudioOptions, | ||
| OutputWhipVideoOptions, | ||
| Resolution, | ||
| ) | ||
| from .config import ( | ||
| CAMERA_INPUT_ID, | ||
| FONT_URL, | ||
| HEIGHT, | ||
| LOGO_IMAGE_ID, | ||
| LOGO_URL, | ||
| MOVIE_INPUT_ID, | ||
| MOVIE_URL, | ||
| OUTPUT_ID, | ||
| WIDTH, | ||
| ) | ||
| from .scene import scene | ||
| class CompositionService: | ||
| def __init__(self, management_token: str, composition_url: str | None = None): | ||
| self.compositions = CompositionClient( | ||
| management_token=management_token, composition_url=composition_url | ||
| ) | ||
| self.composition_id = self.compositions.create_composition().composition_id | ||
| self._camera: WhipInputTarget | None = None | ||
| def register_assets(self) -> None: | ||
| self.compositions.register_font( | ||
| self.composition_id, httpx.get(FONT_URL, follow_redirects=True).content | ||
| ) | ||
| self.compositions.register_image( | ||
| self.composition_id, | ||
| LOGO_IMAGE_ID, | ||
| ImageSpecSvg( | ||
| asset_type=ImageSpecSvgAssetType.SVG, | ||
| url=LOGO_URL, | ||
| resolution=Resolution(width=200, height=200), | ||
| ), | ||
| ) | ||
| def play_movie(self) -> None: | ||
| self.compositions.register_mp4_input( | ||
| self.composition_id, MOVIE_INPUT_ID, url=MOVIE_URL, loop=True | ||
| ) | ||
| def camera(self) -> WhipInputTarget: | ||
| if self._camera is None: | ||
| self._camera = self.compositions.register_whip_input( | ||
| self.composition_id, CAMERA_INPUT_ID, video=True | ||
| ) | ||
| return self._camera | ||
| def stream_to(self, endpoint_url: str, bearer_token: str) -> None: | ||
| self.compositions.register_whip_output( | ||
| self.composition_id, | ||
| OUTPUT_ID, | ||
| endpoint_url=endpoint_url, | ||
| bearer_token=bearer_token, | ||
| video=OutputWhipVideoOptions( | ||
| resolution=Resolution(width=WIDTH, height=HEIGHT), initial=scene() | ||
| ), | ||
| audio=OutputWhipAudioOptions( | ||
| initial=AudioScene( | ||
| inputs=[ | ||
| AudioSceneInput(input_id=CAMERA_INPUT_ID), | ||
| AudioSceneInput(input_id=MOVIE_INPUT_ID, volume=0.2), | ||
| ] | ||
| ) | ||
| ), | ||
| ) | ||
| def cleanup(self) -> None: | ||
| self.compositions.delete_composition(self.composition_id) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import os | ||
| import dotenv | ||
| dotenv.load_dotenv() | ||
| FISHJAM_ID = os.environ["FISHJAM_ID"] | ||
| FISHJAM_TOKEN = os.environ["FISHJAM_MANAGEMENT_TOKEN"] | ||
| COMPOSITION_URL = os.getenv("COMPOSITION_URL") | ||
| HOST = os.getenv("HOST", "localhost") | ||
| PORT = int(os.getenv("PORT", "8000")) | ||
| CAMERA_INPUT_ID = "camera" | ||
| MOVIE_INPUT_ID = "movie" | ||
| LOGO_IMAGE_ID = "fish" | ||
| OUTPUT_ID = "livestream" | ||
| MOVIE_URL = "https://github.com/smelter-labs/smelter-snapshot-tests/raw/refs/heads/main/assets/BigBuckBunny720p24fpsStereo30s.mp4" | ||
| LOGO_URL = "https://fishjam.swmansion.com/favicon.svg" | ||
| FONT_URL = "https://raw.githubusercontent.com/google/fonts/main/ofl/inter/Inter%5Bopsz%2Cwght%5D.ttf" | ||
| FONT_FAMILY = "Inter" | ||
| WIDTH = 1280 | ||
| HEIGHT = 720 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| from fishjam import FishjamClient, RoomOptions | ||
| class FishjamService: | ||
| def __init__(self, fishjam_id: str, management_token: str): | ||
| self.fishjam = FishjamClient(fishjam_id, management_token) | ||
| self.livestream_id = self.fishjam.create_room( | ||
| RoomOptions(room_type="livestream") | ||
| ).id | ||
| def livestream_whip_url(self) -> str: | ||
| return self.fishjam.livestream_whip_url() | ||
| def livestream_whep_url(self) -> str: | ||
| return self.fishjam.livestream_whep_url() | ||
| def create_streamer_token(self) -> str: | ||
| return self.fishjam.create_livestream_streamer_token(self.livestream_id) | ||
| def create_viewer_token(self) -> str: | ||
| return self.fishjam.create_livestream_viewer_token(self.livestream_id) | ||
| def cleanup(self) -> None: | ||
| self.fishjam.delete_room(self.livestream_id) |
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.