Skip to content

Commit 5b2ba1a

Browse files
committed
chore: add recording example and fix log levels
1 parent c83b405 commit 5b2ba1a

12 files changed

Lines changed: 360 additions & 68 deletions

File tree

examples/record_audio/README.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Audio Recording Example
2+
3+
This example demonstrates two types of audio recording with multiple users:
4+
- **Composite Recording**: Records all users mixed into a single audio file
5+
- **Track Recording**: Records individual audio tracks from specific users
6+
7+
## Setup
8+
9+
### Prerequisites
10+
11+
1. **Stream Account**: Get your API key and secret from [Stream Dashboard](https://dashboard.getstream.io/)
12+
2. **Audio Files**: Prepare 3 audio files (WAV, MP3, MP4, etc.)
13+
3. **Environment Setup**: Create a `.env` file in the `examples/` directory
14+
15+
### Environment Variables
16+
17+
Create `examples/.env`:
18+
19+
```bash
20+
STREAM_API_KEY=your_stream_api_key_here
21+
STREAM_API_SECRET=your_stream_api_secret_here
22+
```
23+
24+
### Audio Files
25+
26+
Update the `AUDIO_FILES` list in `main.py` with paths to your actual audio files:
27+
28+
```python
29+
AUDIO_FILES = [
30+
"/path/to/your/audio1.wav", # Bot Alice will play this
31+
"/path/to/your/audio2.wav", # Bot Bob will play this
32+
"/path/to/your/audio3.wav", # Bot Charlie will play this
33+
]
34+
```
35+
36+
## Usage
37+
38+
Run the example with one of two recording types:
39+
40+
```bash
41+
# For composite recording (all audio mixed into one file)
42+
python main.py --type composite
43+
44+
# For track recording (individual audio tracks)
45+
python main.py --type track
46+
```
47+
48+
The recordings will be saved in:
49+
- Composite recordings: `recordings/composite/`
50+
- Track recordings: `recordings/tracks/`
51+
52+
## How It Works
53+
54+
1. Creates three bot users (Alice, Bob, and Charlie) with different audio files
55+
2. Creates a recorder bot to handle the recording
56+
3. Joins all bots to the call and adds their audio tracks
57+
4. Records for 20 seconds
58+
5. Saves the recordings to the specified output directory
59+
6. Cleans up by removing all created users
60+
61+
## Code Example
62+
63+
### Recording API
64+
65+
The `start_recording` method accepts the following parameters:
66+
- `recording_types`: List of recording types (COMPOSITE or TRACK)
67+
- `output_dir`: Directory where recordings will be saved
68+
- `user_id_filter` (optional): For track recording, specify which user's audio to record
69+
70+
```python
71+
# Start recording with specified type
72+
await connection.start_recording(
73+
recording_types=[RecordingType.COMPOSITE], # or RecordingType.TRACK
74+
output_dir="recordings/composite" # or "recordings/tracks"
75+
)
76+
77+
# Record for 20 seconds
78+
await asyncio.sleep(20)
79+
80+
# Stop recording
81+
await connection.stop_recording()
82+
```
83+
84+
### Bot Audio with MediaPlayer
85+
86+
The example uses `aiortc.contrib.media.MediaPlayer` to play audio files for the bots:
87+
88+
```python
89+
# Create a media player from an audio file
90+
player = MediaPlayer(audio_file, loop=True)
91+
92+
# Add the audio track to the call
93+
if player.audio:
94+
await connection.add_tracks(audio=player.audio)
95+
```
96+
97+
The `MediaPlayer` handles:
98+
- Loading audio files (WAV, MP3, MP4, etc.)
99+
- Continuous playback (with `loop=True`)
100+
- Converting audio to the correct format for WebRTC
101+
- Providing the audio track that can be added to the call
102+

examples/record_audio/main.py

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
#!/usr/bin/env python3
2+
3+
import asyncio
4+
import os
5+
import logging
6+
import argparse
7+
from uuid import uuid4
8+
from dotenv import load_dotenv
9+
from aiortc.contrib.media import MediaPlayer
10+
11+
from examples.utils import create_user
12+
from getstream.stream import Stream
13+
from getstream.video import rtc
14+
from getstream.video.rtc.recording import RecordingType
15+
16+
# Configure logging for the Stream SDK
17+
logging.basicConfig(level=logging.WARN)
18+
19+
# Sample audio files - update these paths to your actual audio files
20+
AUDIO_FILES = [
21+
"/path/to/audio/input1.mp3",
22+
"/path/to/audio/input2.mp3",
23+
"/path/to/audio/input3.mp3",
24+
]
25+
26+
27+
async def create_bot_with_audio(client, bot_name: str, audio_file: str):
28+
"""Create a bot user and prepare its audio player."""
29+
if not os.path.exists(audio_file):
30+
print(f"Audio file not found: {audio_file}, skipping bot {bot_name}")
31+
return None
32+
33+
# Create bot user
34+
bot_user_id = f"bot-{bot_name.lower()}-{str(uuid4())[:8]}"
35+
36+
# Create the user in Stream first using the utility function
37+
create_user(client, bot_user_id, f"Bot {bot_name}")
38+
39+
try:
40+
# Create media player from audio file
41+
player = MediaPlayer(audio_file, loop=True)
42+
43+
if player.audio:
44+
print(f"Bot {bot_name} created with audio: {os.path.basename(audio_file)}")
45+
return bot_user_id, player
46+
47+
print(f"No audio track found in {audio_file}")
48+
return None
49+
50+
except Exception as e:
51+
print(f"Error creating bot {bot_name}: {e}")
52+
return None
53+
54+
55+
async def print_recording_status(connection):
56+
"""Print the current recording status."""
57+
status = connection.get_recording_status()
58+
print(f"Recording Status:")
59+
print(f" • Recording Active: {status['recording_enabled']}")
60+
print(f" • Recording Types: {status['recording_types']}")
61+
print(f" • Output Directory: {status['output_directory']}")
62+
if 'user_id_filter' in status:
63+
print(f" • User Filter: {status['user_id_filter']}")
64+
65+
async def record(connection, recording_type, output_dir, user_id_filter=None):
66+
"""Perform recording with the specified parameters."""
67+
print(f"\nStarting {recording_type} recording...")
68+
69+
# Start recording with specified parameters
70+
recording_params = {
71+
"recording_types": [recording_type],
72+
"output_dir": output_dir
73+
}
74+
if user_id_filter:
75+
recording_params["user_ids"] = user_id_filter
76+
77+
await connection.start_recording(**recording_params)
78+
79+
# Show recording status
80+
await print_recording_status(connection)
81+
82+
# Record for 20 seconds
83+
print(f"\nRecording for 20 seconds...")
84+
await asyncio.sleep(20)
85+
86+
# Stop recording
87+
print(f"\nStopping recording...")
88+
await connection.stop_recording()
89+
90+
# Show final results
91+
final_status = connection.get_recording_status()
92+
print(f"\nRecording Complete!")
93+
print(f" • Output Directory: {final_status['output_directory']}")
94+
95+
async def record_composite(call, recorder_connection, bot_connections, players):
96+
"""Record composite audio from all participants."""
97+
await record(
98+
recorder_connection,
99+
RecordingType.COMPOSITE,
100+
"recordings/composite"
101+
)
102+
103+
async def record_tracks(call, recorder_connection, bot_connections, players):
104+
"""Record individual tracks from each participant."""
105+
await record(
106+
recorder_connection,
107+
RecordingType.TRACK,
108+
"recordings/tracks",
109+
user_id_filter=bot_connections[0].user_id # Record only the first bot's audio
110+
)
111+
112+
async def setup_bots(client, bot_names, audio_files):
113+
"""Set up bot users with audio files."""
114+
players = []
115+
bot_user_ids = []
116+
117+
print("\nCreating bot users with audio files...")
118+
119+
# Create all users first
120+
for bot_name, audio_file in zip(bot_names, audio_files):
121+
result = await create_bot_with_audio(client, bot_name, audio_file)
122+
if result:
123+
bot_user_id, player = result
124+
bot_user_ids.append(bot_user_id)
125+
players.append(player)
126+
127+
if not bot_user_ids:
128+
print("No bots could be created. Please check your audio file paths.")
129+
return None, None
130+
131+
print(f"Created {len(bot_user_ids)} bot users")
132+
return bot_user_ids, players
133+
134+
async def main():
135+
"""Main recording example with multiple audio file inputs."""
136+
# Parse command line arguments
137+
parser = argparse.ArgumentParser(description='Stream Video Recording Example')
138+
parser.add_argument('--type', choices=['composite', 'track'], required=True,
139+
help='Type of recording to perform (composite or track)')
140+
args = parser.parse_args()
141+
142+
# Load environment variables
143+
load_dotenv()
144+
145+
print(f"Audio Recording Example - {args.type.title()} Recording")
146+
print("=" * 50)
147+
148+
# Initialize Stream client
149+
client = Stream.from_env()
150+
151+
# Create a unique call
152+
call_id = f"recording-example-{str(uuid4())}"
153+
call = client.video.call("default", call_id)
154+
print(f"Call ID: {call_id}")
155+
156+
# Create the call
157+
call.get_or_create(data={"created_by_id": "recording-example"})
158+
159+
try:
160+
# Set up bots
161+
bot_names = ["Alice", "Bob", "Charlie"]
162+
bot_user_ids, players = await setup_bots(client, bot_names, AUDIO_FILES)
163+
if not bot_user_ids:
164+
return
165+
166+
# Create recorder user
167+
recorder_user_id = f"bot-recorder-{str(uuid4())[:8]}"
168+
create_user(client, recorder_user_id, "Recording Bot")
169+
170+
# Join all bots first and add their tracks
171+
async with (
172+
await rtc.join(call, bot_user_ids[0]) as bot1_connection,
173+
await rtc.join(call, bot_user_ids[1]) as bot2_connection,
174+
await rtc.join(call, bot_user_ids[2]) as bot3_connection,
175+
):
176+
# Add all audio tracks
177+
for connection, player in zip([bot1_connection, bot2_connection, bot3_connection], players):
178+
if player.audio:
179+
await connection.add_tracks(audio=player.audio)
180+
181+
# Now join with recorder and start recording
182+
async with await rtc.join(call, recorder_user_id) as recorder_connection:
183+
print(f"Recorder joined: {recorder_user_id}")
184+
185+
# Run the selected recording type
186+
if args.type == 'composite':
187+
await record_composite(call, recorder_connection,
188+
[bot1_connection, bot2_connection, bot3_connection],
189+
players)
190+
else: # track recording
191+
await record_tracks(call, recorder_connection,
192+
[bot1_connection, bot2_connection, bot3_connection],
193+
players)
194+
195+
for connection in [bot1_connection, bot2_connection, bot3_connection]:
196+
await connection.leave()
197+
await asyncio.sleep(1.0)
198+
except Exception as e:
199+
print(f"Error during recording: {e}")
200+
finally:
201+
# Delete created users
202+
print("Deleting created users...")
203+
try:
204+
# Delete bot users
205+
client.delete_users(bot_user_ids + [recorder_user_id])
206+
print(f"Deleted bot users")
207+
except Exception as e:
208+
print(f"Error during user cleanup: {e}")
209+
210+
print("Done")
211+
212+
213+
if __name__ == "__main__":
214+
print("Before running this example:")
215+
print(" 1. Update the AUDIO_FILES list with paths to your actual audio files")
216+
print(" 2. Make sure you have a .env file with STREAM_API_KEY and STREAM_API_SECRET")
217+
print(" 3. Audio files should be in a format supported by aiortc (WAV, MP3, MP4, etc.)")
218+
print(" 4. Run with --type composite or --type track to specify recording type")
219+
print()
220+
221+
asyncio.run(main())
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "record-audio"
7+
version = "0.1.0"
8+
description = "Recording Audio with Stream"
9+
readme = "README.md"
10+
requires-python = ">=3.10"
11+
dependencies = [
12+
"getstream[webrtc]",
13+
"python-dotenv",
14+
"aiortc",
15+
"numpy",
16+
]
17+
18+
[project.scripts]
19+
record-audio = "main:main"
20+
21+
[tool.hatch.build.targets.wheel]
22+
packages = ["main.py"]

examples/record_audio_mixed/pyproject.toml

Lines changed: 0 additions & 28 deletions
This file was deleted.

examples/record_audio_single_tracks/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)