RTSP Simulator is an edge container service that turns an uploaded video file into a live RTSP stream. Use it to build and test inference pipelines on a device before a physical camera is installed, or to replay a known clip against a Workflow.
You upload an .mp4 file through the web interface or the REST API, the service transcodes it to an RTSP-compatible format, and it publishes the result on a continuous loop until you stop it.
RTSP Simulator is available exclusively for Enterprise customers. Contact the Roboflow sales team to learn more.
Connection Details
Replace <device-ip> with the IP address shown on the device page in Deployment Manager.
| Purpose | Address |
|---|---|
| RTSP stream | rtsp:// |
| Web UI, REST API, and Swagger docs | http:// |
| Port | Protocol | Purpose |
|---|---|---|
| 8080 | HTTP | Web UI, REST API, Swagger docs at /docs |
| 8554 | RTSP (TCP) | Video stream output. UDP transport is disabled; clients that prefer UDP negotiate down to TCP-interleaved. |
Multiple clients can read the stream at the same time.
Video Format
Only .mp4 files are accepted. Uploads are transcoded automatically so the stream works with any RTSP client:
- Codec: H.264, Baseline profile
- Pixel format: yuv420p
- Audio: stripped, video only
- Maximum file size: 5 GB by default
Using the Web Interface
Open http://<device-ip>:8080 in a browser.
Upload
Drag an .mp4 file onto the page. The file uploads and is converted to the streaming format.
Play
Select "Play" to start streaming. The video loops indefinitely and is published to the RTSP server.
Connect
Point your inference pipeline, a Deployment Manager stream, or any RTSP client at rtsp://<device-ip>:8554/stream.
Stop
Select "Stop" to end the stream and remove the converted files.
To use the simulated stream in a deployment, add it as a stream source on the device the same way you would a camera. See Add a Stream.
Using the CLI
For headless or SSH-only access, the rtsp-cli tool provides an interactive terminal interface on the device.
docker exec -it rtsp-simulator rtsp-cliTo import a video without the web UI, copy it into the input directory first, then launch the CLI and press I.
scp video.mp4 user@<device-ip>:/var/lib/rfdm/rtsp-simulator/data/input/| Key | Command | Description |
|---|---|---|
I | Import | Select and convert a video from /data/input/ |
P | Play | Start RTSP streaming |
S | Stop | Stop streaming and delete converted files |
D | Delete | Delete video files |
C | Config | Adjust stream settings (bitrate, buffer, delay) |
R | Refresh | Refresh the status display |
Q | Quit | Exit the CLI |
Testing the Stream
Connect with any RTSP player to confirm the stream is live.
vlc rtsp://<device-ip>:8554/streamThe stream is served over TCP only, so force TCP transport rather than letting ffplay try UDP first.
ffplay -rtsp_transport tcp rtsp://<device-ip>:8554/streamHTTP API
All endpoints return JSON. Interactive Swagger documentation is served at http://<device-ip>:8080/docs. See Services for the rules shared by all on-device service APIs.
The API is unauthenticated, and it can stop a running stream and delete uploaded video. Anything that can reach port 8080 can do both.
Typical Flow
curl -X POST http://<device-ip>:8080/upload -F "file=@video.mp4"
curl -X POST http://<device-ip>:8080/play
curl http://<device-ip>:8080/status
curl -X POST http://<device-ip>:8080/stopThe rtsp_url returned by /play and /status always names localhost, because the service reports the URL from its own point of view. Only a consumer running on the device can use it verbatim. Anywhere else, substitute the device address: rtsp://<device-ip>:8554/stream.
The port is RTSP_PORT, which defaults to 8554 but is configurable per device. Read rtsp_port from /info rather than assuming the default.
/stop also deletes both the original and converted files to free disk space, so a new upload is required before the next /play.
Upload
Only .mp4 is accepted, and an active stream is stopped before the upload begins. The optional settings part is a JSON string of conversion settings applied during transcoding. Because they are baked into the converted file, changing them later requires re-uploading.
curl -X POST http://<device-ip>:8080/upload \
-F "file=@video.mp4" \
-F 'settings={"target_fps": 30, "crf_quality": 23, "max_width": 1920}'Upload a video file
Uploads a video and converts it to the RTSP streaming format. Only .mp4 is accepted. An active stream is stopped before the upload begins.
The optional settings part is a JSON string of conversion settings, applied during transcoding. Because they are baked into the converted file, changing them later requires re-uploading.
Video file to upload. Must be .mp4.
Optional JSON string holding an UploadSettings object. It is a string part rather than a nested object because the service parses it with json.loads before validating, so the UploadSettings schema below documents the shape rather than being referenced here.
{"target_fps": 30, "crf_quality": 23, "max_width": 1920}200Upload and conversion succeededapplication/json
Show propertiesHide properties
Original filename
File size in bytes
File size in megabytes
File extension
400No file selected, unsupported extension, invalid upload settings, or the file is not a readable videoapplication/json
Error message
413File exceeds MAX_FILE_SIZE (5 GB by default)application/json
Error message
422Request failed validationapplication/json
One entry in a request-validation failure.
Show propertiesHide properties
Path to the offending field, for example ["body", "value"].
500Conversion failedapplication/json
Error message
507Not enough free disk space for the uploadapplication/json
Error message
POST /upload HTTP/1.1
Host: device-ip:8080
Content-Type: multipart/form-data
Accept: application/json
{
"file": "binary",
"settings": "{\"target_fps\": 30, \"crf_quality\": 23, \"max_width\": 1920}"
}curl -L \
--request POST \
--url 'http://device-ip:8080/upload' \
--header 'Content-Type: multipart/form-data' \
--header 'Accept: application/json' \
--data '{
"file": "binary",
"settings": "{\"target_fps\": 30, \"crf_quality\": 23, \"max_width\": 1920}"
}'const response = await fetch("http://device-ip:8080/upload", {
method: "POST",
headers: {
"Content-Type": "multipart/form-data",
"Accept": "application/json"
},
body: JSON.stringify({
"file": "binary",
"settings": "{\"target_fps\": 30, \"crf_quality\": 23, \"max_width\": 1920}"
})
});
const data = await response.json();
console.log(data);import requests
url = "http://device-ip:8080/upload"
headers = {
"Content-Type": "multipart/form-data",
"Accept": "application/json"
}
payload = {
"file": "binary",
"settings": "{\"target_fps\": 30, \"crf_quality\": 23, \"max_width\": 1920}"
}
response = requests.post(url, headers=headers, json=payload)
print(response.json()){
"success": true,
"message": "Video uploaded successfully",
"video": {
"filename": "factory_line.mp4",
"size": 125000000,
"size_mb": 119.21,
"extension": ".mp4"
}
}{
"detail": "text"
}{
"detail": "text"
}{
"detail": [
{
"loc": [
"anything"
],
"msg": "text",
"type": "text"
}
]
}{
"detail": "text"
}{
"detail": "text"
}Streaming
Start streaming
Starts publishing the converted video to the RTSP server, looping until stopped. Uses the current stream settings.
200Streaming startedapplication/json
RTSP stream URL as the service sees it, always naming localhost, on the port set by RTSP_PORT (8554 by default). Consumers off the device must substitute the device address; read rtsp_port from /info rather than assuming 8554.
400Already streaming, or no video has been uploadedapplication/json
Error message
500The stream failed to startapplication/json
Error message
POST /play HTTP/1.1
Host: device-ip:8080
Accept: application/jsoncurl -L \
--request POST \
--url 'http://device-ip:8080/play' \
--header 'Accept: application/json'const response = await fetch("http://device-ip:8080/play", {
method: "POST",
headers: {
"Accept": "application/json"
}
});
const data = await response.json();
console.log(data);import requests
url = "http://device-ip:8080/play"
headers = {
"Accept": "application/json"
}
response = requests.post(url, headers=headers)
print(response.json()){
"success": true,
"message": "Streaming started",
"rtsp_url": "rtsp://localhost:8554/stream"
}{
"detail": "text"
}{
"detail": "text"
}Stop streaming and delete video files
Stops the stream and deletes both the original and converted files to free disk space. Uploading again is required before the next /play.
200Streaming stopped and files deletedapplication/json
400Not currently streamingapplication/json
Error message
500Stop failedapplication/json
Error message
POST /stop HTTP/1.1
Host: device-ip:8080
Accept: application/jsoncurl -L \
--request POST \
--url 'http://device-ip:8080/stop' \
--header 'Accept: application/json'const response = await fetch("http://device-ip:8080/stop", {
method: "POST",
headers: {
"Accept": "application/json"
}
});
const data = await response.json();
console.log(data);import requests
url = "http://device-ip:8080/stop"
headers = {
"Accept": "application/json"
}
response = requests.post(url, headers=headers)
print(response.json()){
"success": true,
"message": "text"
}{
"detail": "text"
}{
"detail": "text"
}/delete removes the video files without requiring a stream to be running, stopping one first if it is.
Delete video files
Deletes the original and converted video files, stopping the stream first if it is running. Unlike /stop, this succeeds whether or not a stream is active.
200Files deletedapplication/json
404No video files found to deleteapplication/json
Error message
500Delete failedapplication/json
Error message
POST /delete HTTP/1.1
Host: device-ip:8080
Accept: application/jsoncurl -L \
--request POST \
--url 'http://device-ip:8080/delete' \
--header 'Accept: application/json'const response = await fetch("http://device-ip:8080/delete", {
method: "POST",
headers: {
"Accept": "application/json"
}
});
const data = await response.json();
console.log(data);import requests
url = "http://device-ip:8080/delete"
headers = {
"Accept": "application/json"
}
response = requests.post(url, headers=headers)
print(response.json()){
"success": true,
"message": "text"
}{
"detail": "text"
}{
"detail": "text"
}Status
streaming is true when components.mediamtx.healthy (the streaming server) and components.ffmpeg_process.running (the transcode and publish process) are both true. The components.stream entry does not gate it: components.stream.source_ready can be false briefly during startup while streaming already reads true, which is normal. The breakdown is the fastest way to localize a failed stream: check components to see which part is unhealthy before restarting anything.
Get streaming status
Returns whether the stream is live, plus the health of each component, which is the fastest way to localize a failure.
streaming is true when components.mediamtx.healthy and components.ffmpeg_process.running are both true; the components.stream entry does not gate it. components.stream.source_ready can be false briefly during startup, which is normal.
200Current statusapplication/json
RTSP stream URL as the service sees it, always naming localhost, on the port set by RTSP_PORT (8554 by default). Null when not streaming. Consumers off the device must substitute the device address; read rtsp_port from /info rather than assuming 8554.
Current video, null when nothing is uploaded
Show propertiesHide properties
Original filename
File size in bytes
File size in megabytes
File extension
Show propertiesHide properties
Show propertiesHide properties
Show propertiesHide properties
Whether an encoder process record exists
Exit code when the process is no longer running
Show propertiesHide properties
Whether the streaming server's control API is reachable
Whether the stream source is ready
Number of clients currently reading the stream
0GET /status HTTP/1.1
Host: device-ip:8080
Accept: application/jsoncurl -L \
--request GET \
--url 'http://device-ip:8080/status' \
--header 'Accept: application/json'const response = await fetch("http://device-ip:8080/status", {
method: "GET",
headers: {
"Accept": "application/json"
}
});
const data = await response.json();
console.log(data);import requests
url = "http://device-ip:8080/status"
headers = {
"Accept": "application/json"
}
response = requests.get(url, headers=headers)
print(response.json()){
"streaming": true,
"rtsp_url": "rtsp://localhost:8554/stream",
"video": {
"filename": "sample.mp4",
"size": 1048576,
"size_mb": 1,
"extension": ".mp4"
},
"components": {
"mediamtx": {
"healthy": true,
"error": null
},
"ffmpeg_process": {
"exists": true,
"pid": 1234,
"running": true,
"exit_code": null
},
"stream": {
"api_available": true,
"source_ready": true,
"readers": 1,
"error": null
}
}
}/info reports the limits the service is running with, including free disk space and the maximum upload size, which is worth checking before pushing a large file.
Get system information
Returns disk space, the upload size limit, accepted extensions, and the RTSP port.
200System informationapplication/json
Show propertiesHide properties
Total disk space in bytes
Used disk space in bytes
Free disk space in bytes
Free disk space in GB
Maximum upload size in bytes, set by MAX_FILE_SIZE
Accepted file extensions. Only .mp4 today.
Port the RTSP stream is served on, set by RTSP_PORT
GET /info HTTP/1.1
Host: device-ip:8080
Accept: application/jsoncurl -L \
--request GET \
--url 'http://device-ip:8080/info' \
--header 'Accept: application/json'const response = await fetch("http://device-ip:8080/info", {
method: "GET",
headers: {
"Accept": "application/json"
}
});
const data = await response.json();
console.log(data);import requests
url = "http://device-ip:8080/info"
headers = {
"Accept": "application/json"
}
response = requests.get(url, headers=headers)
print(response.json()){
"disk_space": {
"total": 1,
"used": 1,
"free": 1,
"free_gb": 1
},
"max_file_size": 1,
"max_file_size_gb": 1,
"allowed_extensions": [
"text"
],
"rtsp_port": 1
}Stream Settings Endpoints
Playback settings differ from upload settings: they apply at streaming time and can be changed between sessions without re-uploading. An update takes effect on the next /play, not the stream currently running.
Get stream settings
Returns the current playback settings.
200Current settingsapplication/json
Maximum bitrate in kbps
5000Buffer size in KB
3000Maximum delay in milliseconds
500GET /stream-settings HTTP/1.1
Host: device-ip:8080
Accept: application/jsoncurl -L \
--request GET \
--url 'http://device-ip:8080/stream-settings' \
--header 'Accept: application/json'const response = await fetch("http://device-ip:8080/stream-settings", {
method: "GET",
headers: {
"Accept": "application/json"
}
});
const data = await response.json();
console.log(data);import requests
url = "http://device-ip:8080/stream-settings"
headers = {
"Accept": "application/json"
}
response = requests.get(url, headers=headers)
print(response.json()){
"max_bitrate_kbps": 5000,
"buffer_size_kb": 3000,
"max_delay_ms": 500
}Update stream settings
Persists new playback settings. They apply to the next streaming session, not the one currently running.
Maximum bitrate in kbps
5000Buffer size in KB
3000Maximum delay in milliseconds
500200Settings savedapplication/json
422Request failed validationapplication/json
One entry in a request-validation failure.
Show propertiesHide properties
Path to the offending field, for example ["body", "value"].
500Failed to persist settingsapplication/json
Error message
POST /stream-settings HTTP/1.1
Host: device-ip:8080
Content-Type: application/json
Accept: application/json
{
"max_bitrate_kbps": 5000,
"buffer_size_kb": 3000,
"max_delay_ms": 500
}curl -L \
--request POST \
--url 'http://device-ip:8080/stream-settings' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--data '{
"max_bitrate_kbps": 5000,
"buffer_size_kb": 3000,
"max_delay_ms": 500
}'const response = await fetch("http://device-ip:8080/stream-settings", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({
"max_bitrate_kbps": 5000,
"buffer_size_kb": 3000,
"max_delay_ms": 500
})
});
const data = await response.json();
console.log(data);import requests
url = "http://device-ip:8080/stream-settings"
headers = {
"Content-Type": "application/json",
"Accept": "application/json"
}
payload = {
"max_bitrate_kbps": 5000,
"buffer_size_kb": 3000,
"max_delay_ms": 500
}
response = requests.post(url, headers=headers, json=payload)
print(response.json()){
"success": true,
"message": "text"
}{
"detail": [
{
"loc": [
"anything"
],
"msg": "text",
"type": "text"
}
]
}{
"detail": "text"
}Configuration
Environment Variables
Set these on the service in the device's Configuration tab. See Update Device Configuration.
| Variable | Default | Description |
|---|---|---|
MAX_FILE_SIZE | 5368709120 | Maximum upload size in bytes (5 GB) |
RTSP_PORT | 8554 | RTSP server port |
WEB_PORT | 8080 | Web UI and API port |
LOG_LEVEL | INFO | Logging verbosity: DEBUG, INFO, WARNING, or ERROR |
Upload Settings
These are applied during conversion, so changing them requires re-uploading the video.
| Setting | Default | Description |
|---|---|---|
target_fps | original | Target frame rate, 1 to 120. Omit to keep the original |
crf_quality | 23 | Quality level, 0 to 51 (18 is high, 23 is medium, 28 is low) |
max_width | original | Maximum width for downscaling, 320 to 7680. Omit to keep the original |
Stream Settings
These are applied at playback and can be changed between streaming sessions from the CLI config menu or the /stream-settings endpoint.
| Setting | Default | Description |
|---|---|---|
max_bitrate_kbps | 5000 | Maximum bitrate in kbps, 100 to 100,000 |
buffer_size_kb | 3000 | Buffer size in KB, 100 to 50,000 |
max_delay_ms | 500 | Maximum delay in milliseconds, 50 to 5,000 |