Decode with esp_new_jpeg; fully decode double-encoded camera URLs

esp_new_jpeg scales inside the decoder and is 5-6x faster than tjpgd on
the S3 (1280x800 frame: ~100 ms instead of ~600 ms), so MJPEG streams
now play at a few fps. insecam double-encodes some URLs (&); the
entity decoder now loops until stable.
This commit is contained in:
Juli committed 2026-09-17 00:45:27 +02:00
1 parent 65a391940b
commit eff9e5dfaa
6 files changed
+87 -55

No files matched your search

+4 -2
View File
@@ -56,8 +56,10 @@ your Wi-Fi. Holding the knob down while the widget starts forces the portal.
bodies without `Content-Length`, which is exactly what MJPEG cameras send.
* `jpegframe.c` pulls complete JPEG files out of the byte stream by walking the
marker structure (so an EXIF thumbnail's EOI can't truncate a frame).
* `jpegdec.c` decodes with `esp_jpeg` at the largest power-of-two reduction
that still covers the screen, then bilinear-resamples to a full-bleed 320×170.
* `jpegdec.c` decodes with Espressif's `esp_new_jpeg`, scaling inside the
decoder to the smallest multiple-of-8 size that still covers the screen, then
bilinear-resamples the remainder to a full-bleed 320×170 (≈100 ms for a
1280×800 frame, so MJPEG streams play at a few fps).
* `slideshow.c` runs the state machine: connect, play frames until the interval
elapses or the knob moves, skip dead cameras, keep history for "previous".
* `ui.c` is LVGL 9 via `esp_lvgl_port`: a canvas for the frame, a translucent
+21 -12
View File
@@ -23,16 +23,6 @@ dependencies:
registry_url: https://components.espressif.com
type: service
version: 0.5.3
espressif/esp_jpeg:
component_hash: defb83669293cbf86d0fa86b475ba5517aceed04ed70db435388c151ab37b5d7
dependencies:
- name: idf
require: private
version: '>=5.0'
source:
registry_url: https://components.espressif.com/
type: service
version: 1.3.1
espressif/esp_lvgl_port:
component_hash: d3c020b45c3dfc0d1706a83b63a1c34a6eb444a6e935f9afff79f92b7b94dcef
dependencies:
@@ -47,6 +37,25 @@ dependencies:
registry_url: https://components.espressif.com/
type: service
version: 2.9.0
espressif/esp_new_jpeg:
component_hash: e1e7e6991193c41ed6c9fc386702aa7821433319e7aa9c3147cd3236310cd69c
dependencies: []
source:
registry_url: https://components.espressif.com/
type: service
targets:
- esp32
- esp32s2
- esp32s3
- esp32s31
- esp32p4
- esp32c2
- esp32c3
- esp32c5
- esp32c6
- esp32c61
- esp32h4
version: 1.0.2
espressif/knob:
component_hash: b56694054cd34a683ac2a6f3661963ba00b1dab741a4f3bb59ed6666cbc9f3fb
dependencies:
@@ -108,12 +117,12 @@ dependencies:
version: 9.6.0
direct_dependencies:
- espressif/button
- espressif/esp_jpeg
- espressif/esp_lvgl_port
- espressif/esp_new_jpeg
- espressif/knob
- espressif/led_strip
- idf
- lvgl/lvgl
manifest_hash: d33b0997c1d650a5851082c8eb06024622fb751cdfbef43544acf0e98406433c
manifest_hash: c3ae35dba19eb151e16226d84853ce33ee1ef723b08192955e54805732cd49fd
target: esp32s3
version: 2.0.0
+1 -1
View File
@@ -2,7 +2,7 @@ dependencies:
idf: ">=5.5,<6"
lvgl/lvgl: "^9.3"
espressif/esp_lvgl_port: "^2.7"
espressif/esp_jpeg: "^1.3"
espressif/knob: "^1.1"
espressif/button: "^4.2"
espressif/led_strip: "^3.0"
espressif/esp_new_jpeg: "^1.0"
+1
View File
@@ -106,6 +106,7 @@ static int parse_page(char *html, cam_info_t *out, int max, int *total_pages)
item = next;
continue;
}
for (int pass = 0; pass < 3 && strstr(c->url, "&amp;"); pass++) clean_text(c->url, sizeof c->url);
clean_text(c->url, sizeof c->url);
fill_counter(c->url);
// <img ... title="Live camera Mobotix in Imola, Italy"> ; fall back to the link title
+60 -36
View File
@@ -1,24 +1,21 @@
#include "jpegdec.h"
#include "display.h"
#include "jpeg_decoder.h"
#include "esp_jpeg_dec.h"
#include "esp_timer.h"
#include "esp_heap_caps.h"
#include "esp_log.h"
#include <string.h>
static const char *TAG = "jpeg";
#define WORK_BUF_SIZE 65472 // tjpgd scratch with JD_FASTDECODE=2
#define MAX_INTERMEDIATE (2 * 1024 * 1024)
static void *work_buf;
static uint16_t *mid; // decoded image before resampling (PSRAM)
static size_t mid_cap;
esp_err_t jpegdec_init(void)
{
work_buf = heap_caps_malloc(WORK_BUF_SIZE, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
if (!work_buf) return ESP_ERR_NO_MEM;
mid_cap = 640 * 360 * 2;
mid = heap_caps_malloc(mid_cap, MALLOC_CAP_SPIRAM);
mid = heap_caps_aligned_alloc(16, mid_cap, MALLOC_CAP_SPIRAM); // esp_new_jpeg wants 16-byte alignment
return mid ? ESP_OK : ESP_ERR_NO_MEM;
}
@@ -77,6 +74,59 @@ static void resample_cover(const uint16_t *src, int sw, int sh, uint16_t *dst)
}
}
static int64_t ms(void) { return esp_timer_get_time() / 1000; }
// esp_new_jpeg path: scale in the decoder to the smallest multiple-of-8 size that still
// covers the screen (uniform factor, min 1/8), then bilinear-resample the last few percent.
static esp_err_t decode_new(const uint8_t *jpeg, size_t len, int w, int h, uint16_t *dst, int64_t *t_dec)
{
jpeg_dec_config_t cfg = DEFAULT_JPEG_DEC_CONFIG();
cfg.output_type = JPEG_PIXEL_FORMAT_RGB565_LE;
float f = LCD_H_RES / (float)w;
if (LCD_V_RES / (float)h > f) f = LCD_V_RES / (float)h;
if (f < 0.125f) f = 0.125f;
int sw = w, sh = h;
if (f < 1.0f) {
sw = ((int)(w * f + 7) / 8) * 8;
sh = ((int)(h * f + 7) / 8) * 8;
if (sw < 8) sw = 8;
if (sh < 8) sh = 8;
cfg.scale.width = sw;
cfg.scale.height = sh;
}
size_t need = (size_t)(sw + 16) * (sh + 16) * 2;
if (need > MAX_INTERMEDIATE) return ESP_ERR_INVALID_SIZE;
if (need > mid_cap) {
uint16_t *bigger = heap_caps_aligned_alloc(16, need, MALLOC_CAP_SPIRAM);
if (!bigger) return ESP_ERR_NO_MEM;
heap_caps_free(mid);
mid = bigger;
mid_cap = need;
}
jpeg_dec_handle_t dec = NULL;
if (jpeg_dec_open(&cfg, &dec) != JPEG_ERR_OK) return ESP_FAIL;
jpeg_dec_io_t io = {.inbuf = (uint8_t *)jpeg, .inbuf_len = len};
jpeg_dec_header_info_t info = {0};
jpeg_error_t err = jpeg_dec_parse_header(dec, &io, &info);
int outlen = 0;
if (err == JPEG_ERR_OK) err = jpeg_dec_get_outbuf_len(dec, &outlen);
if (err == JPEG_ERR_OK && (size_t)outlen > mid_cap) err = JPEG_ERR_NO_MEM;
int64_t t0 = ms();
if (err == JPEG_ERR_OK) {
io.outbuf = (uint8_t *)mid;
err = jpeg_dec_process(dec, &io);
}
*t_dec = ms() - t0;
jpeg_dec_close(dec);
if (err != JPEG_ERR_OK) {
ESP_LOGW(TAG, "esp_new_jpeg failed (%d) %dx%d -> %dx%d", err, w, h, sw, sh);
return ESP_FAIL;
}
int ow = cfg.scale.width ? sw : w, oh = cfg.scale.height ? sh : h;
resample_cover(mid, ow, oh, dst);
return ESP_OK;
}
esp_err_t jpegdec_to_screen(const uint8_t *jpeg, size_t len, uint16_t *dst, int *src_w, int *src_h)
{
int w, h;
@@ -84,34 +134,8 @@ esp_err_t jpegdec_to_screen(const uint8_t *jpeg, size_t len, uint16_t *dst, int
if (err != ESP_OK) return err;
*src_w = w;
*src_h = h;
// Largest power-of-two reduction that still covers the screen.
int scale = 0;
while (scale < 3 && (w >> (scale + 1)) >= LCD_H_RES && (h >> (scale + 1)) >= LCD_V_RES) scale++;
int sw = w >> scale, sh = h >> scale;
// tjpgd rounds the scaled size up to whole MCUs; give it room.
size_t need = (size_t)(sw + 16) * (sh + 16) * 2;
if (need > MAX_INTERMEDIATE) return ESP_ERR_INVALID_SIZE;
if (need > mid_cap) {
uint16_t *bigger = heap_caps_realloc(mid, need, MALLOC_CAP_SPIRAM);
if (!bigger) return ESP_ERR_NO_MEM;
mid = bigger;
mid_cap = need;
}
esp_jpeg_image_cfg_t cfg = {
.indata = (uint8_t *)jpeg, .indata_size = len,
.outbuf = (uint8_t *)mid, .outbuf_size = mid_cap,
.out_format = JPEG_IMAGE_FORMAT_RGB565, .out_scale = (esp_jpeg_image_scale_t)scale,
.flags.swap_color_bytes = 0,
.advanced = {.working_buffer = work_buf, .working_buffer_size = WORK_BUF_SIZE},
};
esp_jpeg_image_output_t out = {0};
err = esp_jpeg_decode(&cfg, &out);
if (err != ESP_OK) {
ESP_LOGW(TAG, "decode failed (%s) %dx%d scale 1/%d", esp_err_to_name(err), w, h, 1 << scale);
return err;
}
resample_cover(mid, out.width, out.height, dst);
return ESP_OK;
int64_t t_dec = 0, t0 = ms();
err = decode_new(jpeg, len, w, h, dst, &t_dec);
ESP_LOGD(TAG, "%dx%d %u bytes: decode %lld ms, total %lld ms", w, h, (unsigned)len, (long long)t_dec, (long long)(ms() - t0));
return err;
}
-4
View File
@@ -48,7 +48,3 @@ CONFIG_LV_USE_BAR=y
CONFIG_LV_USE_LABEL=y
CONFIG_LV_USE_PERF_MONITOR=n
# esp_jpeg: the ROM tjpgd is slow; use the bundled one with table-driven Huffman decoding
CONFIG_JD_USE_ROM=n
CONFIG_JD_FASTDECODE_TABLE=y
CONFIG_JD_FORMAT_RGB565=y