PR #25433 opened by James Almer (jamrial) URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25433 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25433.patch
Currently supported: - HEVC and jpeg streams - Gain Map and Alpha channel derived images (Separate streams for color and gmap/alpha) - Most common metadata types (Exif, ICC profiles, CLLI) Not yet supported: - VVC and AVC streams - Animated HEIF - grid/iovl derived images >From 1f2c0da77d530f946f1777cf5c67924e653dc8bd Mon Sep 17 00:00:00 2001 From: James Almer <[email protected]> Date: Sat, 17 Oct 2026 21:28:33 -0300 Subject: [PATCH 1/6] avformat: add a HEIF muxer Currently supported: - HEVC and jpeg streams - Gain Map and Alpha channel derived images (Separate streams for color and gmap/alpha) - Most common metadata types (Exif, ICC profiles, CLLI) Not yet supported: - VVC and AVC streams - Animated HEIF - grid/iovl derived images Signed-off-by: James Almer <[email protected]> --- configure | 1 + libavformat/Makefile | 1 + libavformat/allformats.c | 1 + libavformat/heifenc.c | 1535 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 1538 insertions(+) create mode 100644 libavformat/heifenc.c diff --git a/configure b/configure index 083bd652c49..a2f507b8e9e 100755 --- a/configure +++ b/configure @@ -3916,6 +3916,7 @@ flac_demuxer_select="flac_parser" flv_muxer_select="aac_adtstoasc_bsf iso_writer" gxf_muxer_select="pcm_rechunk_bsf" hds_muxer_select="flv_muxer" +heif_muxer_select="iso_writer" hls_demuxer_select="aac_demuxer ac3_demuxer adts_header ac3_parser eac3_demuxer mov_demuxer mpegts_demuxer" hls_muxer_select="mov_muxer mpegts_muxer webvtt_muxer" hxvs_demuxer_select="h264_parser hevc_parser" diff --git a/libavformat/Makefile b/libavformat/Makefile index 92c09714fe7..e7c83dfc9f3 100644 --- a/libavformat/Makefile +++ b/libavformat/Makefile @@ -270,6 +270,7 @@ OBJS-$(CONFIG_HASH_MUXER) += hashenc.o OBJS-$(CONFIG_HCA_DEMUXER) += hca.o OBJS-$(CONFIG_HCOM_DEMUXER) += hcom.o pcm.o OBJS-$(CONFIG_HDS_MUXER) += hdsenc.o +OBJS-$(CONFIG_HEIF_MUXER) += heifenc.o OBJS-$(CONFIG_HEVC_DEMUXER) += hevcdec.o rawdec.o OBJS-$(CONFIG_HEVC_MUXER) += rawenc.o OBJS-$(CONFIG_EVC_DEMUXER) += evcdec.o rawdec.o diff --git a/libavformat/allformats.c b/libavformat/allformats.c index a4e859da333..0cd1997ef65 100644 --- a/libavformat/allformats.c +++ b/libavformat/allformats.c @@ -212,6 +212,7 @@ extern const FFOutputFormat ff_hash_muxer; extern const FFInputFormat ff_hca_demuxer; extern const FFInputFormat ff_hcom_demuxer; extern const FFOutputFormat ff_hds_muxer; +extern const FFOutputFormat ff_heif_muxer; extern const FFInputFormat ff_hevc_demuxer; extern const FFOutputFormat ff_hevc_muxer; extern const FFInputFormat ff_hls_demuxer; diff --git a/libavformat/heifenc.c b/libavformat/heifenc.c new file mode 100644 index 00000000000..5b1e4856b1e --- /dev/null +++ b/libavformat/heifenc.c @@ -0,0 +1,1535 @@ +/* + * HEIF muxer + * Copyright (c) 2026 James Almer + * + * 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 "config_components.h" + +#include <stdint.h> +#include <inttypes.h> + +#include "libavutil/avassert.h" +#include "libavutil/gain_map.h" +#include "libavutil/intreadwrite.h" +#include "libavutil/mastering_display_metadata.h" +#include "libavutil/mem.h" +#include "libavutil/pixdesc.h" +#include "avformat.h" +#include "avio.h" +#include "avio_internal.h" +#include "hevc.h" +#include "internal.h" +#include "mux.h" + +typedef struct ItemInfoEntry { + unsigned int flags; + unsigned int type; + unsigned int id; + uint8_t *idat_payload; + size_t idat_payload_size; + char *name; +} ItemInfoEntry; + +typedef struct ItemLocationEntry { + int construction_method; + int extent_offset; + int extent_length; +} ItemLocationEntry; + +typedef struct ItemPropertyAssociation { + int essential; + int ipco_id; +} ItemPropertyAssociation; + +typedef struct ItemEntry { + ItemInfoEntry infe; + ItemLocationEntry iloc; + ItemPropertyAssociation *ipma; + unsigned int nb_ipma; + struct ItemReference **iref; // reverse reference + int nb_iref; + int seen_pkt; +} ItemEntry; + +typedef struct HEVCConfiguration { + int id; + AVIOContext *pb; +} HEVCConfiguration; + +typedef struct CleanApperture { + unsigned int cropped_width; + unsigned int cropped_height; + AVRational horiz_off; + AVRational vert_off; +} CleanApperture; + +typedef struct ColorInformation { + unsigned int color_type; + enum AVColorPrimaries color_primaries; + enum AVColorTransferCharacteristic transfer_characteristics; + enum AVColorSpace matrix_coefficients; + enum AVColorRange color_range; + uint8_t *icc_profile; + size_t icc_profile_size; +} ColorInformation; + +typedef struct ContentLightLevel { + unsigned int max_content_light_level; + unsigned int max_pic_average_light_level; +} ContentLightLevel; + +typedef struct ImageSpatialExtents { + int width; + int height; +} ImageSpatialExtents; + +typedef struct PixelInformation { + int nb_channels; + int bps[3]; +} PixelInformation; + +typedef struct ImageRotation { + int rotation; +} ImageRotation; + +typedef struct AuxiliaryType { + char *aux_type; +} AuxiliaryType; + +typedef struct ItemProperties { + unsigned int type; + void *ipco; +} ItemProperties; + +typedef struct ItemReference { + int type; + int from_id; + int *to_id; + unsigned int nb_to_id; +} ItemReference; + +typedef struct GroupList { + int type; + int id; + int *entities; + unsigned int nb_entities; +} GroupList; + +typedef struct HEIFMuxContext { + AVIOContext *mdat_pb; + ItemEntry *item; + unsigned int item_alloc_size; + unsigned int nb_items; + ItemProperties *iprp; + unsigned int iprp_alloc_size; + unsigned int nb_iprp; + ItemReference *iref; + unsigned int iref_alloc_size; + unsigned int nb_iref; + GroupList *grpl; + unsigned int grpl_alloc_size; + unsigned int nb_grpl; + unsigned int primary_item; + enum AVCodecID codec_id; +} HEIFMuxContext; + +/** + * Get the requested item. + */ +static ItemEntry *get_heif_item(HEIFMuxContext *heif, unsigned id) +{ + ItemEntry *item = NULL; + + for (int i = 0; i < heif->nb_items; i++) { + if (heif->item[i].infe.id != id) + continue; + + item = &heif->item[i]; + break; + } + + return item; +} + +static ItemProperties *heif_add_iprp_entry(HEIFMuxContext *heif) +{ + ItemProperties *iprp = av_fast_realloc(heif->iprp, &heif->iprp_alloc_size, + (heif->nb_iprp + 1) * sizeof(*heif->iprp)); + if (!iprp) + return NULL; + + heif->iprp = iprp; + iprp = &heif->iprp[heif->nb_iprp]; + + return iprp; +} + +static int heif_add_ipma_entry(ItemEntry *item, int essential, int id) +{ + ItemPropertyAssociation *ipma = av_dynarray2_add((void**)&item->ipma, + &item->nb_ipma, sizeof(*ipma), NULL); + if (!ipma) + return AVERROR(ENOMEM); + *ipma = (ItemPropertyAssociation) { essential, id }; + + return 0; +} + +#define IPCO_TYPE_CHECK(ipco_var, ipco_type) \ + ItemProperties *iprp = &heif->iprp[j]; \ + if (iprp->type != ipco_type) \ + continue; \ + ipco_var = iprp->ipco + +#define IPCO_TYPE_SET(ipco_var, ipco_type) \ + ItemProperties *iprp = heif_add_iprp_entry(heif); \ + if (!iprp) \ + return AVERROR(ENOMEM); \ + iprp->type = ipco_type; \ + iprp->ipco = av_mallocz(sizeof(*ipco_var)); \ + if (!iprp->ipco) \ + return AVERROR(ENOMEM); \ + ipco_var = iprp->ipco + +static int heif_build_hvcc(AVFormatContext *s, ItemEntry *item, + const AVCodecParameters *par) +{ + HEIFMuxContext *heif = s->priv_data; + HEVCConfiguration *hvcc; + AVIOContext *pb; + uint8_t *buf; + int buf_size; + int ret, j; + + ret = avio_open_dyn_buf(&pb); + if (ret < 0) + return ret; + + ret = ff_isom_write_hvcc(pb, par->extradata, par->extradata_size, 1, s); + if (ret < 0) + goto fail; + + buf_size = avio_get_dyn_buf(pb, &buf); + if (!buf_size) { + ret = AVERROR(EINVAL); + goto fail; + } + + for (j = 0; j < heif->nb_iprp; j++) { + uint8_t *ref; + int ref_size; + + IPCO_TYPE_CHECK(hvcc, MKTAG('h','v','c','C')); + ref_size = avio_get_dyn_buf(hvcc->pb, &ref); + if (!ref_size || buf_size != ref_size) + continue; + if (!memcmp(buf, ref, buf_size)) + break; + } + if (j == heif->nb_iprp) { + ItemProperties *iprp = heif_add_iprp_entry(heif); + if (!iprp) { + ret = AVERROR(ENOMEM); + goto fail; + } + iprp->type = MKTAG('h','v','c','C'); + iprp->ipco = av_mallocz(sizeof(*hvcc)); + if (!iprp->ipco) { + ret = AVERROR(ENOMEM); + goto fail; + } + + hvcc = iprp->ipco; + hvcc->pb = pb; + + heif->nb_iprp++; + } else + ffio_free_dyn_buf(&pb); + + return heif_add_ipma_entry(item, 1, j + 1); +fail: + ffio_free_dyn_buf(&pb); + + return ret; +} + +static int heif_build_ispe(AVFormatContext *s, ItemEntry *item, int width, int height) +{ + HEIFMuxContext *heif = s->priv_data; + ImageSpatialExtents *ispe; + int j; + + for (j = 0; j < heif->nb_iprp; j++) { + IPCO_TYPE_CHECK(ispe, MKTAG('i','s','p','e')); + if (ispe->width == width && + ispe->height == height) + break; + } + if (j == heif->nb_iprp) { + IPCO_TYPE_SET(ispe, MKTAG('i','s','p','e')); + ispe->width = width; + ispe->height = height; + + heif->nb_iprp++; + } + + return heif_add_ipma_entry(item, 1, j + 1); +} + +static int heif_build_clap(AVFormatContext *s, ItemEntry *item, + const AVCodecParameters *par, const AVPacketSideData *sd) +{ + HEIFMuxContext *heif = s->priv_data; + CleanApperture *clap; + int j; + unsigned int top = AV_RL32(sd->data + 0); + unsigned int bottom = AV_RL32(sd->data + 4); + unsigned int left = AV_RL32(sd->data + 8); + unsigned int right = AV_RL32(sd->data + 12); + unsigned int cropped_width, cropped_height; + AVRational horiz_off, vert_off; + + if ((left + (int64_t)right) >= par->width || + (top + (int64_t)bottom) >= par->height) { + av_log(s, AV_LOG_ERROR, "Invalid cropping dimensions in stream side data\n"); + return AVERROR(EINVAL); + } + if (!top && !bottom && !left && !right) + return 0; + + cropped_width = par->width - left - right; + cropped_height = par->height - top - bottom; + horiz_off = + av_sub_q((AVRational) { par->width - cropped_width, 2 }, + (AVRational) { left, 1 }); + vert_off = + av_sub_q((AVRational) { par->height - cropped_height, 2 }, + (AVRational) { top, 1 }); + + for (j = 0; j < heif->nb_iprp; j++) { + IPCO_TYPE_CHECK(clap, MKTAG('c','l','a','p')); + if (clap->cropped_width == cropped_width && + clap->cropped_height == cropped_height && + !av_cmp_q(clap->horiz_off, horiz_off) && + !av_cmp_q(clap->vert_off, vert_off)) + break; + } + if (j == heif->nb_iprp) { + IPCO_TYPE_SET(clap, MKTAG('c','l','a','p')); + clap->cropped_width = cropped_width; + clap->cropped_height = cropped_height; + clap->horiz_off = horiz_off; + clap->vert_off = vert_off; + + heif->nb_iprp++; + } + + return heif_add_ipma_entry(item, 1, j + 1); +} + +static int heif_build_colr(AVFormatContext *s, ItemEntry *item, + const AVCodecParameters *par, const AVPacketSideData *sd) +{ + HEIFMuxContext *heif = s->priv_data; + ColorInformation *colr = NULL; + int j; + + av_assert0(par || sd); + + for (j = 0; j < heif->nb_iprp; j++) { + IPCO_TYPE_CHECK(colr, MKTAG('c','o','l','r')); + switch (colr->color_type) { + case MKTAG('p','r','o','f'): + if (!sd || sd->size != colr->icc_profile_size || + memcmp(sd->data, colr->icc_profile, sd->size)) + continue; + break; + case MKTAG('n','c','l','x'): + if (!par || + colr->color_primaries != par->color_primaries || + colr->transfer_characteristics != par->color_trc || + colr->matrix_coefficients != par->color_space || + colr->color_range != par->color_range) + continue; + break; + } + break; + } + if (j == heif->nb_iprp) { + IPCO_TYPE_SET(colr, MKTAG('c','o','l','r')); + if (sd) { + colr->color_type = MKTAG('p','r','o','f'); + colr->icc_profile = av_memdup(sd->data, sd->size); + if (!colr->icc_profile) { + av_freep(&iprp->ipco); + return AVERROR(ENOMEM); + } + colr->icc_profile_size = sd->size; + } else { + colr->color_type = MKTAG('n','c','l','x'); + colr->color_primaries = par->color_primaries; + colr->transfer_characteristics = par->color_trc; + colr->matrix_coefficients = par->color_space; + colr->color_range = par->color_range; + } + + heif->nb_iprp++; + } + + return heif_add_ipma_entry(item, 0, j + 1); +} + +static int heif_build_clli(AVFormatContext *s, ItemEntry *item, + const AVPacketSideData *sd) +{ + HEIFMuxContext *heif = s->priv_data; + ContentLightLevel *clli; + const AVContentLightMetadata *metadata = + (const AVContentLightMetadata *)sd->data; + int j; + + for (j = 0; j < heif->nb_iprp; j++) { + IPCO_TYPE_CHECK(clli, MKTAG('c','l','l','i')); + if (clli->max_content_light_level == metadata->MaxCLL && + clli->max_pic_average_light_level == metadata->MaxFALL) + break; + } + if (j == heif->nb_iprp) { + IPCO_TYPE_SET(clli, MKTAG('c','l','l','i')); + clli->max_content_light_level = metadata->MaxCLL; + clli->max_pic_average_light_level = metadata->MaxFALL; + + heif->nb_iprp++; + } + + return heif_add_ipma_entry(item, 1, j + 1); +} + +static int heif_build_auxc(AVFormatContext *s, ItemEntry *item, + enum AVStreamGroupParamsType type) +{ + HEIFMuxContext *heif = s->priv_data; + AuxiliaryType *auxc; + const char alpha_hevc_urn[] = "urn:mpeg:hevc:2015:auxid:1"; + const char alpha_mpegb_urn[] = "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"; + const char *alpha_urn = heif->codec_id == AV_CODEC_ID_HEVC + ? alpha_hevc_urn + : alpha_mpegb_urn; + const char *urn = NULL; + int j; + + for (j = 0; j < heif->nb_iprp; j++) { + IPCO_TYPE_CHECK(auxc, MKTAG('a','u','x','C')); + switch (type) { + case AV_STREAM_GROUP_PARAMS_ALPHA: + if (strstr(auxc->aux_type, alpha_urn)) + urn = alpha_urn; + break; + default: + av_assert0(0); + } + if (urn) + break; + } + if (j == heif->nb_iprp) { + IPCO_TYPE_SET(auxc, MKTAG('a','u','x','C')); + switch (type) { + case AV_STREAM_GROUP_PARAMS_ALPHA: + urn = alpha_urn; + break; + default: + av_assert0(0); + } + auxc->aux_type = av_strdup(urn); + if (!auxc->aux_type) + return AVERROR(ENOMEM); + + heif->nb_iprp++; + } + + return heif_add_ipma_entry(item, 1, j + 1); +} + +static int heif_build_pixi(AVFormatContext *s, ItemEntry *item, + const AVCodecParameters *par) +{ + HEIFMuxContext *heif = s->priv_data; + PixelInformation *pixi; + const AVPixFmtDescriptor *pixdesc = av_pix_fmt_desc_get(par->format); + int j; + + for (j = 0; j < heif->nb_iprp; j++) { + int k; + IPCO_TYPE_CHECK(pixi, MKTAG('p','i','x','i')); + if (pixi->nb_channels != pixdesc->nb_components) + continue; + for (k = 0; k < pixdesc->nb_components; k++) + if (pixi->bps[k] != pixdesc->comp[k].depth) + break; + if (k == pixdesc->nb_components) + break; + } + if (j == heif->nb_iprp) { + IPCO_TYPE_SET(pixi, MKTAG('p','i','x','i')); + pixi->nb_channels = pixdesc->nb_components; + for (int j = 0; j < 3; j++) + pixi->bps[j] = pixdesc->comp[j].depth; + + heif->nb_iprp++; + } + + return heif_add_ipma_entry(item, 0, j + 1); +} + +static int heif_build_iprp_entries(AVFormatContext *s, ItemEntry *item, + const AVCodecParameters *par, + AVPacketSideData *side_data, int nb_side_data) +{ + const AVPacketSideData *sd; + int ret; + + sd = av_packet_side_data_get(side_data, nb_side_data, + AV_PKT_DATA_ICC_PROFILE); + ret = heif_build_colr(s, item, par, sd); + if (ret < 0) + return ret; + + sd = av_packet_side_data_get(side_data, nb_side_data, + AV_PKT_DATA_CONTENT_LIGHT_LEVEL); + if (sd) { + ret = heif_build_clli(s, item, sd); + if (ret < 0) + return ret; + } + + ret = heif_build_ispe(s, item, par->width, par->height); + if (ret < 0) + return ret; + + ret = heif_build_pixi(s, item, par); + if (ret < 0) + return ret; + + sd = av_packet_side_data_get(side_data, nb_side_data, + AV_PKT_DATA_FRAME_CROPPING); + if (sd && sd->size >= sizeof(uint32_t) * 4) { + ret = heif_build_clap(s, item, par, sd); + if (ret < 0) + return ret; + } + + return 0; +} + +static int heif_alloc_gain_map(AVFormatContext *s, const AVPacketSideData *sd, ItemEntry *item) +{ + int ret; + + item->infe.idat_payload = av_mallocz(AV_GAIN_MAP_MAX_PAYLOAD_SIZE); + if (!item->infe.idat_payload) + return AVERROR(ENOMEM); + + ret = av_gain_map_params_to_iso21496((AVGainMapParams *)sd->data, item->infe.idat_payload); + if (ret < 0) { + av_log(s, AV_LOG_ERROR, "Failed to parse Gain Map parameters\n"); + return ret; + } + + item->infe.idat_payload_size = ret; + + return 0; +} + +static int heif_init_gain_map(AVFormatContext *s, const AVStreamGroup *stg, ItemEntry *item) +{ + HEIFMuxContext *heif = s->priv_data; + const AVStreamGroupLayeredVideo *layered_video = stg->params.layered_video; + const AVStream *base_st = stg->entries[!layered_video->el_index]->stream; + const AVStream *gain_st = stg->entries[ layered_video->el_index]->stream; + const AVPacketSideData *sd = av_packet_side_data_get(gain_st->codecpar->coded_side_data, + gain_st->codecpar->nb_coded_side_data, + AV_PKT_DATA_GAIN_MAP_PARAMS); + int ret; + + if (!(gain_st->disposition & AV_DISPOSITION_DEPENDENT)) { + av_log(s, AV_LOG_ERROR, "Gain Map group #%d needs the gain map stream to have the " + "dependent disposition\n", stg->index); + return AVERROR(EINVAL); + } + + item->infe.type = MKTAG('t','m','a','p'); + item->infe.id = heif->nb_items; + item->iloc.construction_method = 1; + + if (sd) { + ret = heif_alloc_gain_map(s, sd, item); + if (ret < 0) + return ret; + } + + ret = heif_build_iprp_entries(s, item, base_st->codecpar, + stg->coded_side_data, + stg->nb_coded_side_data); + if (ret < 0) + return ret; + + return 0; +} + +static int heif_init(AVFormatContext *s) +{ + HEIFMuxContext *heif = s->priv_data; + int ret; + + heif->item = av_fast_realloc(NULL, &heif->item_alloc_size, + s->nb_streams * sizeof(*heif->item)); + if (!heif->item) + return AVERROR(ENOMEM); + + ret = avio_open_dyn_buf(&heif->mdat_pb); + if (ret < 0) + return ret; + + heif->codec_id = AV_CODEC_ID_NONE; + + // Build stream items + for (int i = 0; i < s->nb_streams; i++) { + AVStream *st = s->streams[i]; + AVCodecParameters *par = st->codecpar; + ItemEntry *item = &heif->item[i]; + const AVDictionaryEntry *title; + + if (heif->codec_id != AV_CODEC_ID_NONE && heif->codec_id != par->codec_id) { + av_log(s, AV_LOG_ERROR, "All streams must use the same video codec\n"); + return AVERROR_PATCHWELCOME; + } + + memset(item, 0, sizeof(*item)); + heif->nb_items++; + + switch (par->codec_id) { + case AV_CODEC_ID_HEVC: + if (!par->extradata_size) { + av_log(s, AV_LOG_ERROR, "Missing extradata on stream #%d\n", i); + return AVERROR(EINVAL); + } + + ret = heif_build_hvcc(s, item, par); + if (ret < 0) + return ret; + break; + case AV_CODEC_ID_MJPEG: + break; + default: + av_log(s, AV_LOG_ERROR, "Unsupported video codec\n"); + return AVERROR(EINVAL); + } + heif->codec_id = par->codec_id; + + ret = heif_build_iprp_entries(s, item, par, + par->coded_side_data, + par->nb_coded_side_data); + if (ret < 0) + return ret; + + item->infe.type = par->codec_tag; + item->infe.id = heif->nb_items; + + title = av_dict_get(st->metadata, "title", NULL, 0); + if (title) { + item->infe.name = av_strdup(title->value); + if (!item->infe.name) + return AVERROR(ENOMEM); + } + + if (!heif->primary_item && (st->disposition & AV_DISPOSITION_DEFAULT)) + heif->primary_item = item->infe.id; + else + item->infe.flags |= !!(st->disposition & AV_DISPOSITION_DEPENDENT); + } + + // Build stream group items + for (int i = 0; i < s->nb_stream_groups; i++) { + const AVStreamGroup *stg = s->stream_groups[i]; + const AVDictionaryEntry *title; + ItemEntry *item; + ItemReference *iref; + + switch (stg->type) { + case AV_STREAM_GROUP_PARAMS_TILE_GRID: + av_log(s, AV_LOG_ERROR, "Tile Grid derived images are not supported\n"); + return AVERROR_PATCHWELCOME; + case AV_STREAM_GROUP_PARAMS_GAIN_MAP: + if (stg->nb_entries != 2) { + av_log(s, AV_LOG_ERROR, "Invalid stream count in Gain Map group\n"); + return AVERROR(EINVAL); + } + break; + default: + continue; + } + + for (int j = 0; j < stg->nb_entries; j++) { + AVStreamGroupEntry *entry = stg->entries[j]; + + if (entry->type != AV_STREAM_GROUP_ENTRY_STREAM) { + av_log(s, AV_LOG_ERROR, "Derived images referencing other derived images is " + "not supported\n"); + return AVERROR_PATCHWELCOME; + } + } + + item = av_fast_realloc(heif->item, &heif->item_alloc_size, + (heif->nb_items + 1) * sizeof(*heif->item)); + if (!item) + return AVERROR(ENOMEM); + + heif->item = item; + item = &heif->item[heif->nb_items++]; + memset(item, 0, sizeof(*item)); + + switch (stg->type) { + case AV_STREAM_GROUP_PARAMS_GAIN_MAP: + ret = heif_init_gain_map(s, stg, item); + if (ret < 0) + return ret; + break; + default: + av_assert0(0); + } + + iref = av_fast_realloc(heif->iref, &heif->iref_alloc_size, + (heif->nb_iref + 1) * sizeof(*heif->iref)); + if (!iref) + return AVERROR(ENOMEM); + + heif->iref = iref; + iref = &heif->iref[heif->nb_iref]; + + iref->type = MKTAG('d','i','m','g'); + iref->from_id = item->infe.id; + iref->to_id = av_calloc(stg->nb_entries, sizeof(*iref->to_id)); + if (!iref->to_id) + return AVERROR(ENOMEM); + iref->nb_to_id = stg->nb_entries; + + heif->nb_iref++; + + for (int j = 0; j < stg->nb_entries; j++) { + AVStreamGroupEntry *entry = stg->entries[j]; + ItemEntry *to_item = &heif->item[entry->stream->index]; + + av_dynarray_add(&to_item->iref, &to_item->nb_iref, iref); + if (!to_item->nb_iref) + return AVERROR(ENOMEM); + + iref->to_id[j] = to_item->infe.id; + } + + title = av_dict_get(stg->metadata, "title", NULL, 0); + if (title) { + item->infe.name = av_strdup(title->value); + if (!item->infe.name) + return AVERROR(ENOMEM); + } + + if (!heif->primary_item && (stg->disposition & AV_DISPOSITION_DEFAULT)) + heif->primary_item = item->infe.id; + } + + // Build metadata items from streams + for (int i = 0; i < s->nb_streams; i++) { + AVStream *st = s->streams[i]; + ItemEntry *item, *to_item = &heif->item[i]; + ItemReference *iref; + const AVPacketSideData *sd; + + sd = av_packet_side_data_get(st->codecpar->coded_side_data, + st->codecpar->nb_coded_side_data, + AV_PKT_DATA_EXIF); + if (!sd) + continue; + + item = av_fast_realloc(heif->item, &heif->item_alloc_size, + (heif->nb_items + 1) * sizeof(*heif->item)); + if (!item) + return AVERROR(ENOMEM); + + heif->item = item; + item = &heif->item[heif->nb_items++]; + memset(item, 0, sizeof(*item)); + + item->infe.idat_payload = av_memdup(sd->data, sd->size); + if (!item->infe.idat_payload) + return AVERROR(ENOMEM); + item->infe.idat_payload_size = sd->size; + item->infe.type = MKTAG('E','x','i','f'); + item->infe.id = heif->nb_items; + item->infe.flags |= 1; + item->iloc.construction_method = 1; + + to_item = &heif->item[st->index]; + + iref = av_fast_realloc(heif->iref, &heif->iref_alloc_size, + (heif->nb_iref + 1) * sizeof(*heif->iref)); + if (!iref) + return AVERROR(ENOMEM); + + heif->iref = iref; + iref = &heif->iref[heif->nb_iref]; + + iref->type = MKTAG('c','d','s','c'); + iref->from_id = item->infe.id; + iref->to_id = av_mallocz(sizeof(*iref->to_id)); + if (!iref->to_id) + return AVERROR(ENOMEM); + *iref->to_id = to_item->infe.id; + iref->nb_to_id = 1; + + heif->nb_iref++; + } + + // Build group list entries + for (int i = 0; i < heif->nb_iref; i++) { + const ItemReference *iref = &heif->iref[i]; + const ItemEntry *from_item = &heif->item[iref->from_id - 1]; + GroupList *grpl; + int base_id = 0; + + if (iref->type != MKTAG('d','i','m','g') || + from_item->infe.type != MKTAG('t','m','a','p')) + continue; + + for (int j = 0; j < iref->nb_to_id; j++) { + const ItemEntry *to_item = &heif->item[iref->to_id[j] - 1]; + + if (to_item->infe.flags & 1) + continue; + + base_id = to_item->infe.id; + break; + } + + grpl = av_fast_realloc(heif->grpl, &heif->grpl_alloc_size, + (heif->nb_grpl + 1) * sizeof(*heif->grpl)); + if (!grpl) + return AVERROR(ENOMEM); + + heif->grpl = grpl; + grpl = &heif->grpl[heif->nb_grpl]; + + grpl->type = MKTAG('a','l','t','r'); + grpl->id = heif->nb_items + i + 1; + grpl->entities = av_calloc(2, sizeof(*grpl->entities)); + if (!grpl->entities) + return AVERROR(ENOMEM); + grpl->nb_entities = 2; + + heif->nb_grpl++; + + av_assert0(base_id > 0); + grpl->entities[0] = iref->from_id; + grpl->entities[1] = base_id; + } + + // Build stream group derived item references + for (int i = 0; i < s->nb_stream_groups; i++) { + const AVStreamGroup *stg = s->stream_groups[i]; + const AVStreamGroupLayeredVideo *layered_video; + ItemEntry *from_item, *to_item; + ItemReference *iref; + + if (stg->type != AV_STREAM_GROUP_PARAMS_ALPHA) + continue; + + layered_video = stg->params.layered_video; + from_item = &heif->item[stg->entries[ layered_video->el_index]->stream->index]; + to_item = &heif->item[stg->entries[!layered_video->el_index]->stream->index]; + + iref = av_fast_realloc(heif->iref, &heif->iref_alloc_size, + (heif->nb_iref + 1) * sizeof(*heif->iref)); + if (!iref) + return AVERROR(ENOMEM); + + heif->iref = iref; + iref = &heif->iref[heif->nb_iref]; + + iref->type = MKTAG('a','u','x','l'); + iref->from_id = from_item->infe.id; + iref->to_id = av_malloc(sizeof(*iref->to_id)); + if (!iref->to_id) + return AVERROR(ENOMEM); + *iref->to_id = to_item->infe.id; + iref->nb_to_id = 1; + + heif->nb_iref++; + + ret = heif_build_auxc(s, from_item, stg->type); + if (ret < 0) + return ret; + } + + if (!heif->primary_item) + heif->primary_item = 1; + + return 0; +} + +static int64_t update_size(AVIOContext *pb, int64_t pos) +{ + int64_t curpos = avio_tell(pb); + int64_t ret = avio_seek(pb, pos, SEEK_SET); + if (ret < 0) + return ret; + + avio_wb32(pb, curpos - pos); /* rewrite size */ + + ret = avio_seek(pb, curpos, SEEK_SET); + if (ret < 0) + return ret; + + return curpos - pos; +} + +static int heif_write_header(AVFormatContext *s) +{ + HEIFMuxContext *heif = s->priv_data; + int64_t pos = avio_tell(s->pb); + char heic_brand[] = "heic", jpeg_brand[] = "jpeg"; + char *brand = heif->codec_id == AV_CODEC_ID_HEVC + ? heic_brand : jpeg_brand; + + avio_wb32(s->pb, 0); /* size */ + ffio_wfourcc(s->pb, "ftyp"); + ffio_wfourcc(s->pb, brand); + avio_wb32(s->pb, 0); + ffio_wfourcc(s->pb, "mif1"); + ffio_wfourcc(s->pb, brand); + for (int i = s->nb_streams; i < heif->nb_items; i++) { + ItemEntry * item = &heif->item[i]; + + if (item->infe.type != MKTAG('t','m','a','p')) + continue; + ffio_wfourcc(s->pb, "tmap"); + break; + } + + return update_size(s->pb, pos); +} + +static int heif_write_packet(AVFormatContext *s, AVPacket *pkt) +{ + HEIFMuxContext *heif = s->priv_data; + AVStream *st = s->streams[pkt->stream_index]; + ItemEntry *item = &heif->item[pkt->stream_index]; + int64_t extent_offset = avio_tell(heif->mdat_pb); + int size = pkt->size; + + if (item->seen_pkt) { + av_log(s, AV_LOG_ERROR, "Only one packet per stream is supported\n"); + return AVERROR(ENOSYS); + } + + for (int i = 0; i < item->nb_iref; i++) { + ItemReference *iref = item->iref[i]; + ItemEntry *from_item = get_heif_item(heif, iref->from_id); + + if ((item->infe.flags & 1) && + from_item->infe.type == MKTAG('t','m','a','p') && + !from_item->infe.idat_payload) { + const AVPacketSideData *sd = av_packet_side_data_get(pkt->side_data, + pkt->side_data_elems, + AV_PKT_DATA_GAIN_MAP_PARAMS); + if (!sd) + break; + + int ret = heif_alloc_gain_map(s, sd, from_item); + if (ret < 0) + return ret; + } + } + + if (st->codecpar->extradata_size > 6 && + (AV_RB24(st->codecpar->extradata) == 1 || AV_RB32(st->codecpar->extradata) == 1)) { + /* extradata is Annex B, assume the bitstream is too and convert it */ + size = ff_hevc_annexb2mp4(heif->mdat_pb, pkt->data, pkt->size, 1, NULL); + } else + avio_write(heif->mdat_pb, pkt->data, pkt->size); + + item->seen_pkt = 1; + item->iloc.extent_offset = extent_offset; + item->iloc.extent_length = size; + + return 0; +} + +static int heif_write_hdlr_tag(AVFormatContext *s) +{ + int64_t pos = avio_tell(s->pb); + + avio_wb32(s->pb, 0); /* size */ + ffio_wfourcc(s->pb, "hdlr"); + avio_wb32(s->pb, 0); /* Version & flags */ + avio_wb32(s->pb, 0); /* handler */ + ffio_wfourcc(s->pb, "pict"); /* handler type */ + avio_wb32(s->pb, 0); /* reserved */ + avio_wb32(s->pb, 0); /* reserved */ + avio_wb32(s->pb, 0); /* reserved */ + avio_w8(s->pb, 0); + + return update_size(s->pb, pos); +} + +static int heif_write_iinf_tag(AVFormatContext *s) +{ + HEIFMuxContext *heif = s->priv_data; + int64_t ret, pos = avio_tell(s->pb); + + avio_wb32(s->pb, 0); /* size */ + ffio_wfourcc(s->pb, "iinf"); + avio_wb32(s->pb, 0); /* Version & flags */ + avio_wb16(s->pb, heif->nb_items); /* entry_count */ + + for (int i = 0; i < heif->nb_items; i++) { + ItemEntry *item = &heif->item[i]; + int64_t infe_pos = avio_tell(s->pb); + + avio_wb32(s->pb, 0); /* size */ + ffio_wfourcc(s->pb, "infe"); + avio_w8(s->pb, 2); /* Version */ + avio_wb24(s->pb, item->infe.flags); /* flags */ + avio_wb16(s->pb, item->infe.id); /* item_id */ + avio_wb16(s->pb, 0); /* item_protection_index */ + avio_write(s->pb, av_fourcc2str(item->infe.type), 4); /* item_type */ + if (item->infe.name) + avio_write(s->pb, item->infe.name, strlen(item->infe.name)); /* item_name */ + else + avio_w8(s->pb, 0); /* item_name */ + + ret = update_size(s->pb, infe_pos); + if (ret < 0) + return ret; + } + + return update_size(s->pb, pos); +} + +static int heif_write_pitm_tag(AVFormatContext *s) +{ + HEIFMuxContext *heif = s->priv_data; + int64_t pos = avio_tell(s->pb); + + avio_wb32(s->pb, 0); /* size */ + ffio_wfourcc(s->pb, "pitm"); + avio_wb32(s->pb, 0); /* Version & flags */ + avio_wb16(s->pb, heif->primary_item); /* item_id */ + + return update_size(s->pb, pos); +} + + +static int heif_write_ispe_tag(AVFormatContext *s, ImageSpatialExtents *ispe) +{ + int64_t pos = avio_tell(s->pb); + + avio_wb32(s->pb, 0); /* size */ + ffio_wfourcc(s->pb, "ispe"); + avio_wb32(s->pb, 0); /* Version & flags */ + avio_wb32(s->pb, ispe->width); /* image_width */ + avio_wb32(s->pb, ispe->height); /* image_height */ + + return update_size(s->pb, pos); +} + +static int heif_write_clap_tag(AVFormatContext *s, CleanApperture *clap) +{ + int64_t pos = avio_tell(s->pb); + + avio_wb32(s->pb, 0); + ffio_wfourcc(s->pb, "clap"); + avio_wb32(s->pb, clap->cropped_width); /* apertureWidthN */ + avio_wb32(s->pb, 1); /* apertureWidthD */ + avio_wb32(s->pb, clap->cropped_height); /* apertureHeightN */ + avio_wb32(s->pb, 1); /* apertureHeightD */ + + avio_wb32(s->pb, -clap->horiz_off.num); + avio_wb32(s->pb, clap->horiz_off.den); + avio_wb32(s->pb, -clap->vert_off.num); + avio_wb32(s->pb, clap->vert_off.den); + + return update_size(s->pb, pos); +} + +static int heif_write_colr_tag(AVFormatContext *s, ColorInformation *colr) +{ + int64_t pos = avio_tell(s->pb); + + avio_wb32(s->pb, 0); /* size */ + ffio_wfourcc(s->pb, "colr"); + ffio_wfourcc(s->pb, av_fourcc2str(colr->color_type)); + switch (colr->color_type) { + case MKTAG('n','c','l','x'): + avio_wb16(s->pb, colr->color_primaries); + avio_wb16(s->pb, colr->transfer_characteristics); + avio_wb16(s->pb, colr->matrix_coefficients); + avio_w8(s->pb, (colr->color_range == AVCOL_RANGE_JPEG) << 7); + break; + case MKTAG('p','r','o','f'): + av_assert0(colr->icc_profile_size); + avio_write(s->pb, colr->icc_profile, colr->icc_profile_size); + break; + } + return update_size(s->pb, pos); +} + +static int heif_write_clli_tag(AVFormatContext *s, ContentLightLevel *clli) +{ + int64_t pos = avio_tell(s->pb); + + avio_wb32(s->pb, 0); // size + ffio_wfourcc(s->pb, "clli"); + avio_wb16(s->pb, clli->max_content_light_level); + avio_wb16(s->pb, clli->max_pic_average_light_level); + + return update_size(s->pb, pos); +} + +static int heif_write_auxc_tag(AVFormatContext *s, AuxiliaryType *auxc) +{ + int64_t pos = avio_tell(s->pb); + + avio_wb32(s->pb, 0); /* size */ + ffio_wfourcc(s->pb, "auxC"); + avio_wb32(s->pb, 0); /* Version & flags */ + avio_put_str(s->pb, auxc->aux_type); + + return update_size(s->pb, pos); +} + +static int heif_write_pixi_tag(AVFormatContext *s, PixelInformation *pixi) +{ + int64_t pos = avio_tell(s->pb); + + avio_wb32(s->pb, 0); /* size */ + ffio_wfourcc(s->pb, "pixi"); + avio_wb32(s->pb, 0); /* Version & flags */ + avio_w8(s->pb, pixi->nb_channels); /* num_channels */ + for (int i = 0; i < pixi->nb_channels; ++i) + avio_w8(s->pb, pixi->bps[i]); /* bits_per_channel */ + + return update_size(s->pb, pos); +} + +static int heif_write_hvcc_tag(AVFormatContext *s, HEVCConfiguration *hvcc) +{ + int64_t pos = avio_tell(s->pb); + uint8_t *buf; + int buf_size = avio_get_dyn_buf(hvcc->pb, &buf); + + avio_wb32(s->pb, 0); + ffio_wfourcc(s->pb, "hvcC"); + avio_write(s->pb, buf, buf_size); + + return update_size(s->pb, pos); +} + +static int heif_write_ipco_tag(AVFormatContext *s) +{ + HEIFMuxContext *heif = s->priv_data; + int64_t pos = avio_tell(s->pb); + + avio_wb32(s->pb, 0); /* size */ + ffio_wfourcc(s->pb, "ipco"); + for (int i = 0; i < heif->nb_iprp; i++) { + ItemProperties *iprp = &heif->iprp[i]; + switch (iprp->type) { + case MKTAG('h','v','c','C'): { + HEVCConfiguration *hvcc = iprp->ipco; + heif_write_hvcc_tag(s, hvcc); + break; + } + case MKTAG('i','s','p','e'): { + ImageSpatialExtents *ispe = iprp->ipco; + heif_write_ispe_tag(s, ispe); + break; + } + case MKTAG('c','l','a','p'): { + CleanApperture *clap = iprp->ipco; + heif_write_clap_tag(s, clap); + break; + } + case MKTAG('c','o','l','r'): { + ColorInformation *colr = iprp->ipco; + heif_write_colr_tag(s, colr); + break; + } + case MKTAG('c','l','l','i'): { + ContentLightLevel *clli = iprp->ipco; + heif_write_clli_tag(s, clli); + break; + } + case MKTAG('a','u','x','C'): { + AuxiliaryType *auxc = iprp->ipco; + heif_write_auxc_tag(s, auxc); + break; + } + case MKTAG('p','i','x','i'): { + PixelInformation *pixi = iprp->ipco; + heif_write_pixi_tag(s, pixi); + break; + } + } + } + + return update_size(s->pb, pos); +} + +static int heif_write_ipma_tag(AVFormatContext *s) +{ + HEIFMuxContext *heif = s->priv_data; + int64_t pos = avio_tell(s->pb); + int nb_items = 0; + + for (int i = 0; i < heif->nb_items; i++) { + ItemEntry *item = &heif->item[i]; + + if (!item->nb_ipma) + continue; + nb_items++; + } + + if (!nb_items) + return 0; + + avio_wb32(s->pb, 0); /* size */ + ffio_wfourcc(s->pb, "ipma"); + avio_wb32(s->pb, 0); /* Version & flags */ + avio_wb32(s->pb, nb_items); /* entry_count */ + + for (int i = 0; i < heif->nb_items; i++) { + ItemEntry *item = &heif->item[i]; + + if (!item->nb_ipma) + continue; + + avio_wb16(s->pb, item->infe.id); /* item_ID */ + avio_w8(s->pb, item->nb_ipma); /* association_count */ + for (int j = 0; j < item->nb_ipma; j++) + avio_w8(s->pb, (item->ipma[j].essential << 7) | + item->ipma[j].ipco_id); /* association_count */ + } + + return update_size(s->pb, pos); +} + +static int heif_write_iprp_tag(AVFormatContext *s) +{ + int64_t pos = avio_tell(s->pb); + + avio_wb32(s->pb, 0); /* size */ + ffio_wfourcc(s->pb, "iprp"); + heif_write_ipco_tag(s); + heif_write_ipma_tag(s); + + return update_size(s->pb, pos); +} + +static int heif_write_idat_tag(AVFormatContext *s) +{ + HEIFMuxContext *heif = s->priv_data; + int64_t pos = avio_tell(s->pb); + int i; + + for (i = 0; i < heif->nb_items; i++) { + ItemEntry *item = &heif->item[i]; + + if (item->iloc.construction_method == 1) + break; + } + + if (i == heif->nb_items) + return 0; + + avio_wb32(s->pb, 0); /* size */ + ffio_wfourcc(s->pb, "idat"); + for (int i = 0; i < heif->nb_items; i++) { + ItemEntry *item = &heif->item[i]; + + if (item->iloc.construction_method != 1) + continue; + + item->iloc.extent_offset = avio_tell(s->pb); + + switch (item->infe.type) { + case MKTAG('t','m','a','p'): + if (!item->infe.idat_payload_size) { + av_log(s, AV_LOG_ERROR, "Missing Gain Map payload\n"); + return AVERROR(EINVAL); + } + avio_w8(s->pb, 0); /* version */ + break; + case MKTAG('E','x','i','f'): + avio_wb32(s->pb, 0); /* exif_tiff_header_offset */ + break; + default: + av_assert0(0); + } + avio_write(s->pb, item->infe.idat_payload, item->infe.idat_payload_size); + + item->iloc.extent_length = avio_tell(s->pb) - item->iloc.extent_offset; + item->iloc.extent_offset -= (pos + 8); + } + + return update_size(s->pb, pos); +} + +static int heif_write_iref_tag(AVFormatContext *s) +{ + HEIFMuxContext *heif = s->priv_data; + int64_t ret, pos = avio_tell(s->pb); + + avio_wb32(s->pb, 0); /* size */ + ffio_wfourcc(s->pb, "iref"); + avio_wb32(s->pb, 0); /* version + flags */ + + for (int i = 0; i < heif->nb_iref; i++) { + ItemReference *iref = &heif->iref[i]; + int64_t iref_pos = avio_tell(s->pb); + + avio_wb32(s->pb, 0); /* size */ + ffio_wfourcc(s->pb, av_fourcc2str(iref->type)); + avio_wb16(s->pb, iref->from_id); /* from_item_ID */ + avio_wb16(s->pb, iref->nb_to_id); /* reference_count */ + for (int j = 0; j < iref->nb_to_id; j++) + avio_wb16(s->pb, iref->to_id[j]); /* to_item_ID */ + + ret = update_size(s->pb, iref_pos); + if (ret < 0) + return ret; + } + + return update_size(s->pb, pos); +} + +static int heif_write_iloc_tag(AVFormatContext *s) +{ + HEIFMuxContext *heif = s->priv_data; + int64_t pos = avio_tell(s->pb); + unsigned int extra_offset; + + avio_wb32(s->pb, 0); /* size */ + ffio_wfourcc(s->pb, "iloc"); + avio_w8(s->pb, 1); /* version */ + avio_wb24(s->pb, 0); /* flags */ + avio_w8(s->pb, (4 << 4) + 4); /* offset_size(4) and length_size(4) */ + avio_w8(s->pb, 0); /* base_offset_size(4) and index_size(4) */ + avio_wb16(s->pb, heif->nb_items); /* item_count */ + + extra_offset = avio_tell(s->pb) + heif->nb_items * 16 + 8 /* mdat + size */; + for (int i = 0; i < heif->nb_items; i++) { + ItemEntry *item = &heif->item[i]; + + avio_wb16(s->pb, item->infe.id); /* item_id */ + avio_wb16(s->pb, item->iloc.construction_method); /* construction_method */ + avio_wb16(s->pb, 0); /* data_reference_index */ + avio_wb16(s->pb, 1); /* extent_count */ + avio_wb32(s->pb, item->iloc.extent_offset + + (item->iloc.construction_method ? 0 : extra_offset)); + avio_wb32(s->pb, item->iloc.extent_length); + } + + return update_size(s->pb, pos); +} + +static int heif_write_grpl_tag(AVFormatContext *s) +{ + HEIFMuxContext *heif = s->priv_data; + int64_t pos = avio_tell(s->pb); + + avio_wb32(s->pb, 0); /* size */ + ffio_wfourcc(s->pb, "grpl"); + + for (int i = 0; i < heif->nb_grpl; i++) { + GroupList *grpl = &heif->grpl[i]; + int64_t grpl_pos; + int ret; + + grpl_pos = avio_tell(s->pb); + avio_wb32(s->pb, 0); /* size */ + ffio_wfourcc(s->pb, av_fourcc2str(grpl->type)); + avio_wb32(s->pb, 0); /* version + flags */ + avio_wb32(s->pb, heif->nb_items + i + 1); /* group_id */ + avio_wb32(s->pb, grpl->nb_entities); /* num_entities_in_group */ + for (int j = 0; j < grpl->nb_entities; j++) + avio_wb32(s->pb, grpl->entities[j]); /* entity_id */ + + ret = update_size(s->pb, grpl_pos); + if (ret < 0) + return ret; + } + + return update_size(s->pb, pos); +} + +static int heif_write_meta_tag(AVFormatContext *s) +{ + HEIFMuxContext *heif = s->priv_data; + int64_t pos = avio_tell(s->pb); + int ret; + + avio_wb32(s->pb, 0); /* size */ + ffio_wfourcc(s->pb, "meta"); + avio_wb32(s->pb, 0); + + ret = heif_write_hdlr_tag(s); + if (ret < 0) + return ret; + ret = heif_write_iinf_tag(s); + if (ret < 0) + return ret; + ret = heif_write_pitm_tag(s); + if (ret < 0) + return ret; + ret = heif_write_iprp_tag(s); + if (ret < 0) + return ret; + ret = heif_write_idat_tag(s); + if (ret < 0) + return ret; + if (heif->nb_iref) { + ret = heif_write_iref_tag(s); + if (ret < 0) + return ret; + } + if (heif->nb_grpl) { + ret = heif_write_grpl_tag(s); + if (ret < 0) + return ret; + } + ret = heif_write_iloc_tag(s); + if (ret < 0) + return ret; + + return update_size(s->pb, pos); +} + +static int heif_write_mdat_tag(AVFormatContext *s) +{ + HEIFMuxContext *heif = s->priv_data; + uint8_t *buf; + int buf_size = avio_get_dyn_buf(heif->mdat_pb, &buf); + + if (!buf_size) + return AVERROR(EINVAL); + + avio_wb32(s->pb, buf_size + 8); /* size */ + ffio_wfourcc(s->pb, "mdat"); + avio_write(s->pb, buf, buf_size); + + ffio_free_dyn_buf(&heif->mdat_pb); + + return 0; +} + +static int heif_write_trailer(AVFormatContext *s) +{ + int ret; + + ret = heif_write_meta_tag(s); + if (ret < 0) + return ret; + ret = heif_write_mdat_tag(s); + if (ret < 0) + return ret; + + return 0; +} + +static void heif_deinit(AVFormatContext *s) +{ + HEIFMuxContext *heif = s->priv_data; + + for (int i = 0; i < heif->nb_items; i++) { + ItemEntry *item = &heif->item[i]; + av_freep(&item->ipma); + av_freep(&item->iref); + av_freep(&item->infe.idat_payload); + av_freep(&item->infe.name); + } + av_freep(&heif->item); + + for (int i = 0; i < heif->nb_iprp; i++) { + ItemProperties *iprp = &heif->iprp[i]; + switch (iprp->type) { + case MKTAG('a','u','x','C'): { + AuxiliaryType *auxc = iprp->ipco; + av_freep(&auxc->aux_type); + break; + } + case MKTAG('c','o','l','r'): { + ColorInformation *colr = iprp->ipco; + av_freep(&colr->icc_profile); + break; + } + case MKTAG('h','v','c','C'): { + HEVCConfiguration *hvcc = iprp->ipco; + ffio_free_dyn_buf(&hvcc->pb); + break; + } + } + av_freep(&iprp->ipco); + } + av_freep(&heif->iprp); + + for (int i = 0; i < heif->nb_iref; i++) { + ItemReference *iref = &heif->iref[i]; + av_freep(&iref->to_id); + } + av_freep(&heif->iref); + + for (int i = 0; i < heif->nb_grpl; i++) { + GroupList *grpl = &heif->grpl[i]; + av_freep(&grpl->entities); + } + av_freep(&heif->grpl); + + ffio_free_dyn_buf(&heif->mdat_pb); +} + +static const AVCodecTag codec_heif_tags[] = { + { AV_CODEC_ID_HEVC, MKTAG('h','v','c','1') }, + { AV_CODEC_ID_MJPEG, MKTAG('j','p','e','g') }, + { AV_CODEC_ID_NONE, 0 }, +}; + +const FFOutputFormat ff_heif_muxer = { + .p.name = "heif", + .p.long_name = NULL_IF_CONFIG_SMALL("HEIF"), + .p.mime_type = "image/heif", + .p.extensions = "heic,heif", + .priv_data_size = sizeof(HEIFMuxContext), + .p.video_codec = AV_CODEC_ID_HEVC, + .init = heif_init, + .write_header = heif_write_header, + .write_packet = heif_write_packet, + .write_trailer = heif_write_trailer, + .deinit = heif_deinit, + .p.flags = AVFMT_GLOBALHEADER | AVFMT_NOTIMESTAMPS, + .p.codec_tag = (const AVCodecTag* const []){ codec_heif_tags, NULL }, +}; -- 2.54.0 >From 5fcb399ce2ee6032837c86a72fea5f262b88c18a Mon Sep 17 00:00:00 2001 From: James Almer <[email protected]> Date: Thu, 22 Oct 2026 17:12:16 -0300 Subject: [PATCH 2/6] avcodec/encode: add an internal last_frame_props field With this, encode_set_packet_props() can be called from ff_alloc_packet() and ff_get_encode_buffer() in both single or threaded frame scenarios. This is needed for the next change. Signed-off-by: James Almer <[email protected]> --- libavcodec/avcodec.c | 2 ++ libavcodec/avcodec_internal.h | 2 ++ libavcodec/encode.c | 58 ++++++++++++++++++++++++++++--- libavcodec/frame_thread_encoder.c | 1 + 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/libavcodec/avcodec.c b/libavcodec/avcodec.c index 80f2598a921..a48c565f33d 100644 --- a/libavcodec/avcodec.c +++ b/libavcodec/avcodec.c @@ -471,6 +471,8 @@ av_cold void ff_codec_close(AVCodecContext *avctx) av_refstruct_pool_uninit(&avci->progress_frame_pool); if (av_codec_is_decoder(avctx->codec)) ff_decode_internal_uninit(avctx); + else + ff_encode_internal_uninit(avctx); ff_hwaccel_uninit(avctx); diff --git a/libavcodec/avcodec_internal.h b/libavcodec/avcodec_internal.h index d0091386659..3d95bf34ac9 100644 --- a/libavcodec/avcodec_internal.h +++ b/libavcodec/avcodec_internal.h @@ -85,6 +85,8 @@ struct AVCodecInternal *ff_decode_internal_alloc(void); void ff_decode_internal_sync(struct AVCodecContext *dst, const struct AVCodecContext *src); void ff_decode_internal_uninit(struct AVCodecContext *avctx); +void ff_encode_internal_uninit(struct AVCodecContext *avctx); +int ff_encode_extract_frame_props(struct AVCodecContext *avctx, const AVFrame *frame); struct AVCodecInternal *ff_encode_internal_alloc(void); diff --git a/libavcodec/encode.c b/libavcodec/encode.c index 82ea3aedbb4..1f0a582b18e 100644 --- a/libavcodec/encode.c +++ b/libavcodec/encode.c @@ -52,6 +52,8 @@ typedef struct EncodeContext { * potentially padded with silence). Reject all subsequent frames. */ int last_audio_frame; + + AVFrame *last_frame_props; } EncodeContext; static EncodeContext *encode_ctx(AVCodecInternal *avci) @@ -59,8 +61,12 @@ static EncodeContext *encode_ctx(AVCodecInternal *avci) return (EncodeContext*)avci; } +static int encode_set_packet_props(AVCodecContext *avctx, AVPacket *avpkt); + int ff_alloc_packet(AVCodecContext *avctx, AVPacket *avpkt, int64_t size) { + const FFCodec *const codec = ffcodec(avctx->codec); + int ret; if (size < 0 || size > INT_MAX - AV_INPUT_BUFFER_PADDING_SIZE) { av_log(avctx, AV_LOG_ERROR, "Invalid minimum required packet size %"PRId64" (max allowed is %d)\n", size, INT_MAX - AV_INPUT_BUFFER_PADDING_SIZE); @@ -69,6 +75,13 @@ int ff_alloc_packet(AVCodecContext *avctx, AVPacket *avpkt, int64_t size) av_assert0(!avpkt->data); + if (!(avctx->codec->capabilities & AV_CODEC_CAP_DELAY) || + (codec->caps_internal & FF_CODEC_CAP_EOF_FLUSH)) { + ret = encode_set_packet_props(avctx, avpkt); + if (ret < 0) + return ret; + } + av_fast_padded_malloc(&avctx->internal->byte_buffer, &avctx->internal->byte_buffer_size, size); avpkt->data = avctx->internal->byte_buffer; @@ -105,6 +118,7 @@ int avcodec_default_get_encode_buffer(AVCodecContext *avctx, AVPacket *avpkt, in int ff_get_encode_buffer(AVCodecContext *avctx, AVPacket *avpkt, int64_t size, int flags) { + const FFCodec *const codec = ffcodec(avctx->codec); int ret; if (size < 0 || size > INT_MAX - AV_INPUT_BUFFER_PADDING_SIZE) @@ -112,6 +126,13 @@ int ff_get_encode_buffer(AVCodecContext *avctx, AVPacket *avpkt, int64_t size, i av_assert0(!avpkt->data && !avpkt->buf); + if (!(avctx->codec->capabilities & AV_CODEC_CAP_DELAY) || + (codec->caps_internal & FF_CODEC_CAP_EOF_FLUSH)) { + ret = encode_set_packet_props(avctx, avpkt); + if (ret < 0) + return ret; + } + avpkt->size = size; ret = avctx->get_encode_buffer(avctx, avpkt, flags); if (ret < 0) @@ -216,9 +237,23 @@ int avcodec_encode_subtitle(AVCodecContext *avctx, uint8_t *buf, int buf_size, return ret; } +int ff_encode_extract_frame_props(AVCodecContext *avctx, const AVFrame *frame) +{ + AVCodecInternal *avci = avctx->internal; + EncodeContext *ec = encode_ctx(avci); + int ret = 0; + + av_frame_unref(ec->last_frame_props); + if (frame) + ret = av_frame_copy_props(ec->last_frame_props, frame); + + return ret; +} + int ff_encode_get_frame(AVCodecContext *avctx, AVFrame *frame) { AVCodecInternal *avci = avctx->internal; + int ret; if (avci->draining) return AVERROR_EOF; @@ -226,15 +261,20 @@ int ff_encode_get_frame(AVCodecContext *avctx, AVFrame *frame) if (!avci->buffer_frame->buf[0]) return AVERROR(EAGAIN); + ret = ff_encode_extract_frame_props(avctx, avci->buffer_frame); + if (ret < 0) + return ret; + av_frame_move_ref(frame, avci->buffer_frame); return 0; } -static int encode_set_packet_props(AVCodecContext *avctx, AVPacket *avpkt, const AVFrame *frame) +static int encode_set_packet_props(AVCodecContext *avctx, AVPacket *avpkt) { AVCodecInternal *avci = avctx->internal; EncodeContext *ec = encode_ctx(avci); + const AVFrame *frame = ec->last_frame_props; if (avpkt->pts == AV_NOPTS_VALUE) { avpkt->pts = frame->pts; @@ -314,10 +354,6 @@ int ff_encode_encode_cb(AVCodecContext *avctx, AVPacket *avpkt, // encoders with delay have to set the timestamps themselves if (!(avctx->codec->capabilities & AV_CODEC_CAP_DELAY) || (frame && (codec->caps_internal & FF_CODEC_CAP_EOF_FLUSH))) { - ret = encode_set_packet_props(avctx, avpkt, frame); - if (ret < 0) - goto unref; - ret = ff_encode_reordered_opaque(avctx, avpkt, frame); if (ret < 0) goto unref; @@ -964,6 +1000,10 @@ int ff_encode_preinit(AVCodecContext *avctx) return AVERROR(ENOMEM); } + ec->last_frame_props = av_frame_alloc(); + if (!ec->last_frame_props) + return AVERROR(ENOMEM); + for (int i = 0; ff_sd_global_map[i].packet < AV_PKT_DATA_NB; i++) { const enum AVPacketSideDataType type_packet = ff_sd_global_map[i].packet; const enum AVFrameSideDataType type_frame = ff_sd_global_map[i].frame; @@ -1046,6 +1086,14 @@ AVCodecInternal *ff_encode_internal_alloc(void) return av_mallocz(sizeof(EncodeContext)); } +av_cold void ff_encode_internal_uninit(AVCodecContext *avctx) +{ + AVCodecInternal *avci = avctx->internal; + EncodeContext *ec = encode_ctx(avci); + + av_frame_free(&ec->last_frame_props); +} + AVCPBProperties *ff_encode_add_cpb_side_data(AVCodecContext *avctx) { AVPacketSideData *tmp; diff --git a/libavcodec/frame_thread_encoder.c b/libavcodec/frame_thread_encoder.c index 6af3db722e0..d390ae9f9e7 100644 --- a/libavcodec/frame_thread_encoder.c +++ b/libavcodec/frame_thread_encoder.c @@ -105,6 +105,7 @@ static void * attribute_align_arg worker(void *v){ frame = task->indata; pkt = task->outdata; + ff_encode_extract_frame_props(avctx, frame); ret = ff_encode_encode_cb(avctx, pkt, frame, &task->got_packet); pthread_mutex_lock(&c->finished_task_mutex); task->return_code = ret; -- 2.54.0 >From 63fc49649ad966fe9362dd38ee871986b4f31d3f Mon Sep 17 00:00:00 2001 From: James Almer <[email protected]> Date: Thu, 22 Oct 2026 20:24:25 -0300 Subject: [PATCH 3/6] avcodec/encode: propagate frame side data Much like it's done at init for global side data, copy some input frame side data entries to the output packet. Signed-off-by: James Almer <[email protected]> --- libavcodec/encode.c | 41 ++++++++++++++++++++++ tests/ref/fate/filter-gainmap | 2 +- tests/ref/fate/filter-gainmap-fixed | 2 +- tests/ref/fate/filter-gainmap-luma | 2 +- tests/ref/fate/filter-gainmap-maxrgb | 2 +- tests/ref/fate/jpeg-mpf-gainmap | 2 +- tests/ref/fate/jpg-exif-autorotate | 2 +- tests/ref/fate/jpg-exif-noautorotate | 2 +- tests/ref/fate/jpg-exif-rotation-override | 2 +- tests/ref/fate/tiff-fax-g3 | 2 +- tests/ref/fate/tiff-fax-g3s | 2 +- tests/ref/fate/tiff-icc | 2 +- tests/ref/fate/tiff-lzw-rgbaf32le | 2 +- tests/ref/fate/tiff-lzw-rgbf32le | 2 +- tests/ref/fate/tiff-uncompressed-rgbaf32le | 2 +- tests/ref/fate/tiff-uncompressed-rgbf32le | 2 +- tests/ref/fate/tiff-zip-rgbaf32le | 2 +- tests/ref/fate/tiff-zip-rgbf32le | 2 +- 18 files changed, 58 insertions(+), 17 deletions(-) diff --git a/libavcodec/encode.c b/libavcodec/encode.c index 1f0a582b18e..d71f42d31b6 100644 --- a/libavcodec/encode.c +++ b/libavcodec/encode.c @@ -270,11 +270,52 @@ int ff_encode_get_frame(AVCodecContext *avctx, AVFrame *frame) return 0; } + +static int side_data_map(AVPacket *dst, + AVFrameSideData *const *sd_src, int nb_sd_src, + const SideDataMap *map) + +{ + for (int i = 0; map[i].packet < AV_PKT_DATA_NB; i++) { + const enum AVPacketSideDataType type_packet = map[i].packet; + const enum AVFrameSideDataType type_frame = map[i].frame; + const AVFrameSideData *sd_frame; + AVPacketSideData *sd_packet; + + sd_frame = av_frame_side_data_get(sd_src, nb_sd_src, type_frame); + if (!sd_frame) + continue; + + sd_packet = av_packet_side_data_new(&dst->side_data, &dst->side_data_elems, + type_packet, sd_frame->size, 0); + if (!sd_packet) + return AVERROR(ENOMEM); + + memcpy(sd_packet->data, sd_frame->data, sd_frame->size); + } + + return 0; +} + static int encode_set_packet_props(AVCodecContext *avctx, AVPacket *avpkt) { AVCodecInternal *avci = avctx->internal; EncodeContext *ec = encode_ctx(avci); const AVFrame *frame = ec->last_frame_props; + int ret; + + static const SideDataMap sd[] = { + { AV_PKT_DATA_EXIF, AV_FRAME_DATA_EXIF }, + { AV_PKT_DATA_GAIN_MAP_PARAMS, AV_FRAME_DATA_GAIN_MAP_PARAMS }, + { AV_PKT_DATA_IAMF_MIX_GAIN_PARAM, AV_FRAME_DATA_IAMF_MIX_GAIN_PARAM }, + { AV_PKT_DATA_IAMF_DEMIXING_INFO_PARAM, AV_FRAME_DATA_IAMF_DEMIXING_INFO_PARAM }, + { AV_PKT_DATA_IAMF_RECON_GAIN_INFO_PARAM, AV_FRAME_DATA_IAMF_RECON_GAIN_INFO_PARAM }, + { AV_PKT_DATA_NB }, + }; + + ret = side_data_map(avpkt, frame->side_data, frame->nb_side_data, sd); + if (ret < 0) + return ret; if (avpkt->pts == AV_NOPTS_VALUE) { avpkt->pts = frame->pts; diff --git a/tests/ref/fate/filter-gainmap b/tests/ref/fate/filter-gainmap index 96d59042072..d6dc920d110 100644 --- a/tests/ref/fate/filter-gainmap +++ b/tests/ref/fate/filter-gainmap @@ -3,4 +3,4 @@ #codec_id 0: rawvideo #dimensions 0: 64x48 #sar 0: 1/1 -0, 0, 0, 1, 9216, 0x91dedb2f +0, 0, 0, 1, 9216, 0x91dedb2f, S=1, Gain Map Parameters, 148, 0x36e61447 diff --git a/tests/ref/fate/filter-gainmap-fixed b/tests/ref/fate/filter-gainmap-fixed index 93b721a20ec..43524fbe6fe 100644 --- a/tests/ref/fate/filter-gainmap-fixed +++ b/tests/ref/fate/filter-gainmap-fixed @@ -3,4 +3,4 @@ #codec_id 0: rawvideo #dimensions 0: 64x48 #sar 0: 1/1 -0, 0, 0, 1, 9216, 0x28fde0dc +0, 0, 0, 1, 9216, 0x28fde0dc, S=1, Gain Map Parameters, 148, 0x4789039c diff --git a/tests/ref/fate/filter-gainmap-luma b/tests/ref/fate/filter-gainmap-luma index ea6d074487f..7f4db5fcbf0 100644 --- a/tests/ref/fate/filter-gainmap-luma +++ b/tests/ref/fate/filter-gainmap-luma @@ -3,4 +3,4 @@ #codec_id 0: rawvideo #dimensions 0: 64x48 #sar 0: 1/1 -0, 0, 0, 1, 3072, 0xbfac1585 +0, 0, 0, 1, 3072, 0xbfac1585, S=1, Gain Map Parameters, 148, 0xf1fe094e diff --git a/tests/ref/fate/filter-gainmap-maxrgb b/tests/ref/fate/filter-gainmap-maxrgb index 1d521020042..cd767e967ce 100644 --- a/tests/ref/fate/filter-gainmap-maxrgb +++ b/tests/ref/fate/filter-gainmap-maxrgb @@ -3,4 +3,4 @@ #codec_id 0: rawvideo #dimensions 0: 64x48 #sar 0: 1/1 -0, 0, 0, 1, 3072, 0x2253f63c +0, 0, 0, 1, 3072, 0x2253f63c, S=1, Gain Map Parameters, 148, 0x181009a2 diff --git a/tests/ref/fate/jpeg-mpf-gainmap b/tests/ref/fate/jpeg-mpf-gainmap index e39f5d8d979..3961959c240 100644 --- a/tests/ref/fate/jpeg-mpf-gainmap +++ b/tests/ref/fate/jpeg-mpf-gainmap @@ -11,7 +11,7 @@ #dimensions 1: 64x48 #sar 1: 1/1 0, 0, 0, 1, 9216, 0xdfa842b3 -1, 0, 0, 1, 9216, 0x662fcc6a +1, 0, 0, 1, 9216, 0x662fcc6a, S=1, Gain Map Parameters, 148, 0xf4dc167e frame|stream_index=0 frame|stream_index=1|side_datum/gain_map_parameters:side_data_type=Gain map parameters|side_datum/gain_map_parameters:version=0|side_datum/gain_map_parameters:use_base_color_space=1|side_datum/gain_map_parameters:base_hdr_headroom=0/1|side_datum/gain_map_parameters:alternate_hdr_headroom=5895489/1048576|component_entry/rgb:gain_map_min=-3392315/4194304|component_entry/rgb:gain_map_max=13278945/2097152|component_entry/rgb:gamma=691797/1048576|component_entry/rgb:base_offset=1/64|component_entry/rgb:alternate_offset=1/64 stream_group|index=0|nb_streams=2|type=Gain Map (Split base rendition and gain map) diff --git a/tests/ref/fate/jpg-exif-autorotate b/tests/ref/fate/jpg-exif-autorotate index 3210e0a1ee4..4d294dea884 100644 --- a/tests/ref/fate/jpg-exif-autorotate +++ b/tests/ref/fate/jpg-exif-autorotate @@ -5,7 +5,7 @@ #codec_id 0: rawvideo #dimensions 0: 300x200 #sar 0: 1/1 -0, 0, 0, 1, 90000, 0x85315584 +0, 0, 0, 1, 90000, 0x85315584, S=1, EXIF metadata, 360, 0x87f72133 [FRAME] media_type=video stream_index=0 diff --git a/tests/ref/fate/jpg-exif-noautorotate b/tests/ref/fate/jpg-exif-noautorotate index c18cdfff91b..fdb7afe61ee 100644 --- a/tests/ref/fate/jpg-exif-noautorotate +++ b/tests/ref/fate/jpg-exif-noautorotate @@ -5,7 +5,7 @@ e3f1a70f55d680145fca061e4a515133 *tests/data/fate/jpg-exif-noautorotate.image2 #codec_id 0: rawvideo #dimensions 0: 200x300 #sar 0: 1/1 -0, 0, 0, 1, 180000, 0x4c43539a +0, 0, 0, 1, 180000, 0x4c43539a, S=1, EXIF metadata, 144, 0xe072076c [FRAME] media_type=video stream_index=0 diff --git a/tests/ref/fate/jpg-exif-rotation-override b/tests/ref/fate/jpg-exif-rotation-override index 40b758dd2b2..a63fb4260aa 100644 --- a/tests/ref/fate/jpg-exif-rotation-override +++ b/tests/ref/fate/jpg-exif-rotation-override @@ -5,7 +5,7 @@ e91d7401ba21087b06683672b0cdabd3 *tests/data/fate/jpg-exif-rotation-override.ima #codec_id 0: rawvideo #dimensions 0: 200x300 #sar 0: 1/1 -0, 0, 0, 1, 90000, 0x373a5584 +0, 0, 0, 1, 90000, 0x373a5584, S=1, EXIF metadata, 352, 0xca3a1ec0 [FRAME] media_type=video stream_index=0 diff --git a/tests/ref/fate/tiff-fax-g3 b/tests/ref/fate/tiff-fax-g3 index ac1fff91420..cb76ad6573b 100644 --- a/tests/ref/fate/tiff-fax-g3 +++ b/tests/ref/fate/tiff-fax-g3 @@ -3,4 +3,4 @@ #codec_id 0: rawvideo #dimensions 0: 2464x3248 #sar 0: 1/1 -0, 0, 0, 1, 1000384, 0xb758eb8b +0, 0, 0, 1, 1000384, 0xb758eb8b, S=1, EXIF metadata, 255, 0x11f018a0 diff --git a/tests/ref/fate/tiff-fax-g3s b/tests/ref/fate/tiff-fax-g3s index ac1fff91420..5fc9a58b140 100644 --- a/tests/ref/fate/tiff-fax-g3s +++ b/tests/ref/fate/tiff-fax-g3s @@ -3,4 +3,4 @@ #codec_id 0: rawvideo #dimensions 0: 2464x3248 #sar 0: 1/1 -0, 0, 0, 1, 1000384, 0xb758eb8b +0, 0, 0, 1, 1000384, 0xb758eb8b, S=1, EXIF metadata, 391, 0xebdd2fa6 diff --git a/tests/ref/fate/tiff-icc b/tests/ref/fate/tiff-icc index f493629c558..245fba874e9 100644 --- a/tests/ref/fate/tiff-icc +++ b/tests/ref/fate/tiff-icc @@ -5,7 +5,7 @@ bcbf8f9edb2b96020b8cce6f0e27a34e *tests/data/fate/tiff-icc.image2 #codec_id 0: rawvideo #dimensions 0: 1x1 #sar 0: 1/1 -0, 0, 0, 1, 3, 0x034d0182 +0, 0, 0, 1, 3, 0x034d0182, S=1, EXIF metadata, 3348, 0xab6cfbf1 [FRAME] [SIDE_DATA] side_data_type=ICC profile diff --git a/tests/ref/fate/tiff-lzw-rgbaf32le b/tests/ref/fate/tiff-lzw-rgbaf32le index c99aa02ef0d..96dbc7bea3c 100644 --- a/tests/ref/fate/tiff-lzw-rgbaf32le +++ b/tests/ref/fate/tiff-lzw-rgbaf32le @@ -3,4 +3,4 @@ #codec_id 0: rawvideo #dimensions 0: 8x8 #sar 0: 0/1 -0, 0, 0, 1, 1024, 0x877e1d5f +0, 0, 0, 1, 1024, 0x877e1d5f, S=1, EXIF metadata, 354, 0x46071a7a diff --git a/tests/ref/fate/tiff-lzw-rgbf32le b/tests/ref/fate/tiff-lzw-rgbf32le index a6d3fabfda1..d6d82cb6c2a 100644 --- a/tests/ref/fate/tiff-lzw-rgbf32le +++ b/tests/ref/fate/tiff-lzw-rgbf32le @@ -3,4 +3,4 @@ #codec_id 0: rawvideo #dimensions 0: 8x8 #sar 0: 0/1 -0, 0, 0, 1, 768, 0xad26ed90 +0, 0, 0, 1, 768, 0xad26ed90, S=1, EXIF metadata, 330, 0xf8d819c0 diff --git a/tests/ref/fate/tiff-uncompressed-rgbaf32le b/tests/ref/fate/tiff-uncompressed-rgbaf32le index c99aa02ef0d..afdf54b86e5 100644 --- a/tests/ref/fate/tiff-uncompressed-rgbaf32le +++ b/tests/ref/fate/tiff-uncompressed-rgbaf32le @@ -3,4 +3,4 @@ #codec_id 0: rawvideo #dimensions 0: 8x8 #sar 0: 0/1 -0, 0, 0, 1, 1024, 0x877e1d5f +0, 0, 0, 1, 1024, 0x877e1d5f, S=1, EXIF metadata, 206, 0xdb300a0d diff --git a/tests/ref/fate/tiff-uncompressed-rgbf32le b/tests/ref/fate/tiff-uncompressed-rgbf32le index a6d3fabfda1..ce98b6fffde 100644 --- a/tests/ref/fate/tiff-uncompressed-rgbf32le +++ b/tests/ref/fate/tiff-uncompressed-rgbf32le @@ -3,4 +3,4 @@ #codec_id 0: rawvideo #dimensions 0: 8x8 #sar 0: 0/1 -0, 0, 0, 1, 768, 0xad26ed90 +0, 0, 0, 1, 768, 0xad26ed90, S=1, EXIF metadata, 318, 0xd76b1867 diff --git a/tests/ref/fate/tiff-zip-rgbaf32le b/tests/ref/fate/tiff-zip-rgbaf32le index c99aa02ef0d..2542e2b98cb 100644 --- a/tests/ref/fate/tiff-zip-rgbaf32le +++ b/tests/ref/fate/tiff-zip-rgbaf32le @@ -3,4 +3,4 @@ #codec_id 0: rawvideo #dimensions 0: 8x8 #sar 0: 0/1 -0, 0, 0, 1, 1024, 0x877e1d5f +0, 0, 0, 1, 1024, 0x877e1d5f, S=1, EXIF metadata, 354, 0xe22c1a09 diff --git a/tests/ref/fate/tiff-zip-rgbf32le b/tests/ref/fate/tiff-zip-rgbf32le index a6d3fabfda1..e30f6a3c306 100644 --- a/tests/ref/fate/tiff-zip-rgbf32le +++ b/tests/ref/fate/tiff-zip-rgbf32le @@ -3,4 +3,4 @@ #codec_id 0: rawvideo #dimensions 0: 8x8 #sar 0: 0/1 -0, 0, 0, 1, 768, 0xad26ed90 +0, 0, 0, 1, 768, 0xad26ed90, S=1, EXIF metadata, 330, 0xaacc195d -- 2.54.0 >From 00d4b332715f0df29f46785e45b11420778822b6 Mon Sep 17 00:00:00 2001 From: James Almer <[email protected]> Date: Fri, 23 Oct 2026 12:02:05 -0300 Subject: [PATCH 4/6] fftools/ffmpeg_mux_init: add support for Alpha group type Signed-off-by: James Almer <[email protected]> --- fftools/ffmpeg_mux_init.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fftools/ffmpeg_mux_init.c b/fftools/ffmpeg_mux_init.c index 011519f8d0d..5975e57f774 100644 --- a/fftools/ffmpeg_mux_init.c +++ b/fftools/ffmpeg_mux_init.c @@ -2648,6 +2648,7 @@ static int of_map_group(Muxer *mux, AVDictionary **dict, AVBPrint *bp, const cha } case AV_STREAM_GROUP_PARAMS_LCEVC: case AV_STREAM_GROUP_PARAMS_TREF: + case AV_STREAM_GROUP_PARAMS_ALPHA: case AV_STREAM_GROUP_PARAMS_DOLBY_VISION: case AV_STREAM_GROUP_PARAMS_GAIN_MAP: break; @@ -2679,6 +2680,8 @@ static int of_parse_group_token(Muxer *mux, const char *token, char *ptr) { .i64 = AV_STREAM_GROUP_PARAMS_TREF }, .unit = "type" }, { "gain_map", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = AV_STREAM_GROUP_PARAMS_GAIN_MAP }, .unit = "type" }, + { "alpha", NULL, 0, AV_OPT_TYPE_CONST, + { .i64 = AV_STREAM_GROUP_PARAMS_ALPHA }, .unit = "type" }, { NULL }, }; const AVClass class = { -- 2.54.0 >From 3bf1af5ac26cacfb918fb18f9c42c63775126134 Mon Sep 17 00:00:00 2001 From: James Almer <[email protected]> Date: Fri, 23 Oct 2026 18:27:41 -0300 Subject: [PATCH 5/6] tests/fate/image: add tests for the HEIF muxer Signed-off-by: James Almer <[email protected]> --- tests/fate/image.mak | 39 ++++++++++++++++++++- tests/filtergraphs/heif_alpha | 3 ++ tests/ref/fate/heif-mux-still-image-1-item | 9 +++++ tests/ref/fate/heif-mux-still-image-alpha | 15 ++++++++ tests/ref/fate/heif-mux-still-image-gainmap | 17 +++++++++ 5 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 tests/filtergraphs/heif_alpha create mode 100644 tests/ref/fate/heif-mux-still-image-1-item create mode 100644 tests/ref/fate/heif-mux-still-image-alpha create mode 100644 tests/ref/fate/heif-mux-still-image-gainmap diff --git a/tests/fate/image.mak b/tests/fate/image.mak index a42d19861a3..c916a4e5a21 100644 --- a/tests/fate/image.mak +++ b/tests/fate/image.mak @@ -641,6 +641,43 @@ fate-jpeg-mpf-gainmap: CMD = transcode lavfi testsrc2=s=64x48:r=1:d=1 jpeg_mpf \ FATE_FFMPEG_FFPROBE += $(FATE_JPEG_MPF-yes) fate-jpeg-mpf: $(FATE_JPEG_MPF-yes) +# heif muxing - still image with 1 item. +FATE_HEIF_SAMPLES-$(call REMUX, HEIF MOV, HEVC_DECODER HEVC_PARSER) \ + += fate-heif-mux-still-image-1-item +fate-heif-mux-still-image-1-item: CMD = stream_remux mov $(TARGET_SAMPLES)/heif-conformance/C002.heic "" heif "" "" "-c:v copy" + +# heif jpeg muxing - still image with a Gain Map signaled with a tmap derived image +FATE_HEIF-$(call ALLYES, LAVFI_INDEV TESTSRC2_FILTER SCALE_FILTER FORMAT_FILTER \ + SPLIT_FILTER GAINMAP_FILTER MJPEG_ENCODER \ + HEIF_MUXER FILE_PROTOCOL) += fate-heif-mux-still-image-gainmap +fate-heif-mux-still-image-gainmap: tests/data/filtergraphs/jpeg_mpf_gainmap +fate-heif-mux-still-image-gainmap: CMD = transcode lavfi testsrc2=s=64x48:r=1:d=1 heif \ + "-/filter_complex $(TARGET_PATH)/tests/data/filtergraphs/jpeg_mpf_gainmap \ + -map [base] -map [gain] -disposition:v:1 dependent -stream_group type=gain_map:video_size=64x48:st=0:st=1:el_index=1 \ + -pix_fmt yuv444p -c:v mjpeg -dct int" "-c:v copy -map 0" \ + "-of compact -show_entries stream_group=index,id,nb_streams,type:stream_group_components:stream_group_disposition:stream_group_tags:stream_group_side_data:stream_group_stream=index,id:stream_group_stream_disposition:stream_group_stream_side_data_list" + +# heif jpeg muxing - still image with Alpha channel signaled with an auxl reference +tests/data/heif-mux-still-image-alpha.heic: TAG = GEN +tests/data/heif-mux-still-image-alpha.heic: tests/data/filtergraphs/heif_alpha +tests/data/heif-mux-still-image-alpha.heic: ffmpeg$(PROGSSUF)$(EXESUF) | tests/data + $(M)$(TARGET_EXEC) $(TARGET_PATH)/$< -nostdin \ + -i $(TARGET_SAMPLES)/png1/55c99e750a5fd6_50314226.png \ + -sws_flags +accurate_rnd+bitexact \ + -/filter_complex $(TARGET_PATH)/tests/data/filtergraphs/heif_alpha \ + -map "[base]" -map "[alpha]" -disposition:v:1 dependent -stream_group "type=alpha:st=0:st=1:el_index=1" \ + -flags +bitexact -fflags +bitexact -c:v mjpeg -dct int -f heif $(TARGET_PATH)/$@ -y 2>/dev/null + +FATE_HEIF_SAMPLES-$(call ALLYES, SCALE_FILTER FORMAT_FILTER SPLIT_FILTER ALPHAEXTRACT_FILTER \ + PNG_DECODER MJPEG_ENCODER IMAGE_PNG_PIPE_DEMUXER HEIF_MUXER FILE_PROTOCOL) += fate-heif-mux-still-image-alpha +fate-heif-mux-still-image-alpha: tests/data/heif-mux-still-image-alpha.heic +fate-heif-mux-still-image-alpha: CMD = stream_demux mov $(TARGET_PATH)/tests/data/heif-mux-still-image-alpha.heic "" "-c:v copy -map 0:g:0" \ + "-of compact -show_entries stream_group=index,id,nb_streams,type:stream_group_components:stream_group_disposition:stream_group_tags:stream_group_side_data:stream_group_stream=index,id:stream_group_stream_disposition:stream_group_stream_side_data_list" + +FATE_FFMPEG_FFPROBE += $(FATE_HEIF-yes) +FATE_SAMPLES_FFMPEG_FFPROBE += $(FATE_HEIF_SAMPLES-yes) +fate-heif: $(FATE_HEIF-yes) $(FATE_HEIF_SAMPLES-yes) + FATE_IMG2_MUXER-$(call ALLYES, LAVFI_INDEV COLOR_FILTER FORMAT_FILTER \ PGM_ENCODER IMAGE2_MUXER IMAGE2_DEMUXER FILE_PROTOCOL FFPROBE) \ += fate-img2-update-filemtime @@ -653,4 +690,4 @@ FATE_SAMPLES_FFMPEG += $(FATE_IMAGE) FATE_SAMPLES_FFPROBE += $(FATE_IMAGE_PROBE) FATE_SAMPLES_FFMPEG_FFPROBE += $(FATE_IMAGE_TRANSCODE) -fate-image: $(FATE_IMAGE) $(FATE_IMAGE_PROBE) $(FATE_IMAGE_TRANSCODE) $(FATE_IMG2_MUXER-yes) $(FATE_JPEG_MPF-yes) +fate-image: $(FATE_IMAGE) $(FATE_IMAGE_PROBE) $(FATE_IMAGE_TRANSCODE) $(FATE_IMG2_MUXER-yes) $(FATE_JPEG_MPF-yes) $(FATE_HEIF-yes) $(FATE_HEIF_SAMPLES-yes) diff --git a/tests/filtergraphs/heif_alpha b/tests/filtergraphs/heif_alpha new file mode 100644 index 00000000000..4ca4c406aee --- /dev/null +++ b/tests/filtergraphs/heif_alpha @@ -0,0 +1,3 @@ +[0:v]split[a][b]; +[a]scale,format=yuv444p[base]; +[b]alphaextract,scale,format=yuv420p[alpha] diff --git a/tests/ref/fate/heif-mux-still-image-1-item b/tests/ref/fate/heif-mux-still-image-1-item new file mode 100644 index 00000000000..64f1db968f8 --- /dev/null +++ b/tests/ref/fate/heif-mux-still-image-1-item @@ -0,0 +1,9 @@ +da9ee2beb0a504c7fec74fbe6aa3b67d *tests/data/fate/heif-mux-still-image-1-item.heif +111923 tests/data/fate/heif-mux-still-image-1-item.heif +#extradata 0: 100, 0xee3e15e9 +#tb 0: 1/1 +#media_type 0: video +#codec_id 0: hevc +#dimensions 0: 1280x720 +#sar 0: 0/1 +0, 0, 0, 1, 111554, 0xa0679859 diff --git a/tests/ref/fate/heif-mux-still-image-alpha b/tests/ref/fate/heif-mux-still-image-alpha new file mode 100644 index 00000000000..62c159424c1 --- /dev/null +++ b/tests/ref/fate/heif-mux-still-image-alpha @@ -0,0 +1,15 @@ +#tb 0: 1/1 +#media_type 0: video +#codec_id 0: mjpeg +#dimensions 0: 330x330 +#sar 0: 0/1 +#tb 1: 1/1 +#media_type 1: video +#codec_id 1: mjpeg +#dimensions 1: 330x330 +#sar 1: 0/1 +0, 0, 0, 1, 20288, 0x486f2063 +1, 0, 0, 1, 5591, 0x402eae5c +stream_group|index=0|id=0x2|nb_streams=2|type=Alpha (Split color and alpha channels)|disposition:default=0|disposition:dub=0|disposition:original=0|disposition:comment=0|disposition:lyrics=0|disposition:karaoke=0|disposition:forced=0|disposition:hearing_impaired=0|disposition:visual_impaired=0|disposition:clean_effects=0|disposition:attached_pic=0|disposition:timed_thumbnails=0|disposition:non_diegetic=0|disposition:captions=0|disposition:descriptions=0|disposition:metadata=0|disposition:dependent=0|disposition:still_image=0|disposition:multilayer=0 +stream|index=0|id=0x1|disposition:default=1|disposition:dub=0|disposition:original=0|disposition:comment=0|disposition:lyrics=0|disposition:karaoke=0|disposition:forced=0|disposition:hearing_impaired=0|disposition:visual_impaired=0|disposition:clean_effects=0|disposition:attached_pic=0|disposition:timed_thumbnails=0|disposition:non_diegetic=0|disposition:captions=0|disposition:descriptions=0|disposition:metadata=0|disposition:dependent=0|disposition:still_image=0|disposition:multilayer=0 +stream|index=1|id=0x2|disposition:default=0|disposition:dub=0|disposition:original=0|disposition:comment=0|disposition:lyrics=0|disposition:karaoke=0|disposition:forced=0|disposition:hearing_impaired=0|disposition:visual_impaired=0|disposition:clean_effects=0|disposition:attached_pic=0|disposition:timed_thumbnails=0|disposition:non_diegetic=0|disposition:captions=0|disposition:descriptions=0|disposition:metadata=0|disposition:dependent=0|disposition:still_image=0|disposition:multilayer=0 diff --git a/tests/ref/fate/heif-mux-still-image-gainmap b/tests/ref/fate/heif-mux-still-image-gainmap new file mode 100644 index 00000000000..00cec836bf2 --- /dev/null +++ b/tests/ref/fate/heif-mux-still-image-gainmap @@ -0,0 +1,17 @@ +c0fbb770d9404a6346e61d6e60b55f16 *tests/data/fate/heif-mux-still-image-gainmap.heif +5108 tests/data/fate/heif-mux-still-image-gainmap.heif +#tb 0: 1/1 +#media_type 0: video +#codec_id 0: mjpeg +#dimensions 0: 64x48 +#sar 0: 1/1 +#tb 1: 1/1 +#media_type 1: video +#codec_id 1: mjpeg +#dimensions 1: 64x48 +#sar 1: 1/1 +0, 0, 0, 1, 2659, 0xe03fbe12 +1, 0, 0, 1, 1955, 0xf1c1f1d5 +stream_group|index=0|id=0x3|nb_streams=2|type=Gain Map (Split base rendition and gain map)|component_entry/gain_map__split_base_rendition_and_gain_map_:el_index=1|component_entry/gain_map__split_base_rendition_and_gain_map_:width=64|component_entry/gain_map__split_base_rendition_and_gain_map_:height=48|disposition:default=0|disposition:dub=0|disposition:original=0|disposition:comment=0|disposition:lyrics=0|disposition:karaoke=0|disposition:forced=0|disposition:hearing_impaired=0|disposition:visual_impaired=0|disposition:clean_effects=0|disposition:attached_pic=0|disposition:timed_thumbnails=0|disposition:non_diegetic=0|disposition:captions=0|disposition:descriptions=0|disposition:metadata=0|disposition:dependent=0|disposition:still_image=0|disposition:multilayer=0 +stream|index=0|id=0x1|disposition:default=1|disposition:dub=0|disposition:original=0|disposition:comment=0|disposition:lyrics=0|disposition:karaoke=0|disposition:forced=0|disposition:hearing_impaired=0|disposition:visual_impaired=0|disposition:clean_effects=0|disposition:attached_pic=0|disposition:timed_thumbnails=0|disposition:non_diegetic=0|disposition:captions=0|disposition:descriptions=0|disposition:metadata=0|disposition:dependent=0|disposition:still_image=0|disposition:multilayer=0 +stream|index=1|id=0x2|disposition:default=0|disposition:dub=0|disposition:original=0|disposition:comment=0|disposition:lyrics=0|disposition:karaoke=0|disposition:forced=0|disposition:hearing_impaired=0|disposition:visual_impaired=0|disposition:clean_effects=0|disposition:attached_pic=0|disposition:timed_thumbnails=0|disposition:non_diegetic=0|disposition:captions=0|disposition:descriptions=0|disposition:metadata=0|disposition:dependent=1|disposition:still_image=0|disposition:multilayer=0|side_datum/gain_map_parameters:side_data_type=Gain Map Parameters|side_datum/gain_map_parameters:version=0|side_datum/gain_map_parameters:use_base_color_space=1|side_datum/gain_map_parameters:base_hdr_headroom=0/1|side_datum/gain_map_parameters:alternate_hdr_headroom=5895489/1048576|side_datum/gain_map_parameters:nb_channels=1|component_entry/gain_map_channel:gain_map_min=-3392315/4194304|component_entry/gain_map_channel:gain_map_max=13278945/2097152|component_entry/gain_map_channel:gamma=691797/104 8576|component_entry/gain_map_channel:base_offset=1/64|component_entry/gain_map_channel:alternate_offset=1/64 -- 2.54.0 >From f251e571e3694a63adcfde6a0f4de4e9030109ac Mon Sep 17 00:00:00 2001 From: James Almer <[email protected]> Date: Fri, 23 Oct 2026 18:28:11 -0300 Subject: [PATCH 6/6] tests/fate/ffmpeg: add a test for alpha merging in ffmpeg.c Signed-off-by: James Almer <[email protected]> --- tests/fate/ffmpeg.mak | 7 +++++++ tests/ref/fate/ffmpeg-heif-merge-alpha | 6 ++++++ 2 files changed, 13 insertions(+) create mode 100644 tests/ref/fate/ffmpeg-heif-merge-alpha diff --git a/tests/fate/ffmpeg.mak b/tests/fate/ffmpeg.mak index aa682ba202f..75aa2e0b42a 100644 --- a/tests/fate/ffmpeg.mak +++ b/tests/fate/ffmpeg.mak @@ -469,3 +469,10 @@ FATE_SAMPLES_FFMPEG-$(call FRAMECRC, MOV, HEVC, HEVC_PARSER) += fate-ffmpeg-heif # binding the internal filtegraph with a caller defined filtergraph fate-ffmpeg-heif-merge-filtergraph: CMD = framecrc -i $(TARGET_SAMPLES)/heif-conformance/C007.heic -filter_complex "sws_flags=+accurate_rnd+bitexact\;[0:g:0]scale=w=1280:h=720[out]" -map "[out]" FATE_SAMPLES_FFMPEG-$(call FRAMECRC, MOV, HEVC, HEVC_PARSER SCALE_FILTER) += fate-ffmpeg-heif-merge-filtergraph + +# alpha merging +fate-ffmpeg-heif-merge-alpha: tests/data/heif-mux-still-image-alpha.heic +fate-ffmpeg-heif-merge-alpha: CMD = framecrc -auto_conversion_filters -i $(TARGET_PATH)/tests/data/heif-mux-still-image-alpha.heic +FATE_SAMPLES_FFMPEG-$(call FRAMECRC, MOV, MJPEG, ALPHAEXTRACT_FILTER ALPHAMERGE_FILTER \ + SCALE_FILTER FORMAT_FILTER SPLIT_FILTER \ + PNG_DECODER MJPEG_ENCODER IMAGE_PNG_PIPE_DEMUXER HEIF_MUXER) += fate-ffmpeg-heif-merge-alpha diff --git a/tests/ref/fate/ffmpeg-heif-merge-alpha b/tests/ref/fate/ffmpeg-heif-merge-alpha new file mode 100644 index 00000000000..1352293e0e1 --- /dev/null +++ b/tests/ref/fate/ffmpeg-heif-merge-alpha @@ -0,0 +1,6 @@ +#tb 0: 1/1 +#media_type 0: video +#codec_id 0: rawvideo +#dimensions 0: 330x330 +#sar 0: 0/1 +0, 0, 0, 1, 435600, 0xedd62079 -- 2.54.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
