# RTMP CLI
- [中文版](./README_CN.md)
- Complex Example: ⭐⭐⭐
## Example Brief
This example provides five RTMP modes in one firmware and controls them through a serial CLI, allowing users to publish, receive, relay, and loop back streams, then switch modes after stopping the active session without rebuilding or reflashing:
- **push** — publish live camera video and microphone audio to MediaMTX, nginx-rtmp, YouTube, Twitch, or another RTMP server
- **pull** — play a remote RTMP stream on the board's LCD and speaker
- **server** — receive a stream published by `ffmpeg` and relay it to multiple players
- **live** — let network players pull the board's live camera and microphone stream directly from the board
- **loopback** — run server, push, and pull together to exercise the complete RTMP path on the board without a PC
The **server** mode only relays media, so **live** combines server and push to expose the board's camera stream, while **loopback** also adds pull; three independent slots allow these services to run together.
All modes use the same service model: the application creates capture, playback, and RTMP services as needed, links media producers to consumers with `esp_media_service_link()`, and controls their lifecycle through common APIs instead of implementing RTMP protocol handling and media data flow; the serial CLI also provides a command surface for scripts and agent-driven automation.
### Typical Use Cases
- Live-streaming a camera to a CDN ingest endpoint such as YouTube or Twitch
- Video doorbells and surveillance nodes that publish to a central media server
- An all-in-one streaming box that players pull from directly, with no server to operate
- Network broadcast displays that play a remote RTMP stream on a local LCD
### Runtime Flow
1. `esp_board_manager` initializes the camera, LCD, audio ADC, and audio DAC
2. `esp_wifi_service` connects the station
3. `esp_cli_service` starts the `rtmp>` console
4. **push**: `esp_video_capture_service` → `esp_media_service_link()` → `esp_rtmp_service` ROLE_SINK
5. **server**: `esp_rtmp_service` ROLE_SERVER relays only; there is no media link
6. **pull**: ROLE_SRC → `esp_media_service_link()` → `esp_video_player_service` → LCD and speaker
The consuming side of a link is always started first. On teardown the order is reversed inside each slot, and the client slots are released before the server.
### File Structure
```
rtmp_cli/
├── main/
│ ├── app_main.c Application entry: NVS, board, codecs, Wi-Fi, CLI
│ ├── rtmp_example.h Board and Wi-Fi helpers
│ ├── rtmp_session.c/h Three slots: create, setup, link, start, and stop
│ ├── rtmp_cli.c/h `rtmp` and `wifi` command parsing
│ ├── rtmp_settings.h Media and protocol defaults
│ ├── Kconfig.projbuild Wi-Fi credentials and default peer URLs
│ └── idf_component.yml
├── sdkconfig.defaults
├── sdkconfig.defaults.esp32p4
├── sdkconfig.defaults.esp32s3
├── sdkconfig.defaults.esp32s31
├── sdkconfig.ci
├── partitions.csv
├── pytest_rtmp_cli.py
├── README.md
└── README_CN.md
```
## Environment Setup
### Hardware Required
- ESP32-P4 Function EV Board (camera, MIPI LCD, audio codec)
- A PC on the same network for ffmpeg, ffplay, or an RTMP server
- For chips without on-chip Wi-Fi, the board must provide available network connectivity
The camera and the LCD are both optional. Without a camera the push slot still works with `-v none`, and without a display the pull slot plays audio only. `rtmp loopback -v none` needs neither.
### Default IDF Branch
This example supports IDF release/v5.4 (>= v5.4.3), release/v5.5 (>= v5.5.2), and IDF v6.1.
### Software Requirements
- Install FFmpeg, which provides both `ffmpeg` and `ffplay`
- Configure Wi-Fi SSID and password in menuconfig
- To play what the board serves (`server`, `live`): `ffplay rtmp://<device-ip>:1935/live/stream`
- To publish into the board (`server`): `ffmpeg` with `-f flv rtmp://<device-ip>:1935/live/stream`
- To receive a push or serve a pull (`push`, `pull`), run an RTMP server on the PC. [MediaMTX](https://github.com/bluenviron/mediamtx) needs no configuration for this example:
```
./mediamtx
# RTMP is served on rtmp://<pc-ip>:1935/<path>
```
## Build and Flash
### Build Preparation
Before building, ensure the ESP-IDF environment is set up. If not, run in the ESP-IDF root directory:
```
./install.sh
. ./export.sh
```
Go to this example directory:
```
cd adf_examples/protocols/rtmp_cli
```
This example uses [ESP Board Manager](https://github.com/espressif/esp-board-manager) for the camera, LCD, audio codec, and other board peripherals. The [`esp-bmgr-assist`](https://pypi.org/project/esp-bmgr-assist/) helper tool is recommended as the default entry point.
Install it in the activated ESP-IDF Python environment (only needed once per environment):
```bash
pip install esp-bmgr-assist
pip install --upgrade esp-bmgr-assist # run this command when an update is requested
```
List the currently visible boards:
```bash
idf.py bmgr -l
```
Example output:
```text
ℹ️ Board Components:
espressif/esp_boards:
[1] esp32_c3_lyra
[2] esp32_lyrat_4_3
[3] esp32_lyrat_mini_1_1
[4] esp32_p4_eye
[5] esp32_p4_function_ev_board
[6] esp32_s31_function_coreboard_1
[7] esp32_s31_korvo_1
[8] esp32_s3_box_3
[9] esp32_s3_box_lite
[10] esp32_s3_korvo_2_3
[11] esp32_s3_lcd_ev_board
[12] esp_vocat_1_0
[13] esp_vocat_1_2
```
The example output above is based on the board list and ordering from `esp_boards` 0.5.2. Different `esp_boards` versions or custom board dependencies may change the list and indexes. Use the actual output of `idf.py bmgr -l` when selecting a board.
Select a board:
```bash
idf.py bmgr -b <board_index|board_name>
```
For example, to select `esp32_p4_function_ev_board`:
```bash
idf.py bmgr -b 5
# or
idf.py bmgr -b esp32_p4_function_ev_board
```
On first invocation of `idf.py bmgr`, the component is downloaded automatically based on the `espressif/esp_board_manager` dependency declared in `main/idf_component.yml`.
> [!NOTE]
> To switch to a different board supported by `esp_board_manager`, repeat the same steps with the new board name or index. Use `idf.py fullclean` before rebuilding if needed.
> The selected board should provide `camera`, `display_lcd`, `audio_adc` and `audio_dac` to exercise every slot.
> For a custom board, see [Creating a Board Guide](https://docs.espressif.com/projects/esp-board-manager/en/latest/create-board/index.html).
> For more information about `esp_board_manager`, see the [ESP Board Manager Getting Started Guide](https://github.com/espressif/esp-board-manager/blob/main/esp_board_manager/README.md).
### Project Configuration
Default options are in `sdkconfig.defaults` and `sdkconfig.defaults.<target>`. Media defaults live in `main/rtmp_settings.h`. You usually only need to set Wi-Fi:
```bash
idf.py menuconfig
```
Configure:
- **RTMP CLI Example Configuration** → **WiFi SSID**
- **RTMP CLI Example Configuration** → **WiFi Password**
- **RTMP CLI Example Configuration** → **Default push URL** / **Default pull URL** (used when `rtmp push` / `rtmp pull` are given no URL)
> For CI, use `${CI_WIFI_SSID}` / `${CI_WIFI_PASSWORD}` in `sdkconfig.ci`. Do not commit Wi-Fi passwords in `sdkconfig.defaults`; set the password locally in menuconfig.
Common tunables in `main/rtmp_settings.h`:
- `RTMP_VIDEO_WIDTH`, `RTMP_VIDEO_HEIGHT`, `RTMP_VIDEO_FPS`, `RTMP_VIDEO_CODEC`
- `RTMP_AUDIO_CODEC`, `RTMP_AUDIO_SAMPLE_RATE`, `RTMP_AUDIO_BITRATE`
- `RTMP_SERVER_PORT`, `RTMP_SERVER_APP`, `RTMP_SERVER_STREAM`, `RTMP_SERVER_MAX_CLIENTS`
- `RTMP_CHUNK_SIZE`, `RTMP_RECV_AUDIO_CACHE`, `RTMP_RECV_VIDEO_CACHE`
Every media default can also be overridden per session from the command line.
Press `s` to save and `Esc` to exit menuconfig.
### Resource Optimization
The default configuration keeps every RTMP role and codec choice used by the five CLI modes; a fixed-function product can reduce firmware size in menuconfig:
- Open **Component config → ESP-RTMP Service** and use `CONFIG_ESP_RTMP_SERVICE_SINK_SUPPORT` for push, `CONFIG_ESP_RTMP_SERVICE_SRC_SUPPORT` for pull, and `CONFIG_ESP_RTMP_SERVICE_SERVER_SUPPORT` for server; live needs `SERVER` and `SINK`, while loopback needs all three roles.
- Open **Component config → Audio Codec Configuration** and **Component config → Video Codec Configuration** to toggle the `CONFIG_AUDIO_ENCODER_*_SUPPORT`, `CONFIG_AUDIO_DECODER_*_SUPPORT`, `CONFIG_VIDEO_ENCODER_*_SUPPORT`, and `CONFIG_VIDEO_DECODER_*_SUPPORT` options; push and live need encoders, pull needs decoders, a relay-only server needs neither, and loopback needs both, so keep only the formats in use and one target-appropriate implementation for each video codec.
For example, a fixed H264/AAC publisher normally keeps only the RTMP `SINK` role and the H264/AAC encoders. Disabling a role or codec makes its CLI mode or format unavailable; run `idf.py fullclean` before rebuilding after changing these options.
At runtime, `rtmp stop` destroys the active RTMP, capture, and player services, while the board devices, codec registrations, and Wi-Fi remain initialized so another supported mode can start without rebooting.
### Build and Flash Commands
```
idf.py build
idf.py -p PORT flash monitor
```
Exit the monitor with `Ctrl-]`. The CLI prompt is `rtmp>`; type `help` for commands.
## How to Use the Example
### Functionality and Usage
After startup, the example initializes peripherals, connects to Wi-Fi, and starts the CLI. If the credentials in menuconfig are wrong or missing, connect at runtime with `wifi <ssid> <password>`.
| Command | Description |
|---------|-------------|
| `rtmp server [options]` | Start the relay server on `rtmp://<device-ip>:<port>/<app>` |
| `rtmp push [url] [options]` | Publish the camera and microphone to an RTMP URL |
| `rtmp pull [url] [options]` | Play an RTMP stream on the LCD and speaker |
| `rtmp live [options]` | Server plus publisher, so players pull the board's camera from the board |
| `rtmp loopback [options]` | Server plus publisher plus player, the whole chain on the board alone |
| `rtmp stop` | Stop and destroy every active RTMP, capture, and playback slot |
| `rtmp info` | Print the active slots, their URLs, and the server's client and session table |
| `wifi [ssid] [password]` | Connect to Wi-Fi; omit both arguments to reuse the menuconfig values |
Omitting the URL on `rtmp push` and `rtmp pull` uses `RTMP_EXAMPLE_PUSH_URL` and `RTMP_EXAMPLE_PULL_URL`.
| Option | Slots | Description |
|--------|-------|-------------|
| `-p <port>` | server, live, loopback | Listen port, default `1935` |
| `--app <name>` | server, live, loopback | RTMP application name, default `live` |
| `--stream <name>` | server, live, loopback | Stream name, default `stream` |
| `--max-clients <n>` | server, live, loopback | Server client limit, default `4` |
| `-v h264\|mjpeg\|none` | push, live, loopback | Video codec; `h264` on push/live, `mjpeg` on loopback; `none` for audio-only |
| `-a aac\|pcm\|g711a\|g711u\|none` | push, live, loopback | Audio codec, default `aac`; `none` for a video-only stream |
| `--res <WxH>` | push, live, loopback | Capture resolution, for example `--res 1280x720` |
| `--fps <n>` | push, live, loopback | Capture frame-rate |
| `--bitrate <bps>` | push, live, loopback | Video bitrate; omit it to derive one from the frame size |
| `--no-video` / `--no-audio` | push, live, loopback | Shorthand for `-v none` / `-a none` |
| `--chunk <bytes>` | all | RTMP chunk size, default `4096` |
| `--cache <bytes>` | pull | Video receive cache size |
| `--insecure` | push, pull | Accept an `rtmps://` server without verifying its certificate |
Selecting `g711a` or `g711u` pins the audio to 8 kHz mono, because G.711 is defined only at that rate.
Automated tests (`pytest_rtmp_cli.py`) run `rtmp loopback -v none` after the CLI is ready and expect `PREPARING -> PLAYING`.
**Serve the camera to a PC player** with `rtmp live`, then on the PC:
```
rtmp> rtmp live
```
```
ffplay rtmp://192.168.1.23:1935/live/stream
```
Other combinations:
```
rtmp> rtmp live -v h264 --res 1280x720 --fps 15 --bitrate 2000000
rtmp> rtmp live -v none # audio-only stream
rtmp> rtmp live -a none -p 8935 # video-only stream on port 8935
rtmp> rtmp live --app app1 --stream cam0 # rtmp://<device-ip>:1935/app1/cam0
```
**Publish to a remote RTMP server.** Start MediaMTX on the PC, then:
```
rtmp> rtmp stop
rtmp> rtmp push rtmp://192.168.1.10:1935/live/stream
```
```
ffplay rtmp://192.168.1.10:1935/live/stream
```
A CDN ingest endpoint works the same way; the stream key is the last path element:
```
rtmp> rtmp push rtmp://a.rtmp.youtube.com/live2/<your-stream-key>
```
For `rtmps://` endpoints the certificate is checked against the IDF certificate bundle. Add `--insecure` to accept a self-signed test server:
```
rtmp> rtmp push rtmps://192.168.1.10:1936/live/stream --insecure
```
**Play a remote stream:**
```
rtmp> rtmp stop
rtmp> rtmp pull rtmp://192.168.1.10:1935/live/stream
```
Codecs come from the stream metadata, so no codec option is needed on this side. Use `--cache` to widen the receive buffer on a lossy network.
**Act as the relay server only.** The board carries no media of its own. Publish from the PC:
```
ffmpeg -re -f lavfi -i testsrc=size=640x480:rate=15 -f lavfi -i sine -c:v libx264 -preset ultrafast -tune zerolatency -c:a aac -f flv rtmp://192.168.1.23:1935/live/stream
```
```
ffplay rtmp://192.168.1.23:1935/live/stream
```
**Run the whole chain on the board** with `rtmp loopback`. Camera frames travel capture → SINK → local server → SRC → LCD. `rtmp loopback -v none` runs on a board with no camera and no display.
`rtmp loopback` defaults to MJPEG so the on-board LCD can keep up: ESP32-P4 decodes JPEG in hardware but H264 in software. `rtmp live` and `rtmp push` stay on H264 so ffplay can read them. Pass `-v h264` on loopback only when a PC will also pull the same stream.
The three slots are independent. Each start restarts only its own slot, so a publisher can be repointed without dropping a running server:
```
rtmp> rtmp server
rtmp> rtmp push rtmp://127.0.0.1:1935/live/stream # equivalent to 'rtmp live'
rtmp> rtmp push rtmp://192.168.1.10:1935/live/stream # repoints, server keeps running
rtmp> rtmp stop # releases everything
```
Only one instance of each role exists at a time. When a peer has already closed the connection, `rtmp info` marks the slot `(peer left)` and the next start on that slot releases it automatically.
### Log Output
The startup sequence is numbered `[ 1 ]` to `[ 5 ]` and ends with `CLI ready`. During a session every RTMP event is logged with the slot that raised it:
- `push: PEER_CLOSED` or `pull: PEER_CLOSED` — the remote side dropped the connection
- `server: SERVER_CLIENT_CONNECTED` — a client reached the relay server
- `server: SERVER_PULLER_STARTED` / `SERVER_PULLER_STOPPED` — raised only when an in-process local pusher is registered through `esp_rtmp_server_monitor_puller()`. Clients that connect over a socket, including this example's publisher at `127.0.0.1`, do not raise them. Use `SERVER_CLIENT_CONNECTED` and `rtmp info` instead
The pull slot starts the player first, then the source. Audio-only streams such as `rtmp loopback -v none` go straight to `PREPARING -> PLAYING`. An A/V stream may also log `PLAYER_SERVICE: Stream 0 took a late video track, restarting the feed session` when the video header arrives after audio; that is expected. A short burst of `H264_DEC` errors at that moment means the player joined mid-GOP.
These two fragments are from one `rtmp loopback -v none` run on ESP32-P4 Function EV. The first is the command starting the relay. The second is the player leaving `PREPARING` after audio frames arrived. The `ffmpeg ...` in the first fragment is the literal string the firmware prints.
```text
rtmp loopback -v none
I (45589) ADF_EVENT_HUB: Create 'server': domain registered
I (45589) ESP_SERVICE: [server] Initialized
I (45590) ESP_SERVICE: [server] Started
I (45591) RTMP_SESSION: Local RTMP server listening on port 1935, app 'live', up to 4 clients
I (45591) RTMP_SESSION: Publish into it with: ffmpeg ... -f flv rtmp://192.168.3.101:1935/live/stream
I (45592) RTMP_SESSION: Play from it with: ffplay rtmp://192.168.3.101:1935/live/stream
```
```text
I (47574) RTMP_SESSION: Playing rtmp://127.0.0.1:1935/live/stream
I (47574) RTMP_SESSION: Loopback running: camera -> SINK -> local server -> SRC -> LCD
rtmp>
rtmp> I (47584) RTMP_SERVER: Add client 0x48263ab4 count 2
I (47584) RTMP_SERVER: Start close client 0x4827b79c
I (47613) ESP_PLAYER: Set av_mask: 3
I (47613) ESP_PLAYER: Set sync_mode: 1
I (47613) ESP_PLAYER: Set av_mask: 1
I (47614) ESP_PLAYER: set dec cfg, type: 541278529, line: 129
I (47614) ESP_PLAYER_STATE: Handling cmd: PREPARE in state: IDLE
I (47614) ESP_PLAYER_STATE: State transition: IDLE -> PREPARING
I (47615) ESP_PLAYER_STATE: Entering PREPARING state
I (47616) ESP_PLAYER_STATE: Audio decoder started, waiting for ready event
W (47617) ESP_GMF_ASMP_DEC: Not enough memory for out, need:4096, old: 1024, new: 4096
I (47619) ESP_PLAYER_STATE: Handling cmd: REPORT_AUDIO_INFO in state: PREPARING
I (47624) ESP_PLAYER_AUDIO_RENDER: Audio render opened successfully
I (47625) ESP_PLAYER_STATE: Handling cmd: PLAYING in state: PREPARING
I (47625) ESP_PLAYER_STATE: State transition: PREPARING -> PLAYING
I (47625) ESP_PLAYER_STATE: Entering PLAYING state, old_state: PREPARING
```
`PREPARING -> PLAYING` is the proof that data crossed capture, publisher, server and player. `rtmp stop` ends with `All RTMP slots released`.
## Troubleshooting
- `rtmp push failed: ESP_ERR_NOT_FOUND` or `V4L2_SRC: Fail to open device`: the camera is not ready. Check the cable and board selection, or publish audio only with `-v none`.
- `rtmp push` fails to connect: the client URL must include a stream name, as in `rtmp://host:port/app/stream`; a server URL stops at the application: `rtmp://host:port/app`.
- ffplay reports `Connection refused`: no server is listening. Run `rtmp server` or `rtmp live`, and use the address printed by `rtmp info`.
- ffplay connects but shows no picture, or reports an unknown codec: the stream is MJPEG. Restart with the default `-v h264`. MJPEG over RTMP is an Espressif extension; third-party players cannot decode it.
- `rtmp push rtmps://...` fails in the handshake: retry with `--insecure` for a self-signed test server.
- The server role has no authentication. RTMPS is available to the client roles only.
The example enables `CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY` so `--insecure` can skip certificate checks; use only in demo environments.
## Technical Support
- Technical support: [esp32.com](https://esp32.com/viewforum.php?f=20) forum
- Issues: [esp-adf issues](https://github.com/espressif/esp-adf/issues)
We will get back to you as soon as possible.
To create a project from this example, run:
idf.py create-project-from-example "espressif/adf_examples=0.2.6:rtmp_cli"