PR #22917 opened by Jack Dunford (dun4d) URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22917 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22917.patch
AJPG is a format supported by some early webcams. Bayer sensor data stored in JPEG. Signed-off-by: Jack Dunford <[email protected]> >From 6de9be1b632b079100a7f35c163894ce6e6d88d4 Mon Sep 17 00:00:00 2001 From: dun4d <[email protected]> Date: Fri, 24 Apr 2026 22:35:45 +0100 Subject: [PATCH] libavcodec/ajpgdec: AJPG decoder AJPG is a format supported by some early webcams. Bayer sensor data stored in JPEG. Signed-off-by: Jack Dunford <[email protected]> --- libavcodec/ajpgdec.c | 512 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 512 insertions(+) create mode 100644 libavcodec/ajpgdec.c diff --git a/libavcodec/ajpgdec.c b/libavcodec/ajpgdec.c new file mode 100644 index 0000000000..efe978dfe0 --- /dev/null +++ b/libavcodec/ajpgdec.c @@ -0,0 +1,512 @@ +/* + * AJPG (AIPTEK, Kensington) JPEG Decoder. + * Copyright (c) 2026 Jack Dunford + * + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "avcodec.h" +#include "codec_internal.h" +#include "decode.h" +#include "libavutil/log.h" +#include "libavutil/mem.h" +#include "libavutil/common.h" +#include "libavutil/opt.h" +#include <stddef.h> + +/* JPEG Annex K Huffman tables as a prebuilt DHT blob. */ +static const uint8_t annex_k_dht[] = { + /* Luma DC (Tc=0, Th=0) */ + 0xFF,0xC4, 0x00,0x1F, 0x00, + 0x00,0x01,0x05,0x01,0x01,0x01,0x01,0x01, + 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,0x0B, + /* Luma AC (Tc=1, Th=0) */ + 0xFF,0xC4, 0x00,0xB5, 0x10, + 0x00,0x02,0x01,0x03,0x03,0x02,0x04,0x03, + 0x05,0x05,0x04,0x04,0x00,0x00,0x01,0x7D, + 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12,0x21,0x31,0x41,0x06, + 0x13,0x51,0x61,0x07,0x22,0x71,0x14,0x32,0x81,0x91,0xA1,0x08, + 0x23,0x42,0xB1,0xC1,0x15,0x52,0xD1,0xF0,0x24,0x33,0x62,0x72, + 0x82,0x09,0x0A,0x16,0x17,0x18,0x19,0x1A,0x25,0x26,0x27,0x28, + 0x29,0x2A,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x43,0x44,0x45, + 0x46,0x47,0x48,0x49,0x4A,0x53,0x54,0x55,0x56,0x57,0x58,0x59, + 0x5A,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x73,0x74,0x75, + 0x76,0x77,0x78,0x79,0x7A,0x83,0x84,0x85,0x86,0x87,0x88,0x89, + 0x8A,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A,0xA2,0xA3, + 0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xB2,0xB3,0xB4,0xB5,0xB6, + 0xB7,0xB8,0xB9,0xBA,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9, + 0xCA,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA,0xE1,0xE2, + 0xE3,0xE4,0xE5,0xE6,0xE7,0xE8,0xE9,0xEA,0xF1,0xF2,0xF3,0xF4, + 0xF5,0xF6,0xF7,0xF8,0xF9,0xFA, + /* Chroma DC (Tc=0, Th=1) */ + 0xFF,0xC4, 0x00,0x1F, 0x01, + 0x00,0x03,0x01,0x01,0x01,0x01,0x01,0x01, + 0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x00, + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,0x0B, + /* Chroma AC (Tc=1, Th=1) */ + 0xFF,0xC4, 0x00,0xB5, 0x11, + 0x00,0x02,0x01,0x02,0x04,0x04,0x03,0x04, + 0x07,0x05,0x04,0x04,0x00,0x01,0x02,0x77, + 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21,0x31,0x06,0x12,0x41, + 0x51,0x07,0x61,0x71,0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, + 0xA1,0xB1,0xC1,0x09,0x23,0x33,0x52,0xF0,0x15,0x62,0x72,0xD1, + 0x0A,0x16,0x24,0x34,0xE1,0x25,0xF1,0x17,0x18,0x19,0x1A,0x26, + 0x27,0x28,0x29,0x2A,0x35,0x36,0x37,0x38,0x39,0x3A,0x43,0x44, + 0x45,0x46,0x47,0x48,0x49,0x4A,0x53,0x54,0x55,0x56,0x57,0x58, + 0x59,0x5A,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x73,0x74, + 0x75,0x76,0x77,0x78,0x79,0x7A,0x82,0x83,0x84,0x85,0x86,0x87, + 0x88,0x89,0x8A,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A, + 0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xB2,0xB3,0xB4, + 0xB5,0xB6,0xB7,0xB8,0xB9,0xBA,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7, + 0xC8,0xC9,0xCA,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA, + 0xE2,0xE3,0xE4,0xE5,0xE6,0xE7,0xE8,0xE9,0xEA,0xF2,0xF3,0xF4, + 0xF5,0xF6,0xF7,0xF8,0xF9,0xFA, +}; + +/* Dodgy header from sample files */ +static const uint8_t old_sof0[] = { + 0xFF, 0xC0, 0x00, 0x14, 0x08, 0x01, 0xE0, 0x02, 0x80, 0x04, + 0x01, 0x22, 0x00, 0x02, 0x22, 0x00, 0x03, 0x22, 0x00, 0x04, 0x22, 0x00 +}; + +/* State context for the decoder */ +typedef struct AJPGContext { + const AVClass *class; + AVCodecContext *mjpeg_avctx; + AVPacket *pkt; + AVFrame *tmp; + int bayer_pattern; + /* AJPG or MJPEG */ + int format_probed; + int is_aiptek_bayer; +} AJPGContext; + +/* Command line options to change bayer table -bayer */ +#define OFFSET(x) offsetof(AJPGContext, x) +#define VD AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_DECODING_PARAM + +static const AVOption ajpg_options[] = { + { "bayer", "Bayer color pattern", OFFSET(bayer_pattern), AV_OPT_TYPE_INT, {.i64 = -1}, -2, INT_MAX, VD, .unit = "bayer" }, + { "none", "Disable weaving (output raw CMYK frame)", 0, AV_OPT_TYPE_CONST, {.i64 = -2}, 0, 0, VD, .unit = "bayer" }, + { "auto", "Auto-detect from metadata", 0, AV_OPT_TYPE_CONST, {.i64 = -1}, 0, 0, VD, .unit = "bayer" }, + { "rggb", "RGGB", 0, AV_OPT_TYPE_CONST, {.i64 = AV_PIX_FMT_BAYER_RGGB8}, 0, 0, VD, .unit = "bayer" }, + { "bggr", "BGGR", 0, AV_OPT_TYPE_CONST, {.i64 = AV_PIX_FMT_BAYER_BGGR8}, 0, 0, VD, .unit = "bayer" }, + { "gbrg", "GBRG", 0, AV_OPT_TYPE_CONST, {.i64 = AV_PIX_FMT_BAYER_GBRG8}, 0, 0, VD, .unit = "bayer" }, + { "grbg", "GRBG", 0, AV_OPT_TYPE_CONST, {.i64 = AV_PIX_FMT_BAYER_GRBG8}, 0, 0, VD, .unit = "bayer" }, + { NULL } +}; + +static const AVClass ajpg_class = { + .class_name = "ajpg", + .item_name = av_default_item_name, + .option = ajpg_options, + .version = LIBAVUTIL_VERSION_INT, +}; + +/* JPG Markers */ +static int ajpg_patch_sof0(uint8_t *buf, int size) +{ + int i, off, h, w, comp, c; + + for (i = 0; i < size - 8; i++) { + if (buf[i] != 0xFF || buf[i + 1] != 0xC0) + continue; + + off = i + 4; + if (off + 6 > size) + return AVERROR_INVALIDDATA; + + h = (buf[off + 1] << 8) | buf[off + 2]; + w = (buf[off + 3] << 8) | buf[off + 4]; + h /= 2; + w /= 2; + + buf[off + 1] = (h >> 8) & 0xFF; + buf[off + 2] = h & 0xFF; + buf[off + 3] = (w >> 8) & 0xFF; + buf[off + 4] = w & 0xFF; + + buf[off + 5] = 4; + + comp = off + 6; + for (c = 0; c < 4; c++) { + if (comp + 3 > size) + return AVERROR_INVALIDDATA; + buf[comp + 1] = 0x11; + comp += 3; + } + return 0; + } + return AVERROR_INVALIDDATA; +} + +static const uint8_t *find_soi(const uint8_t *buf, int size) +{ + for (int i = 0; i < size - 1; i++) + if (buf[i] == 0xFF && buf[i+1] == 0xD8) + return buf + i; + return NULL; +} + +static const uint8_t *find_marker(const uint8_t *buf, int size, uint8_t marker) +{ + int i = 2; + uint8_t m; + int len; + + while (i + 1 < size) { + if (buf[i] != 0xFF) { + i++; + continue; + } + m = buf[i+1]; + if (m == marker) + return buf + i; + + if (m == 0xD8 || m == 0xD9 || (m >= 0xD0 && m <= 0xD7)) { + i += 2; + continue; + } + + if (i + 3 >= size) + break; + len = (buf[i+2] << 8) | buf[i+3]; + if (len < 2) + break; + i += 2 + len; + } + return NULL; +} + +/* BAYER Auto Detect */ +static int detect_bayer_pattern(const uint8_t *buf, int size, int user_choice) +{ + const uint8_t *app1; + uint8_t pattern_id; + + if (user_choice != -1) + return user_choice; + + app1 = find_marker(buf, size, 0xE1); + if (app1 && size - (app1 - buf) >= 16) { + if (memcmp(app1 + 4, "EPJPGBYR", 8) == 0) { + pattern_id = app1[13]; + switch (pattern_id) { + case 0x00: return AV_PIX_FMT_BAYER_RGGB8; + case 0x01: return AV_PIX_FMT_BAYER_GBRG8; + case 0x02: return AV_PIX_FMT_BAYER_BGGR8; + case 0x03: return AV_PIX_FMT_BAYER_GRBG8; + } + } + } + return AV_PIX_FMT_BAYER_RGGB8; +} + +/* AJPG Init */ +static av_cold int ajpg_decode_init(AVCodecContext *avctx) +{ + AJPGContext *s = avctx->priv_data; + const AVCodec *mjpeg = avcodec_find_decoder(AV_CODEC_ID_MJPEG); + int ret; + + if (!mjpeg) { + av_log(avctx, AV_LOG_ERROR, "Internal MJPEG decoder not available\n"); + return AVERROR_DECODER_NOT_FOUND; + } + + s->mjpeg_avctx = avcodec_alloc_context3(mjpeg); + if (!s->mjpeg_avctx) + return AVERROR(ENOMEM); + + ret = avcodec_open2(s->mjpeg_avctx, mjpeg, NULL); + if (ret < 0) { + avcodec_free_context(&s->mjpeg_avctx); + return ret; + } + + s->pkt = av_packet_alloc(); + s->tmp = av_frame_alloc(); + if (!s->pkt || !s->tmp) { + avcodec_free_context(&s->mjpeg_avctx); + av_packet_free(&s->pkt); + av_frame_free(&s->tmp); + return AVERROR(ENOMEM); + } + + s->format_probed = 0; + return 0; +} + +/* Cold open */ +static av_cold int ajpg_decode_end(AVCodecContext *avctx) +{ + AJPGContext *s = avctx->priv_data; + avcodec_free_context(&s->mjpeg_avctx); + av_packet_free(&s->pkt); + av_frame_free(&s->tmp); + return 0; +} + +/* Frame decode */ +static int ajpg_decode_frame(AVCodecContext *avctx, AVFrame *frame, + int *got_frame, AVPacket *avpkt) +{ + AJPGContext *s = avctx->priv_data; + const uint8_t *src = avpkt->data; + int src_size = avpkt->size; + uint8_t *patched = NULL; + int patched_size = 0; + int owns_patched = 0; + + /* ALL variables declared at the top to satisfy C89 */ + int ret, skipped, has_dht, needs_sof_patch; + int pre, post, dht_size; + const uint8_t *soi, *sof, *sos, *app1; + + if (src_size == 0) { + *got_frame = 0; + return 0; + } + + av_packet_unref(s->pkt); + av_frame_unref(s->tmp); + + soi = find_soi(src, src_size); + if (!soi) { + av_log(avctx, AV_LOG_ERROR, "No SOI marker\n"); + return AVERROR_INVALIDDATA; + } + skipped = (int)(soi - src); + src_size -= skipped; + src = soi; + + /* First-Frame Fingerprint Probing --- */ + if (!s->format_probed) { + /* Assume MJPEG until not */ + s->is_aiptek_bayer = 0; + + if (find_marker(src, src_size, 0xC4) == NULL) { + s->is_aiptek_bayer = 1; + } + + app1 = find_marker(src, src_size, 0xE1); + if (app1 && src_size - (app1 - src) >= 16) { + if (memcmp(app1 + 4, "EPJPGBYR", 8) == 0) { + s->is_aiptek_bayer = 1; + } + } + + if (s->is_aiptek_bayer) { + av_log(avctx, AV_LOG_INFO, "Detected AIPTEK Bayer stream. Enabling AJPG pipeline.\n"); + } else { + av_log(avctx, AV_LOG_INFO, "Detected standard MJPEG stream. Bypassing AJPG pipeline.\n"); + } + s->format_probed = 1; + } + + /* --- MJPEG Passthrough --- */ + if (!s->is_aiptek_bayer) { + /* Pass the actual avpkt, NOT the empty s->pkt! */ + ret = avcodec_send_packet(s->mjpeg_avctx, avpkt); + if (ret < 0) + return ret; + + ret = avcodec_receive_frame(s->mjpeg_avctx, frame); + if (ret < 0) + return ret; + + avctx->width = s->mjpeg_avctx->width; + avctx->height = s->mjpeg_avctx->height; + avctx->pix_fmt = s->mjpeg_avctx->pix_fmt; + + *got_frame = 1; + return avpkt->size; + } + + /* --- 1: Stream Analysis and Patching --- */ + has_dht = (find_marker(src, src_size, 0xC4) != NULL); + sof = find_marker(src, src_size, 0xC0); + needs_sof_patch = 0; + + if (sof && (src_size - (sof - src) >= sizeof(old_sof0))) { + if (memcmp(sof, old_sof0, sizeof(old_sof0)) == 0) + needs_sof_patch = 1; + } + + if (!needs_sof_patch && sof && (src_size - (sof - src) >= 20)) { + if (sof[11] == 0x22) + needs_sof_patch = 1; + } + + if (has_dht && !needs_sof_patch) { + patched = (uint8_t *)src; + patched_size = src_size; + } else { + sos = find_marker(src, src_size, 0xDA); + if (!sos) + return AVERROR_INVALIDDATA; + + pre = (int)(sos - src); + post = src_size - pre; + dht_size = has_dht ? 0 : sizeof(annex_k_dht); + + patched_size = pre + dht_size + post; + patched = av_malloc(patched_size + AV_INPUT_BUFFER_PADDING_SIZE + 2); + if (!patched) + return AVERROR(ENOMEM); + owns_patched = 1; + + if (has_dht) { + memcpy(patched, src, src_size); + } else { + memcpy(patched, src, pre); + memcpy(patched + pre, annex_k_dht, sizeof(annex_k_dht)); + memcpy(patched + pre + sizeof(annex_k_dht), sos, post); + } + + if (needs_sof_patch) + ajpg_patch_sof0(patched, patched_size); + + patched[patched_size] = 0xFF; + patched[patched_size + 1] = 0xD9; + patched_size += 2; + memset(patched + patched_size, 0, AV_INPUT_BUFFER_PADDING_SIZE); + } + + /* --- 2: Feed to internal MJPEG decoder --- */ + s->pkt->data = patched; + s->pkt->size = patched_size; + + ret = avcodec_send_packet(s->mjpeg_avctx, s->pkt); + if (ret < 0) + goto done; + + ret = avcodec_receive_frame(s->mjpeg_avctx, s->tmp); + if (ret < 0) + goto done; + + /* --- THE FIX: Anti-Segfault Check --- */ + /* Ensure all 4 color planes exist before proceeding. If the frame is corrupted, + data[3] will be NULL and attempting to read it will cause a segfault. */ + if (!s->tmp->data[0] || !s->tmp->data[1] || !s->tmp->data[2] || !s->tmp->data[3]) { + av_log(avctx, AV_LOG_WARNING, "Corrupted frame dropped (missing color planes).\n"); + ret = avpkt->size; + *got_frame = 0; + goto done; + } + + /* --- 3a: CMYK Debug View (-bayer none) --- */ + if (s->bayer_pattern == -2) { + int x, y; + avctx->width = s->tmp->width; + avctx->height = s->tmp->height; + avctx->pix_fmt = AV_PIX_FMT_RGB24; + + ret = ff_get_buffer(avctx, frame, 0); + if (ret < 0) + goto done; + + for (y = 0; y < s->tmp->height; y++) { + uint8_t *c_row = s->tmp->data[0] + y * s->tmp->linesize[0]; + uint8_t *m_row = s->tmp->data[1] + y * s->tmp->linesize[1]; + uint8_t *y_row = s->tmp->data[2] + y * s->tmp->linesize[2]; + uint8_t *k_row = s->tmp->data[3] + y * s->tmp->linesize[3]; + + uint8_t *rgb_row = frame->data[0] + y * frame->linesize[0]; + + for (x = 0; x < s->tmp->width; x++) { + int c = 255 - c_row[x]; + int m = 255 - m_row[x]; + int y_ink = 255 - y_row[x]; + int k = 255 - k_row[x]; + + int r = (255 - c) * (255 - k) / 255; + int g = (255 - m) * (255 - k) / 255; + int b = (255 - y_ink) * (255 - k) / 255; + + rgb_row[x * 3 + 0] = av_clip_uint8(r); + rgb_row[x * 3 + 1] = av_clip_uint8(g); + rgb_row[x * 3 + 2] = av_clip_uint8(b); + } + } + + frame->pts = s->tmp->pts; + frame->pkt_dts = s->tmp->pkt_dts; + frame->pict_type = AV_PICTURE_TYPE_I; + + *got_frame = 1; + ret = avpkt->size; + goto done; + } + + /* --- 3b: Bayer Weaving --- */ + avctx->width = s->tmp->width * 2; + avctx->height = s->tmp->height * 2; + avctx->pix_fmt = detect_bayer_pattern(src, src_size, s->bayer_pattern); + + ret = ff_get_buffer(avctx, frame, 0); + if (ret < 0) + goto done; + + { + /* Declare loop variables at top of scope block */ + int x, y; + for (y = 0; y < s->tmp->height; y++) { + uint8_t *dst_row0 = frame->data[0] + (y * 2) * frame->linesize[0]; + uint8_t *dst_row1 = frame->data[0] + (y * 2 + 1) * frame->linesize[0]; + + uint8_t *src_r = s->tmp->data[0] + y * s->tmp->linesize[0]; + uint8_t *src_g1 = s->tmp->data[1] + y * s->tmp->linesize[1]; + uint8_t *src_g2 = s->tmp->data[2] + y * s->tmp->linesize[2]; + uint8_t *src_b = s->tmp->data[3] + y * s->tmp->linesize[3]; + + for (x = 0; x < s->tmp->width; x++) { + dst_row0[x * 2] = src_r[x]; + dst_row0[x * 2 + 1] = src_g1[x]; + dst_row1[x * 2] = src_g2[x]; + dst_row1[x * 2 + 1] = src_b[x]; + } + } + } + + frame->pts = s->tmp->pts; + frame->pkt_dts = s->tmp->pkt_dts; + frame->pict_type = AV_PICTURE_TYPE_I; + + *got_frame = 1; + ret = avpkt->size; + +done: + if (owns_patched) + av_free(patched); + return ret; +} + +/* Register */ +const FFCodec ff_ajpg_decoder = { + .p.name = "ajpg", + CODEC_LONG_NAME("AJPG"), + .p.type = AVMEDIA_TYPE_VIDEO, + .p.id = AV_CODEC_ID_AJPG, + .priv_data_size = sizeof(AJPGContext), + .p.priv_class = &ajpg_class, + .init = ajpg_decode_init, + FF_CODEC_DECODE_CB(ajpg_decode_frame), + .close = ajpg_decode_end, + .p.capabilities = AV_CODEC_CAP_DR1, +}; \ No newline at end of file -- 2.52.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
