ESPHome component to display network video streams (RTSP/H264 and MJPEG) on the ESP32-P4 with hardware decoding and LVGL display.
- MJPEG support - Hardware JPEG decoding optimized for network streams
- H264/RTSP support - Software H264 decoding (Constrained Baseline profile only; see limitations)
- Hardware decoding - ESP32-P4 hardware JPEG decoder (100 ms timeout)
- COM marker stripping - ffmpeg/go2rtc MJPEG compatibility
- WiFi handling - Automatic wait for the WiFi connection (15 s retry delay)
- LVGL display - Native integration with an LVGL canvas
- RGB565 - Color format optimized for display
- Multi-resolution - Supports 320x240, 640x480, etc.
- Hardware: ESP32-P4 (with hardware JPEG decoder)
- ESPHome: Recent version with ESP32-P4 support
- LVGL: Configured LVGL component
- Network: Configured and working WiFi
external_components:
- source:
type: git
url: https://github.com/youkorr/ip-camera-viewer
ref: main
components:
- ip_camera_viewer
- h264_hp
- esp_h264
refresh: 0s# Network configuration
wifi:
ssid: "YourSSID"
password: "YourPassword"
# LVGL configuration
lvgl:
displays:
- display_id: my_display
# IP camera configuration
ip_camera_viewer:
- id: security_cam_1
url: "http://<host>:1984/api/stream.mjpeg?src=frigate1_esp32"
protocol: mjpeg
width: 640
height: 480
canvas_id: security_canvas
update_interval: 66msTo get an optimized MJPEG stream from your RTSP cameras:
# go2rtc.yaml (Frigate)
go2rtc:
streams:
frigate1_esp32:
- "ffmpeg:rtsp://user:pass@<host>/stream2#video=mjpeg#width=320#height=240#quality=80#fps=15"ip_camera_viewer:
- id: security_cam_1
url: "http://<host>/api/stream.mjpeg?src=frigate1_esp32"
protocol: mjpeg
width: 640
height: 480
canvas_id: security_canvas
update_interval: 66msWhy MJPEG?
- Hardware decoding (fast and efficient)
- Low latency
- No H264 profile issues
- COM markers automatically stripped
- Built-in JPEG validation
lvgl:
pages:
- id: security_page
bg_color: 0x1a1a1a
on_load:
- lambda: |-
ESP_LOGI("security", "Security page loaded - configuring canvas");
// Configure the canvas for ip_camera_viewer
auto canvas = id(security_canvas);
if (canvas != nullptr) {
lv_coord_t w = lv_obj_get_width(canvas);
lv_coord_t h = lv_obj_get_height(canvas);
ESP_LOGI("security", "Canvas size: %dx%d", w, h);
if (w > 0 && h > 0) {
// IMPORTANT: Call configure_canvas on security_cam_1
id(security_cam_1).configure_canvas(canvas);
ESP_LOGI("security", "Canvas configured successfully!");
} else {
ESP_LOGW("security", "Canvas size is 0x0, waiting for initialization");
}
}
widgets:
- canvas:
id: security_canvas
width: 640
height: 480
x: 10
y: 10
bg_color: 0x000000
- label:
id: security_title
text: "SECURITY CAMERA"
x: 350
y: 10
text_color: 0xFFFFFF
- button:
id: btn_start_camera
width: 100
height: 40
x: 350
y: 60
bg_color: 0x27ae60
on_click:
then:
- lambda: |-
ESP_LOGI("security", "Starting camera");
id(security_cam_1).set_enabled(true);
widgets:
- label:
text: "START"
text_color: 0xFFFFFF
align: CENTER
- button:
id: btn_stop_camera
width: 100
height: 40
x: 350
y: 110
bg_color: 0xe74c3c
on_click:
then:
- lambda: |-
ESP_LOGI("security", "Stopping camera");
id(security_cam_1).set_enabled(false);
widgets:
- label:
text: "STOP"
text_color: 0xFFFFFF
align: CENTER
# Global variables (optional)
globals:
- id: cam1_state
type: bool
initial_value: 'false'Symptom:
[W][ip_camera_viewer]: Canvas not configured
Solution:
Call configure_canvas() in the LVGL page on_load:
on_load:
- lambda: |-
auto canvas = id(security_canvas);
id(security_cam_1).configure_canvas(canvas);IMPORTANT: Call configure_canvas() on security_cam_1 (ip_camera_viewer),
NOT on security_display (multi_camera_display)!
Symptom:
[W][ip_camera_viewer]: WiFi not ready yet, waiting for connection...
[E][ip_camera_viewer]: Host is unreachable (errno 118)
Solution: The component automatically waits for the WiFi connection with a 15-second delay between attempts. No configuration required — it is automatic!
The component checks:
- WiFi is connected
- STA interface is active
- Before any camera connection attempt
Symptom:
[E][ip_camera_viewer]: jpeg_parse_com_marker(63): COM marker data underflow
Solution: Already fixed! The component automatically strips the COM markers added by ffmpeg/go2rtc that are incompatible with the ESP32-P4 decoder.
Expected logs:
[I][ip_camera_viewer]: Stripping COM marker at offset 2 (length 17 bytes)
[D][ip_camera_viewer]: Stripped COM markers: 1585 -> 1568 bytes (saved 17 bytes)
Symptom:
[W][ip_camera_viewer]: No H264 frames decoded yet (1000 attempts)
Solution: Already fixed! The component now sends SPS/PPS with the first frame (I-frame or P-frame), not only with I-frames.
Expected logs:
[I][ip_camera_viewer]: Sent SPS+PPS (26+8 bytes) with FIRST frame (NAL type 1)
[I][ip_camera_viewer]: First frame decoded successfully! Decoder initialized and working.
Symptom:
[W][security]: Canvas size is 0x0, waiting for initialization
Solution:
Do NOT configure the canvas in the page on_load, but in a button after LVGL is
initialized:
on_click:
then:
- lambda: |-
static bool canvas_configured = false;
if (!canvas_configured) {
auto canvas = id(security_canvas);
lv_coord_t w = lv_obj_get_width(canvas);
if (w > 0 && lv_obj_get_height(canvas) > 0) {
id(security_cam_1).configure_canvas(canvas);
canvas_configured = true;
}
}
id(security_cam_1).set_enabled(true);[I][ip_camera_viewer]: WiFi ready, starting camera...
[I][ip_camera_viewer]: MJPEG connected - Status: 200
[I][ip_camera_viewer]: First JPEG frame: 1585 bytes
[I][ip_camera_viewer]: Stripping COM marker at offset 2 (length 17 bytes)
[I][ip_camera_viewer]: First JPEG frame analysis:
[I][ip_camera_viewer]: Size: 1568 bytes
[I][ip_camera_viewer]: SOI marker: 0xFFD8 (valid FFD8)
[I][ip_camera_viewer]: Format: Baseline DCT (SOF0) - fully supported
[I][ip_camera_viewer]: First JPEG decoded successfully: 153600 bytes output
[I][ip_camera_viewer]: Frames: 100 - FPS: 15.0
[I][ip_camera_viewer]: WiFi ready, starting camera...
[I][ip_camera_viewer]: RTSP connected
[I][ip_camera_viewer]: SPS received: 26 bytes
[I][ip_camera_viewer]: PPS received: 8 bytes
[I][ip_camera_viewer]: Sent SPS+PPS (26+8 bytes) with FIRST frame (NAL type 1)
[I][ip_camera_viewer]: Frame #1: NAL type 1 (P-frame), size 2847 bytes
[I][ip_camera_viewer]: First frame decoded successfully! Decoder initialized and working.
[I][ip_camera_viewer]: Decoded YUV size: 115200 bytes
ip_camera_viewer:
- id: security_cam_1
url: "rtsp://username:password@:ip-camera/stream2"
protocol: rtsp # "h264" is accepted as an alias
width: 640
height: 352
canvas_id: security_canvas
update_interval: 100msProtocol values: use
mjpeg(HTTP MJPEG) orrtsp(RTSP/H.264).h264is accepted as an alias forrtsp.
Credentials are taken from the URL (rtsp://user:pass@host:port/path). Both
Basic and Digest authentication are supported, and Digest is detected
automatically from the camera's 401 challenge — so cameras that only accept
Digest (Reolink, Hikvision, Dahua, many others) work without a proxy. A password
containing @ is handled correctly.
go2rtc:
streams:
frigate1:
- rtsp://username:password@ip-camera/stream1H264 limitations (important):
- The ESP32-P4 has no hardware H264 decoder. Decoding is done in software by
Espressif's
esp_h264(tinyH264), which supports the Constrained Baseline profile only. - Main and High profile streams cannot be decoded — even though they play fine in VLC (VLC ships a full decoder). Most IP cameras (Reolink, Hikvision, Dahua, Tapo, ...) default to Main/High profile.
- The component logs the detected profile at startup, e.g.
H264 stream profile_idc=77 (Main), and warns when it is not Baseline. - Software decoding is also slower than MJPEG and a large GOP adds latency.
Recommended for non-Baseline cameras: use MJPEG via go2rtc (decoded by the ESP32-P4 hardware JPEG decoder — fast and reliable), or have go2rtc/ffmpeg transcode the stream to H264 Baseline. See the go2rtc examples above.
Recommendation: Use MJPEG via go2rtc for better performance!
ip_camera_viewer:
- id: security_cam_1
url: "http://HA:1984/api/stream.mjpeg?src=cam1"
protocol: mjpeg
width: 640
height: 480
canvas_id: canvas1
- id: security_cam_2
url: "http://HA:1984/api/stream.mjpeg?src=cam2"
protocol: mjpeg
width: 320
height: 240
canvas_id: canvas2A switch platform is provided to turn each camera on or off from Home
Assistant or the ESPHome UI. The switch enables/disables the referenced
ip_camera_viewer instance and persists its state across reboots (restored
on boot, defaults to OFF).
ip_camera_viewer:
- id: security_cam_1
url: "http://<host>/api/stream.mjpeg?src=cam1"
protocol: mjpeg
width: 640
height: 480
canvas_id: security_canvas
switch:
- platform: ip_camera_viewer
name: "Security Camera 1"
camera_id: security_cam_1Configuration variables:
- name (Required, string): the name of the switch.
- camera_id (Required, ID): the ID of the
ip_camera_viewerinstance to control. - All other options from the base Switch
component (
id,icon,restore_mode, etc.) are supported.
Turning the switch on is equivalent to calling set_enabled(true) on the
camera; turning it off calls set_enabled(false).
// Enable/disable the camera
id(security_cam_1).set_enabled(true);
id(security_cam_1).set_enabled(false);
// Configure the LVGL canvas
auto canvas = id(security_canvas);
id(security_cam_1).configure_canvas(canvas);
// Check the state
bool is_running = id(security_cam_1).is_enabled();-
Critical H264 SPS/PPS fix
- Sends SPS/PPS with the FIRST frame (not only I-frames)
- Avoids "No frames decoded" when the stream starts with P-frames
- File:
ip_camera_viewer.cpp
-
MJPEG COM marker fix
- Strips COM markers (FF FE) added by ffmpeg
- The ESP32-P4 hardware decoder does not support COM markers
- Function:
strip_jpeg_com_markers_() - File:
ip_camera_viewer.cpp
-
JPEG timeout fix
- Timeout increased from 40 ms -> 100 ms
- Required for network latency
- File:
ip_camera_viewer.cpp
-
WiFi timing fix
- Automatically waits for the WiFi connection
- 15 s delay between attempts
- Checks
is_connected()andhas_sta() - File:
ip_camera_viewer.cpp
-
WiFi API compatibility fix
- Replaces
get_ip_address()withhas_sta() - Compatible with newer ESPHome versions
- File:
ip_camera_viewer.cpp
- Replaces
- MJPEG input: JPEG Baseline DCT (SOF0)
- H264 input: NAL units, Annex B format (00 00 00 01)
- Output: RGB565 (2 bytes/pixel)
- Buffer: 320x240 = 153600 bytes RGB565
- MJPEG: ~15 FPS @ 320x240 (hardware decoding)
- H264: ~10 FPS @ 320x240 (software decoding)
- SRAM memory: ~220 KB
- PSRAM memory: ~6.7 MB
logger:
level: DEBUG
logs:
ip_camera_viewer: DEBUG# Test MJPEG in a browser
http://HA:1984/api/stream.mjpeg?src=frigate1_esp32
# Test H264 with ffplay
ffplay -rtsp_transport tcp rtsp://user:pass@1:554/stream2- Repository: https://github.com/youkorr/ip-camera-viewer
- ESPHome: https://esphome.io
- go2rtc: https://github.com/AlexxIT/go2rtc
The ip_camera_viewer component (everything under components/ip_camera_viewer)
was created and is maintained solely by youkorr. It is the author's original
work and does not reuse any ESPHome or Espressif source code. Because it is an
ESPHome external component, it follows the same dual-license arrangement as
ESPHome so the two can be combined and distributed without any license
conflict:
- C++/runtime code (
.c,.cpp,.h,.hpp,.tcc,.ino) is licensed under the GPLv3 (it is compiled and linked against ESPHome's GPLv3 C++ runtime). - Python code and all other parts of this repository are licensed under the MIT license.
See the LICENSE file for the full text of both licenses. This applies
only to the ip_camera_viewer component; bundled third-party code keeps its own
license (see below).
- ESPHome (https://github.com/esphome/esphome) — Copyright (c) 2019 ESPHome, dual-licensed MIT/GPLv3.
- Espressif
esp_h264(bundled undercomponents/esp_h264) — Copyright (c) Espressif Systems, licensed under Apache-2.0. It is included unmodified as a build dependency (software H.264 / RTSP decoding) so the project compiles out of the box, and keeps its own SPDX headers andLICENSE/NOTICEfiles. The ESP32-P4 hardware JPEG decoder is used through ESP-IDF's public APIs.
For problems or questions:
- Check the "Troubleshooting" section above
- Enable DEBUG logs
- Open an issue on GitHub with the full logs
Author: youkorr Version: 1.0.0