avformat/movenc: add support for LCEVC track muxing

Signed-off-by: James Almer <jamrial@gmail.com>
This commit is contained in:
James Almer
2026-02-16 12:02:14 -03:00
parent 77ddfcfeb1
commit 0878ae59f9
9 changed files with 430 additions and 4 deletions
+1
View File
@@ -3,6 +3,7 @@ releases are sorted from youngest to oldest.
version <next>:
- Extend AMF Color Converter (vf_vpp_amf) HDR capabilities
- LCEVC track muxing support in MP4 muxer
version 8.1:
+1
View File
@@ -1135,6 +1135,7 @@ STLIBOBJS-$(CONFIG_IMAGE_JPEGXL_PIPE_DEMUXER) += jpegxl_parse.o
STLIBOBJS-$(CONFIG_JPEGXL_ANIM_DEMUXER) += jpegxl_parse.o
STLIBOBJS-$(CONFIG_MATROSKA_DEMUXER) += mpeg4audio_sample_rates.o
STLIBOBJS-$(CONFIG_MOV_DEMUXER) += ac3_channel_layout_tab.o
STLIBOBJS-$(CONFIG_MOV_MUXER) += h2645_parse.o lcevctab.o
STLIBOBJS-$(CONFIG_MXF_MUXER) += golomb.o
STLIBOBJS-$(CONFIG_MP3_MUXER) += mpegaudiotabs.o
STLIBOBJS-$(CONFIG_NUT_MUXER) += mpegaudiotabs.o
+3 -1
View File
@@ -389,7 +389,8 @@ OBJS-$(CONFIG_MOV_DEMUXER) += mov.o mov_chan.o mov_esds.o \
OBJS-$(CONFIG_MOV_MUXER) += movenc.o \
movenchint.o mov_chan.o rtp.o \
movenccenc.o movenc_ttml.o rawutils.o \
apv.o dovi_isom.o evc.o cbs.o cbs_av1.o cbs_apv.o
apv.o dovi_isom.o evc.o cbs.o cbs_av1.o cbs_apv.o \
lcevc.o
OBJS-$(CONFIG_MP2_MUXER) += rawenc.o
OBJS-$(CONFIG_MP3_DEMUXER) += mp3dec.o replaygain.o
OBJS-$(CONFIG_MP3_MUXER) += mp3enc.o rawenc.o id3v2enc.o
@@ -751,6 +752,7 @@ SHLIBOBJS-$(CONFIG_JPEGXL_ANIM_DEMUXER) += jpegxl_parse.o
SHLIBOBJS-$(CONFIG_MATROSKA_DEMUXER) += mpeg4audio_sample_rates.o
SHLIBOBJS-$(CONFIG_MATROSKA_MUXER) += opus_frame_duration_tab.o
SHLIBOBJS-$(CONFIG_MOV_DEMUXER) += ac3_channel_layout_tab.o
SHLIBOBJS-$(CONFIG_MOV_MUXER) += h2645_parse.o lcevctab.o
SHLIBOBJS-$(CONFIG_MP3_MUXER) += mpegaudiotabs.o
SHLIBOBJS-$(CONFIG_MXF_MUXER) += golomb_tab.o \
rangecoder_dec.o
+19
View File
@@ -0,0 +1,19 @@
/*
* 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 "libavcodec/h2645_parse.c"
+278
View File
@@ -0,0 +1,278 @@
/*
* LCEVC helper functions for muxers
*
* 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 "libavutil/error.h"
#include "libavutil/intreadwrite.h"
#include "libavutil/mem.h"
#include "libavcodec/bytestream.h"
#include "libavcodec/h2645_parse.h"
#include "libavcodec/lcevc.h"
#include "libavcodec/lcevctab.h"
#include "libavcodec/lcevc_parse.h"
#include "avio.h"
#include "avio_internal.h"
#include "lcevc.h"
typedef struct LCEVCDecoderConfigurationRecord {
uint8_t profile_idc;
uint8_t level_idc;
uint8_t chroma_format_idc;
uint8_t bit_depth_luma_minus8;
uint8_t bit_depth_chroma_minus8;
uint32_t pic_width_in_luma_samples;
uint32_t pic_height_in_luma_samples;
} LCEVCDecoderConfigurationRecord;
/**
* Rewrite the NALu stripping the unneeded blocks.
* Given that length fields coded inside the NALu are not aware of any emulation_3bytes
* present in the bitstream, we need to keep track of the raw buffer as we navigate
* the stripped buffer in order to write proper NALu sizes.
*/
static int write_nalu(LCEVCDecoderConfigurationRecord *lvcc, AVIOContext *pb,
const H2645NAL *nal)
{
GetByteContext gbc, raw_gbc;
int64_t start = avio_tell(pb), end;
int sc = 0, gc = 0;
int skipped_byte_pos = 0, nalu_length = 3;
bytestream2_init(&gbc, nal->data, nal->size);
bytestream2_init(&raw_gbc, nal->raw_data, nal->raw_size);
avio_wb16(pb, 0); // size placeholder
avio_wb16(pb, bytestream2_get_be16(&gbc)); // nal_unit_header
bytestream2_skip(&raw_gbc, 2);
while (bytestream2_get_bytes_left(&gbc) > 1 && (!sc || !gc)) {
GetBitContext gb;
uint64_t payload_size;
int payload_size_type, payload_type;
int block_size, raw_block_size, block_end;
init_get_bits8(&gb, gbc.buffer, bytestream2_get_bytes_left(&gbc));
payload_size_type = get_bits(&gb, 3);
payload_type = get_bits(&gb, 5);
payload_size = payload_size_type;
if (payload_size_type == 6)
return AVERROR_PATCHWELCOME;
if (payload_size_type == 7)
payload_size = get_mb(&gb);
if (payload_size > INT_MAX - (get_bits_count(&gb) >> 3))
return AVERROR_INVALIDDATA;
block_size = raw_block_size = payload_size + (get_bits_count(&gb) >> 3);
if (block_size >= bytestream2_get_bytes_left(&gbc))
return AVERROR_INVALIDDATA;
block_end = bytestream2_tell(&gbc) + block_size;
// Take into account removed emulation 3bytes, as payload_size in
// the bitstream is not aware of them.
for (; skipped_byte_pos < nal->skipped_bytes; skipped_byte_pos++) {
if (nal->skipped_bytes_pos[skipped_byte_pos] >= block_end)
break;
raw_block_size++;
}
switch (payload_type) {
case 0:
if (sc)
break;
lvcc->profile_idc = get_bits(&gb, 4);
lvcc->level_idc = get_bits(&gb, 4);
avio_write(pb, raw_gbc.buffer, raw_block_size);
nalu_length += raw_block_size;
sc = 1;
break;
case 1: {
int resolution_type, bit_depth;
int processed_planes_type_flag;
if (gc)
break;
processed_planes_type_flag = get_bits1(&gb);
resolution_type = get_bits(&gb, 6);
skip_bits1(&gb);
lvcc->chroma_format_idc = get_bits(&gb, 2);
skip_bits(&gb, 2);
bit_depth = get_bits(&gb, 2) * 2; // enhancement_depth_type
lvcc->bit_depth_luma_minus8 = bit_depth;
lvcc->bit_depth_chroma_minus8 = bit_depth;
if (resolution_type < 63) {
lvcc->pic_width_in_luma_samples = ff_lcevc_resolution_type[resolution_type].width;
lvcc->pic_height_in_luma_samples = ff_lcevc_resolution_type[resolution_type].height;
} else {
int upsample_type, tile_dimensions_type;
int temporal_step_width_modifier_signalled_flag, level1_filtering_signalled_flag;
// Skip syntax elements until we get to the custom dimension ones
temporal_step_width_modifier_signalled_flag = get_bits1(&gb);
skip_bits(&gb, 3);
upsample_type = get_bits(&gb, 3);
level1_filtering_signalled_flag = get_bits1(&gb);
skip_bits(&gb, 4);
tile_dimensions_type = get_bits(&gb, 2);
skip_bits(&gb, 4);
if (processed_planes_type_flag)
skip_bits(&gb, 4);
if (temporal_step_width_modifier_signalled_flag)
skip_bits(&gb, 8);
if (upsample_type)
skip_bits_long(&gb, 64);
if (level1_filtering_signalled_flag)
skip_bits(&gb, 8);
if (tile_dimensions_type) {
if (tile_dimensions_type == 3)
skip_bits_long(&gb, 32);
skip_bits(&gb, 8);
}
lvcc->pic_width_in_luma_samples = get_bits(&gb, 16);
lvcc->pic_height_in_luma_samples = get_bits(&gb, 16);
}
if (!lvcc->pic_width_in_luma_samples || !lvcc->pic_height_in_luma_samples)
break;
avio_write(pb, raw_gbc.buffer, raw_block_size);
nalu_length += raw_block_size;
gc = 1;
break;
}
case 5:
avio_write(pb, raw_gbc.buffer, raw_block_size);
nalu_length += raw_block_size;
break;
default:
break;
}
bytestream2_skip(&gbc, block_size);
bytestream2_skip(&raw_gbc, raw_block_size);
}
if (!sc || !gc)
return AVERROR_INVALIDDATA;
avio_w8(pb, 0x80); // rbsp_alignment bits
end = avio_tell(pb);
avio_seek(pb, start, SEEK_SET);
avio_wb16(pb, nalu_length);
avio_seek(pb, end, SEEK_SET);
return 0;
}
int ff_isom_write_lvcc(AVIOContext *pb, const uint8_t *data, int len)
{
LCEVCDecoderConfigurationRecord lvcc = { 0 };
AVIOContext *idr_pb = NULL, *nidr_pb = NULL;
H2645Packet h2645_pkt = { 0 };
uint8_t *idr, *nidr;
uint32_t idr_size = 0, nidr_size = 0;
int ret, nb_idr = 0, nb_nidr = 0;
if (len <= 6)
return AVERROR_INVALIDDATA;
/* check for start code */
if (AV_RB32(data) != 0x00000001 &&
AV_RB24(data) != 0x000001) {
avio_write(pb, data, len);
return 0;
}
ret = ff_h2645_packet_split(&h2645_pkt, data, len, NULL, 0, AV_CODEC_ID_LCEVC, 0);
if (ret < 0)
return ret;
ret = avio_open_dyn_buf(&idr_pb);
if (ret < 0)
goto fail;
ret = avio_open_dyn_buf(&nidr_pb);
if (ret < 0)
goto fail;
/* look for IDR or NON_IDR */
for (int i = 0; i < h2645_pkt.nb_nals; i++) {
const H2645NAL *nal = &h2645_pkt.nals[i];
if (nal->type == LCEVC_IDR_NUT) {
nb_idr++;
ret = write_nalu(&lvcc, idr_pb, nal);
if (ret < 0)
return ret;
} else if (nal->type == LCEVC_NON_IDR_NUT) {
nb_nidr++;
ret = write_nalu(&lvcc, nidr_pb, nal);
if (ret < 0)
return ret;
}
}
idr_size = avio_get_dyn_buf(idr_pb, &idr);
nidr_size = avio_get_dyn_buf(nidr_pb, &nidr);
if (!idr_size && !nidr_size) {
ret = AVERROR_INVALIDDATA;
goto fail;
}
avio_w8(pb, 1); /* version */
avio_w8(pb, lvcc.profile_idc);
avio_w8(pb, lvcc.level_idc);
avio_w8(pb, (lvcc.chroma_format_idc << 6) |
(lvcc.bit_depth_luma_minus8 << 3) |
lvcc.bit_depth_chroma_minus8);
avio_w8(pb, 0xff); /* 2 bits nal size length - 1 (11) + 6 bits reserved (111111)*/
avio_wb32(pb, lvcc.pic_width_in_luma_samples);
avio_wb32(pb, lvcc.pic_height_in_luma_samples);
avio_w8(pb, 0xff);
int nb_arrays = !!nb_idr + !!nb_nidr;
avio_w8(pb, nb_arrays);
if (nb_idr) {
avio_w8(pb, LCEVC_IDR_NUT);
avio_wb16(pb, nb_idr);
avio_write(pb, idr, idr_size);
}
if (nb_nidr) {
avio_w8(pb, LCEVC_NON_IDR_NUT);
avio_wb16(pb, nb_idr);
avio_write(pb, nidr, nidr_size);
}
ret = 0;
fail:
ffio_free_dyn_buf(&idr_pb);
ffio_free_dyn_buf(&nidr_pb);
ff_h2645_packet_uninit(&h2645_pkt);
return ret;
}
+29
View File
@@ -0,0 +1,29 @@
/*
* LCEVC helper functions for muxers
*
* 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
*/
#ifndef AVFORMAT_LCEVC_H
#define AVFORMAT_LCEVC_H
#include <stdint.h>
#include "avio.h"
int ff_isom_write_lvcc(AVIOContext *pb, const uint8_t *data, int len);
#endif /* AVFORMAT_LCEVC_H */
+19
View File
@@ -0,0 +1,19 @@
/*
* 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 "libavcodec/lcevctab.c"
+79 -2
View File
@@ -38,6 +38,7 @@
#include "avc.h"
#include "evc.h"
#include "apv.h"
#include "lcevc.h"
#include "libavcodec/ac3_parser_internal.h"
#include "libavcodec/dnxhddata.h"
#include "libavcodec/flac.h"
@@ -1681,6 +1682,19 @@ static int mov_write_evcc_tag(AVIOContext *pb, MOVTrack *track)
return update_size(pb, pos);
}
static int mov_write_lvcc_tag(AVIOContext *pb, MOVTrack *track)
{
int64_t pos = avio_tell(pb);
avio_wb32(pb, 0);
ffio_wfourcc(pb, "lvcC");
ff_isom_write_lvcc(pb, track->extradata[track->last_stsd_index],
track->extradata_size[track->last_stsd_index]);
return update_size(pb, pos);
}
static int mov_write_vvcc_tag(AVIOContext *pb, MOVTrack *track)
{
int64_t pos = avio_tell(pb);
@@ -2880,6 +2894,8 @@ static int mov_write_video_tag(AVFormatContext *s, AVIOContext *pb, MOVMuxContex
}
else if (track->par->codec_id ==AV_CODEC_ID_EVC) {
mov_write_evcc_tag(pb, track);
} else if (track->par->codec_id == AV_CODEC_ID_LCEVC) {
mov_write_lvcc_tag(pb, track);
} else if (track->par->codec_id ==AV_CODEC_ID_APV) {
mov_write_apvc_tag(mov->fc, pb, track);
} else if (track->par->codec_id == AV_CODEC_ID_VP9) {
@@ -5222,6 +5238,9 @@ static int mov_write_moov_tag(AVIOContext *pb, MOVMuxContext *mov,
if (track->tag == MKTAG('r','t','p',' ')) {
track->tref_tag = MKTAG('h','i','n','t');
track->tref_id = mov->tracks[track->src_track].track_id;
} else if (track->tag == MKTAG('l','v','c','1')) {
track->tref_tag = MKTAG('s','b','a','s');
track->tref_id = mov->tracks[track->src_track].track_id;
} else if (track->par->codec_type == AVMEDIA_TYPE_AUDIO) {
const AVPacketSideData *sd = av_packet_side_data_get(track->st->codecpar->coded_side_data,
track->st->codecpar->nb_coded_side_data,
@@ -6847,6 +6866,7 @@ int ff_mov_write_packet(AVFormatContext *s, AVPacket *pkt)
par->codec_id == AV_CODEC_ID_VVC ||
par->codec_id == AV_CODEC_ID_VP9 ||
par->codec_id == AV_CODEC_ID_EVC ||
par->codec_id == AV_CODEC_ID_LCEVC ||
par->codec_id == AV_CODEC_ID_TRUEHD) && !trk->extradata_size[0] &&
!TAG_IS_AVCI(trk->tag)) {
/* copy frame to create needed atoms */
@@ -6957,6 +6977,25 @@ int ff_mov_write_packet(AVFormatContext *s, AVPacket *pkt)
} else {
size = ff_vvc_annexb2mp4(pb, pkt->data, pkt->size, 0, NULL);
}
} else if (par->codec_id == AV_CODEC_ID_LCEVC && trk->extradata_size[trk->last_stsd_index] > 0 &&
*(uint8_t *)trk->extradata[trk->last_stsd_index] != 1) {
/* extradata is Annex B, assume the bitstream is too and convert it */
if (trk->hint_track >= 0 && trk->hint_track < mov->nb_tracks) {
ret = ff_nal_parse_units_buf(pkt->data, &reformatted_data, &size);
if (ret < 0)
return ret;
avio_write(pb, reformatted_data, size);
} else {
if (trk->cenc.aes_ctr) {
size = ff_mov_cenc_avc_parse_nal_units(&trk->cenc, pb, pkt->data, size);
if (size < 0) {
ret = size;
goto err;
}
} else {
size = ff_nal_parse_units(pb, pkt->data, pkt->size);
}
}
} else if (par->codec_id == AV_CODEC_ID_AV1 && !trk->cenc.aes_ctr) {
if (trk->hint_track >= 0 && trk->hint_track < mov->nb_tracks) {
ret = ff_av1_filter_obus_buf(pkt->data, &reformatted_data,
@@ -8041,10 +8080,25 @@ static int mov_init(AVFormatContext *s)
s->streams[0]->disposition |= AV_DISPOSITION_DEFAULT;
}
#if CONFIG_IAMFENC
for (i = 0; i < s->nb_stream_groups; i++) {
AVStreamGroup *stg = s->stream_groups[i];
if (stg->type == AV_STREAM_GROUP_PARAMS_LCEVC) {
if (stg->nb_streams != 2) {
av_log(s, AV_LOG_ERROR, "Exactly two Streams are supported for Stream Groups of type LCEVC\n");
return AVERROR(EINVAL);
}
AVStreamGroupLCEVC *lcevc = stg->params.lcevc;
if (lcevc->lcevc_index > 1)
return AVERROR(EINVAL);
AVStream *st = stg->streams[lcevc->lcevc_index];
if (st->codecpar->codec_id != AV_CODEC_ID_LCEVC) {
av_log(s, AV_LOG_ERROR, "Stream #%u is not an LCEVC stream\n", lcevc->lcevc_index);
return AVERROR(EINVAL);
}
}
#if CONFIG_IAMFENC
if (stg->type != AV_STREAM_GROUP_PARAMS_IAMF_AUDIO_ELEMENT)
continue;
@@ -8062,8 +8116,8 @@ static int mov_init(AVFormatContext *s)
if (!mov->nb_tracks) // We support one track for the entire IAMF structure
mov->nb_tracks++;
}
#endif
}
for (i = 0; i < s->nb_streams; i++) {
AVStream *st = s->streams[i];
@@ -8378,6 +8432,28 @@ static int mov_init(AVFormatContext *s)
}
}
for (i = 0; i < s->nb_stream_groups; i++) {
AVStreamGroup *stg = s->stream_groups[i];
if (stg->type != AV_STREAM_GROUP_PARAMS_LCEVC)
continue;
AVStreamGroupLCEVC *lcevc = stg->params.lcevc;
AVStream *st = stg->streams[lcevc->lcevc_index];
MOVTrack *track = st->priv_data;
for (i = 0; i < mov->nb_tracks; i++) {
MOVTrack *trk = &mov->tracks[i];
if (trk->st == stg->streams[!lcevc->lcevc_index])
break;
}
track->src_track = i;
track->par->width = lcevc->width;
track->par->height = track->height = lcevc->height;
}
enable_tracks(s);
return 0;
}
@@ -8892,6 +8968,7 @@ static const AVCodecTag codec_mp4_tags[] = {
{ AV_CODEC_ID_VVC, MKTAG('v', 'v', 'c', '1') },
{ AV_CODEC_ID_VVC, MKTAG('v', 'v', 'i', '1') },
{ AV_CODEC_ID_EVC, MKTAG('e', 'v', 'c', '1') },
{ AV_CODEC_ID_LCEVC, MKTAG('l', 'v', 'c', '1') },
{ AV_CODEC_ID_APV, MKTAG('a', 'p', 'v', '1') },
{ AV_CODEC_ID_MPEG2VIDEO, MKTAG('m', 'p', '4', 'v') },
{ AV_CODEC_ID_MPEG1VIDEO, MKTAG('m', 'p', '4', 'v') },
+1 -1
View File
@@ -32,7 +32,7 @@
#include "version_major.h"
#define LIBAVFORMAT_VERSION_MINOR 13
#define LIBAVFORMAT_VERSION_MICRO 100
#define LIBAVFORMAT_VERSION_MICRO 101
#define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \
LIBAVFORMAT_VERSION_MINOR, \