/* * 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/fifo.h" #include "libavutil/intreadwrite.h" #include "libavutil/opt.h" #include "libavutil/timestamp.h" #include "libavcodec/bsf.h" #include "libavcodec/bsf_internal.h" enum TrimType { TRIM_PTS, TRIM_DTS, TRIM_PKT_INDEX, TRIM_MSEC_PT, TRIM_MSEC_DT, TRIM_DUR_TS, TRIM_DUR_T_MSEC, }; /* The packet discriminant a bound is compared against; the indexes count consumed * and exported packets. */ enum TrimAxis { AXIS_PTS, AXIS_DTS, AXIS_INDEX, AXIS_OUT_INDEX, }; /* An axis, a unit, and whether the value is relative; the duration types are * the relative forms of pts and msec_pt. */ static const struct { enum TrimAxis axis; int msec; int relative; } trim_types[] = { [TRIM_PTS] = { AXIS_PTS, 0, 0 }, [TRIM_DTS] = { AXIS_DTS, 0, 0 }, [TRIM_PKT_INDEX] = { AXIS_INDEX, 0, 0 }, [TRIM_MSEC_PT] = { AXIS_PTS, 1, 0 }, [TRIM_MSEC_DT] = { AXIS_DTS, 1, 0 }, [TRIM_DUR_TS] = { AXIS_PTS, 0, 1 }, [TRIM_DUR_T_MSEC] = { AXIS_PTS, 1, 1 }, }; typedef struct TrimBound { /* Resolved from the options, immutable once init returns. */ int active; ///< the option was set at all enum TrimAxis axis; int relative; ///< offset is measured from the reference packet int64_t offset; ///< option value converted to axis units /* Rebuilt from the fields above by every reset. */ int64_t value; ///< resolved bound, meaningful once pending is clear int pending; ///< the reference packet has not been seen yet } TrimBound; typedef struct TrimContext { const AVClass *class; int64_t start; int64_t end; int start_type; int end_type; int start_rel; int end_rel; int trim_packets; TrimBound start_bound; TrimBound end_bound; int audio; ///< trim through skip samples side data int64_t pkt_idx; ///< packets consumed so far int64_t out_idx; ///< packets exported so far int64_t skip_carry; ///< samples of head skip left over by dropped packets uint8_t skip_reason; ///< reason that head skip came with int preroll; int preroll_size; AVFifo *preroll_fifo; ///< packets held since the last keyframe, NULL when off AVPacket *pending; ///< in-range packet waiting for the preroll to drain int preroll_full; ///< the group overflowed, so hold nothing until the next one int64_t nb_exported; ///< packets that left the filter int64_t last_pts; ///< the last of them, kept for the closing summary int64_t last_dts; int last_key; } TrimContext; /* Bounds mix option values with stream values; either can overflow. */ static int add_checked(AVBSFContext *ctx, int64_t *acc, int64_t delta, const char *what) { if ((delta > 0 && *acc > INT64_MAX - delta) || (delta < 0 && *acc < INT64_MIN - delta)) { av_log(ctx, AV_LOG_ERROR, "%s is not representable\n", what); return AVERROR(ERANGE); } *acc += delta; return 0; } static int64_t packet_axis(const TrimContext *s, const AVPacket *pkt, enum TrimAxis axis) { switch (axis) { case AXIS_PTS: return pkt->pts; case AXIS_DTS: return pkt->dts; case AXIS_OUT_INDEX: return s->out_idx; default: return s->pkt_idx; } } /* The dts stands in for the pts, being monotonic and never above it. */ static enum TrimAxis monotonic_axis(enum TrimAxis axis) { return axis == AXIS_PTS ? AXIS_DTS : axis; } /* An index counts whole packets, so only time places a bound inside one. */ static int axis_is_time(enum TrimAxis axis) { return axis == AXIS_PTS || axis == AXIS_DTS; } /* Nothing converted here is ever negative, so a negative result is the * INT64_MIN av_rescale_q reports for one it cannot represent. */ static int convert_checked(AVBSFContext *ctx, int64_t *value, AVRational from, AVRational to, const char *what) { int64_t converted = av_rescale_q(*value, from, to); if (converted < 0) { av_log(ctx, AV_LOG_ERROR, "%s is not representable\n", what); return AVERROR(ERANGE); } *value = converted; return 0; } static int time_base_to_samples(AVBSFContext *ctx, int64_t *value, const char *what) { return convert_checked(ctx, value, ctx->time_base_in, (AVRational){ 1, ctx->par_in->sample_rate }, what); } static int samples_to_time_base(AVBSFContext *ctx, int64_t *value, const char *what) { return convert_checked(ctx, value, (AVRational){ 1, ctx->par_in->sample_rate }, ctx->time_base_in, what); } /* The samples a packet decodes to, zero where the codec does not give them. */ static int64_t packet_samples(AVBSFContext *ctx, const AVPacket *pkt) { return FFMAX(av_get_audio_frame_duration2(ctx->par_in, pkt->size), 0); } /* A skip reaching past a dropped packet only needs how much of itself that * packet used up, which a duration still estimates closely enough. */ static int64_t packet_skip_consumed(AVBSFContext *ctx, const AVPacket *pkt) { int64_t nb_samples = packet_samples(ctx, pkt); if (nb_samples > 0) return nb_samples; nb_samples = av_rescale_q(pkt->duration, ctx->time_base_in, (AVRational){ 1, ctx->par_in->sample_rate }); return FFMAX(nb_samples, 0); } /* How far a packet reaches past its axis value. Audio reaches as far as the * samples it decodes to, a duration also covering any gap after them; without * them it reaches nowhere, so no bound can fall inside it. */ static int packet_span(AVBSFContext *ctx, const AVPacket *pkt, enum TrimAxis axis, int64_t *span) { TrimContext *s = ctx->priv_data; *span = 0; if (!axis_is_time(axis)) return 0; if (s->audio) { *span = packet_samples(ctx, pkt); return samples_to_time_base(ctx, span, "packet span"); } *span = FFMAX(pkt->duration, 0); return 0; } static int bound_init(AVBSFContext *ctx, TrimBound *b, int active, int64_t value, int type, int rel, const char *name) { b->active = active; if (!active) return 0; b->axis = trim_types[type].axis; b->relative = rel || trim_types[type].relative; if (trim_types[type].relative && value < 0) { av_log(ctx, AV_LOG_ERROR, "%s duration must not be negative\n", name); return AVERROR(EINVAL); } if (trim_types[type].msec) { int64_t ts = av_rescale_q(value, (AVRational){ 1, 1000 }, ctx->time_base_in); if (ts == INT64_MIN) { av_log(ctx, AV_LOG_ERROR, "%s of %"PRId64" ms is not representable " "in the stream time base\n", name, value); return AVERROR(ERANGE); } value = ts; } b->offset = value; return 0; } static void bound_reset(TrimBound *b) { b->value = b->offset; b->pending = b->relative; } static int bound_resolve(AVBSFContext *ctx, TrimBound *b, int64_t ref, const char *name) { int ret = add_checked(ctx, &b->value, ref, name); if (ret < 0) return ret; b->pending = 0; return 0; } /* How much of the packet lies before the start bound, EAGAIN when all of it * does. */ static int trim_head(AVBSFContext *ctx, const AVPacket *pkt, int64_t *head) { TrimContext *s = ctx->priv_data; int64_t v, span; int ret; if (!s->start_bound.active) return 0; /* A packet without the discriminant a bound uses cannot be placed. */ v = packet_axis(s, pkt, s->start_bound.axis); if (v == AV_NOPTS_VALUE) return AVERROR(EAGAIN); /* The start is measured from the first packet of the stream. */ if (s->start_bound.pending) { ret = bound_resolve(ctx, &s->start_bound, v, "start bound"); if (ret < 0) return ret; } if (v >= s->start_bound.value) return 0; ret = packet_span(ctx, pkt, s->start_bound.axis, &span); if (ret < 0) return ret; if (!s->trim_packets || av_sat_add64(v, span) <= s->start_bound.value) return AVERROR(EAGAIN); *head = s->start_bound.value - v; return 0; } /* How much of the packet lies past the end bound, measured from head, where it * starts contributing. EAGAIN when none of it is inside, EOF once nothing later * can be. */ static int trim_tail(AVBSFContext *ctx, const AVPacket *pkt, int64_t head, int64_t *tail) { TrimContext *s = ctx->priv_data; int64_t v; int ret; if (!s->end_bound.active) return 0; /* The end is measured from the first exported packet, at the point it is * trimmed to. */ if (s->end_bound.pending) { int64_t ref = packet_axis(s, pkt, s->end_bound.axis); if (ref == AV_NOPTS_VALUE) return AVERROR(EAGAIN); if (axis_is_time(s->end_bound.axis)) { ret = add_checked(ctx, &ref, head, "end anchor"); if (ret < 0) return ret; } ret = bound_resolve(ctx, &s->end_bound, ref, "end bound"); if (ret < 0) return ret; } /* Only a discriminant that cannot come back into range may end the stream. */ v = packet_axis(s, pkt, monotonic_axis(s->end_bound.axis)); if (v != AV_NOPTS_VALUE && v >= s->end_bound.value) return AVERROR_EOF; v = packet_axis(s, pkt, s->end_bound.axis); if (v == AV_NOPTS_VALUE) return AVERROR(EAGAIN); /* Reordering can still bring in-range packets after this one. */ if (v >= s->end_bound.value) return AVERROR(EAGAIN); if (s->trim_packets) { int64_t span, past; ret = packet_span(ctx, pkt, s->end_bound.axis, &span); if (ret < 0) return ret; past = av_sat_add64(v, span); if (past > s->end_bound.value) *tail = past - s->end_bound.value; } return 0; } /* Audio is trimmed in sample space through skip samples side data, leaving the * timestamps untouched. */ static int trim_audio(AVBSFContext *ctx, AVPacket *pkt, int64_t head, int keep) { TrimContext *s = ctx->priv_data; int64_t nb_samples = packet_samples(ctx, pkt); int64_t tail = 0, anchor, input_head; int trimming = head != 0; uint8_t head_reason = 0, tail_reason = 0, input_reason; size_t size; uint8_t *side = av_packet_get_side_data(pkt, AV_PKT_DATA_SKIP_SAMPLES, &size); int ret; ret = time_base_to_samples(ctx, &head, "head trim"); if (ret < 0) return ret; /* A decoder replaces the skip it carries with any nonzero one a packet * brings, whether larger or smaller, so resolve the incoming skip first. */ input_head = s->skip_carry; input_reason = s->skip_reason; if (side && size >= 8 && AV_RL32(side)) { input_head = AV_RL32(side); input_reason = size >= 10 ? AV_RL8(side + 8) : 0; } /* That skip and this filter's trim are offsets from the same boundary, so * the larger wins and brings its own reason; the trim has none. */ if (input_head >= head) { head = input_head; head_reason = input_reason; } /* Only a trim of this filter collapses a packet; one already covered by its * own skip is passed through. */ if (!keep || (trimming && nb_samples > 0 && head >= nb_samples)) goto drop; /* A bound measured from this packet is anchored past its composed skip. */ anchor = head; ret = samples_to_time_base(ctx, &anchor, "end anchor"); if (ret < 0) return ret; ret = trim_tail(ctx, pkt, anchor, &tail); if (ret < 0) { if (ret != AVERROR(EAGAIN)) return ret; goto drop; } trimming |= tail != 0; ret = time_base_to_samples(ctx, &tail, "tail trim"); if (ret < 0) return ret; if (side && size >= 8 && AV_RL32(side + 4) >= tail) { tail = AV_RL32(side + 4); tail_reason = size >= 10 ? AV_RL8(side + 9) : 0; } if (trimming && nb_samples > 0 && head >= nb_samples - tail) goto drop; /* The side data below carries any excess from here on. */ s->skip_carry = 0; s->skip_reason = 0; if (!head && !tail) return 0; if (head > UINT32_MAX || tail > UINT32_MAX) { av_log(ctx, AV_LOG_ERROR, "skip samples count is not representable\n"); return AVERROR(ERANGE); } if (!side || size < 10) { side = av_packet_new_side_data(pkt, AV_PKT_DATA_SKIP_SAMPLES, 10); if (!side) return AVERROR(ENOMEM); } AV_WL32(side, head); AV_WL32(side + 4, tail); AV_WL8 (side + 8, head_reason); AV_WL8 (side + 9, tail_reason); return 0; drop: /* A decoder subtracts the samples of every frame it discards and carries * the rest, so do the same across dropped packets. */ if (head > 0) { int64_t consumed = packet_skip_consumed(ctx, pkt); if (consumed <= 0) { av_log(ctx, AV_LOG_ERROR, "cannot carry a %"PRId64" sample skip past " "a dropped packet of unknown sample count\n", head); return AVERROR(EINVAL); } s->skip_carry = FFMAX(head - consumed, 0); s->skip_reason = s->skip_carry ? head_reason : 0; } return AVERROR(EAGAIN); } /* Everything but audio is trimmed on the timeline itself, which no side data * can express. */ static int trim_shift(AVBSFContext *ctx, AVPacket *pkt, int64_t head) { int64_t tail = 0; int ret; ret = trim_tail(ctx, pkt, head, &tail); if (ret < 0) return ret; if (!head && !tail) return 0; /* Bounds on different axes can select disjoint parts of one packet, and * each trim is below the duration, so the subtraction cannot overflow. */ if (head >= pkt->duration - tail) return AVERROR(EAGAIN); if (pkt->pts != AV_NOPTS_VALUE) { ret = add_checked(ctx, &pkt->pts, head, "packet pts"); if (ret < 0) return ret; } if (pkt->dts != AV_NOPTS_VALUE) { ret = add_checked(ctx, &pkt->dts, head, "packet dts"); if (ret < 0) return ret; } pkt->duration -= head + tail; return 0; } static void preroll_clear(AVBSFContext *ctx) { TrimContext *s = ctx->priv_data; AVPacket *held; while (s->preroll_fifo && av_fifo_read(s->preroll_fifo, &held, 1) >= 0) av_packet_free(&held); } /* A held packet is decoded for the frames after it to reference and dropped * before output, so the exported stream still begins at the bound. */ static void preroll_export(AVBSFContext *ctx, AVPacket *pkt) { TrimContext *s = ctx->priv_data; AVPacket *held; if (av_fifo_read(s->preroll_fifo, &held, 1) >= 0) { av_packet_move_ref(pkt, held); av_packet_free(&held); pkt->flags |= AV_PKT_FLAG_DISCARD; return; } av_packet_move_ref(pkt, s->pending); av_packet_free(&s->pending); } static int preroll_hold(AVBSFContext *ctx, AVPacket *pkt) { TrimContext *s = ctx->priv_data; AVPacket *held; int ret; if (s->preroll_full) return AVERROR(EAGAIN); held = av_packet_alloc(); if (!held) return AVERROR(ENOMEM); av_packet_move_ref(held, pkt); ret = av_fifo_write(s->preroll_fifo, &held, 1); if (ret < 0) { av_packet_move_ref(pkt, held); av_packet_free(&held); if (ret != AVERROR(ENOSPC)) return ret; /* A group too long to hold is dropped whole rather than exported as a * fragment no decoder can start from. */ av_log(ctx, AV_LOG_WARNING, "preroll exceeds %d packets, the first " "packets in range will not be decodable\n", s->preroll_size); preroll_clear(ctx); s->preroll_full = 1; } return AVERROR(EAGAIN); } static void log_exported_packet(AVBSFContext *ctx, const char *which, int64_t idx, int64_t pts, int64_t dts, int key) { av_log(ctx, AV_LOG_VERBOSE, "%s exported packet: output index %"PRId64", " "pts %s, dts %s, keyframe %d\n", which, idx, av_ts2str(pts), av_ts2str(dts), key); } static void packet_exported(AVBSFContext *ctx, const AVPacket *pkt) { TrimContext *s = ctx->priv_data; s->last_pts = pkt->pts; s->last_dts = pkt->dts; s->last_key = !!(pkt->flags & AV_PKT_FLAG_KEY); if (!s->nb_exported++) log_exported_packet(ctx, "first", 0, s->last_pts, s->last_dts, s->last_key); } /* Only the flush or close ending the stream can tell which packet was last. */ static void trim_report(AVBSFContext *ctx) { TrimContext *s = ctx->priv_data; if (s->nb_exported) log_exported_packet(ctx, "last", s->nb_exported - 1, s->last_pts, s->last_dts, s->last_key); else if (s->pkt_idx) av_log(ctx, AV_LOG_WARNING, "no packet was in range, the output is " "empty\n"); } static int trim_filter(AVBSFContext *ctx, AVPacket *pkt) { TrimContext *s = ctx->priv_data; int64_t head = 0; int ret; /* The preroll an in-range packet needs is exported ahead of it. */ if (s->pending) { preroll_export(ctx, pkt); packet_exported(ctx, pkt); return 0; } ret = ff_bsf_get_packet_ref(ctx, pkt); if (ret < 0) return ret; /* Only the group the start bound falls in is worth keeping, so the window * starts over at every keyframe. */ if (s->preroll_fifo && pkt->flags & AV_PKT_FLAG_KEY) { preroll_clear(ctx); s->preroll_full = 0; } ret = trim_head(ctx, pkt, &head); /* A packet before the start still composes with the skip it carries, so the * audio path runs on it too and is told the verdict instead. */ if (s->audio && (!ret || ret == AVERROR(EAGAIN))) ret = trim_audio(ctx, pkt, head, !ret); else if (!ret) ret = trim_shift(ctx, pkt, head); else if (ret == AVERROR(EAGAIN) && s->preroll_fifo) ret = preroll_hold(ctx, pkt); s->pkt_idx++; if (ret < 0) { av_packet_unref(pkt); return ret; } s->out_idx++; if (s->preroll_fifo && av_fifo_can_read(s->preroll_fifo)) { s->pending = av_packet_alloc(); if (!s->pending) { av_packet_unref(pkt); return AVERROR(ENOMEM); } av_packet_move_ref(s->pending, pkt); preroll_export(ctx, pkt); } packet_exported(ctx, pkt); return 0; } /* Runtime state is derived from the options, so a flush restores it exactly. */ static void trim_reset(AVBSFContext *ctx) { TrimContext *s = ctx->priv_data; bound_reset(&s->start_bound); bound_reset(&s->end_bound); s->pkt_idx = 0; s->out_idx = 0; s->nb_exported = 0; s->skip_carry = 0; s->skip_reason = 0; preroll_clear(ctx); av_packet_free(&s->pending); s->preroll_full = 0; } static void trim_flush(AVBSFContext *ctx) { trim_report(ctx); trim_reset(ctx); } static void trim_close(AVBSFContext *ctx) { TrimContext *s = ctx->priv_data; trim_report(ctx); preroll_clear(ctx); av_packet_free(&s->pending); av_fifo_freep2(&s->preroll_fifo); } static int trim_init(AVBSFContext *ctx) { TrimContext *s = ctx->priv_data; int ret; if (s->start == INT64_MIN && s->end == INT64_MAX) { av_log(ctx, AV_LOG_ERROR, "At least one of start or end must be set\n"); return AVERROR(EINVAL); } ret = bound_init(ctx, &s->start_bound, s->start != INT64_MIN, s->start, s->start_type, s->start_rel, "start"); if (ret < 0) return ret; ret = bound_init(ctx, &s->end_bound, s->end != INT64_MAX, s->end, s->end_type, s->end_rel, "end"); if (ret < 0) return ret; /* A packet count measured from the first exported packet is a promise about * the exported stream, so it is counted there. */ if (s->end_bound.axis == AXIS_INDEX && s->end_bound.relative) s->end_bound.axis = AXIS_OUT_INDEX; /* Shifting the timeline leaves the samples where they were, so audio with * no sample space to be trimmed in cannot be trimmed at all. */ if (s->trim_packets && ctx->par_in->codec_type == AVMEDIA_TYPE_AUDIO) { if (ctx->par_in->sample_rate <= 0 || ctx->time_base_in.num <= 0 || ctx->time_base_in.den <= 0) { av_log(ctx, AV_LOG_ERROR, "audio is trimmed through skip samples " "side data, which needs a sample rate and a time base\n"); return AVERROR(EINVAL); } s->audio = 1; } /* The window is bounded by a keyframe, which only video has. Audio needs a * preroll counted in samples, which is not the one this would give. */ if (s->preroll && s->start_bound.active && ctx->par_in->codec_type == AVMEDIA_TYPE_VIDEO) { s->preroll_fifo = av_fifo_alloc2(1, sizeof(AVPacket *), AV_FIFO_FLAG_AUTO_GROW); if (!s->preroll_fifo) return AVERROR(ENOMEM); av_fifo_auto_grow_limit(s->preroll_fifo, s->preroll_size); } trim_reset(ctx); return 0; } #define OFFSET(x) offsetof(TrimContext, x) #define FLAGS (AV_OPT_FLAG_VIDEO_PARAM|AV_OPT_FLAG_AUDIO_PARAM|AV_OPT_FLAG_BSF_PARAM) static const AVOption trim_options[] = { { "start", "time or index marking the start of the accepted range", OFFSET(start), AV_OPT_TYPE_INT64, { .i64 = INT64_MIN }, INT64_MIN, INT64_MAX, FLAGS }, { "start_type", "how to interpret start", OFFSET(start_type), AV_OPT_TYPE_INT, { .i64 = TRIM_PTS }, 0, TRIM_MSEC_DT, FLAGS, .unit = "start_type" }, { "pts", "stream time base pts", 0, AV_OPT_TYPE_CONST, { .i64 = TRIM_PTS }, 0, 0, FLAGS, .unit = "start_type" }, { "dts", "stream time base dts", 0, AV_OPT_TYPE_CONST, { .i64 = TRIM_DTS }, 0, 0, FLAGS, .unit = "start_type" }, { "pkt_index", "packet index", 0, AV_OPT_TYPE_CONST, { .i64 = TRIM_PKT_INDEX }, 0, 0, FLAGS, .unit = "start_type" }, { "msec_pt", "milliseconds, matched against pts", 0, AV_OPT_TYPE_CONST, { .i64 = TRIM_MSEC_PT }, 0, 0, FLAGS, .unit = "start_type" }, { "msec_dt", "milliseconds, matched against dts", 0, AV_OPT_TYPE_CONST, { .i64 = TRIM_MSEC_DT }, 0, 0, FLAGS, .unit = "start_type" }, { "start_rel", "interpret start relative to the first packet of the stream", OFFSET(start_rel), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, FLAGS }, { "end", "time or index marking the end of the accepted range", OFFSET(end), AV_OPT_TYPE_INT64, { .i64 = INT64_MAX }, INT64_MIN, INT64_MAX, FLAGS }, { "end_type", "how to interpret end", OFFSET(end_type), AV_OPT_TYPE_INT, { .i64 = TRIM_PTS }, 0, TRIM_DUR_T_MSEC, FLAGS, .unit = "end_type" }, { "pts", "stream time base pts", 0, AV_OPT_TYPE_CONST, { .i64 = TRIM_PTS }, 0, 0, FLAGS, .unit = "end_type" }, { "dts", "stream time base dts", 0, AV_OPT_TYPE_CONST, { .i64 = TRIM_DTS }, 0, 0, FLAGS, .unit = "end_type" }, { "pkt_index", "packet index", 0, AV_OPT_TYPE_CONST, { .i64 = TRIM_PKT_INDEX }, 0, 0, FLAGS, .unit = "end_type" }, { "msec_pt", "milliseconds, matched against pts", 0, AV_OPT_TYPE_CONST, { .i64 = TRIM_MSEC_PT }, 0, 0, FLAGS, .unit = "end_type" }, { "msec_dt", "milliseconds, matched against dts", 0, AV_OPT_TYPE_CONST, { .i64 = TRIM_MSEC_DT }, 0, 0, FLAGS, .unit = "end_type" }, { "dur_ts", "duration in stream time base", 0, AV_OPT_TYPE_CONST, { .i64 = TRIM_DUR_TS }, 0, 0, FLAGS, .unit = "end_type" }, { "dur_t_msec", "duration in milliseconds", 0, AV_OPT_TYPE_CONST, { .i64 = TRIM_DUR_T_MSEC }, 0, 0, FLAGS, .unit = "end_type" }, { "end_rel", "interpret end relative to the first exported packet", OFFSET(end_rel), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, FLAGS }, { "trim_packets", "trim packets straddling a boundary instead of exporting them " "untouched or dropping them", OFFSET(trim_packets), AV_OPT_TYPE_BOOL, { .i64 = 1 }, 0, 1, FLAGS }, { "preroll", "export the packets the first packet in range needs to be decodable, " "flagged for the decoder to drop them after decoding", OFFSET(preroll), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, FLAGS }, { "preroll_size", "maximum number of packets held for preroll", OFFSET(preroll_size), AV_OPT_TYPE_INT, { .i64 = 300 }, 1, INT_MAX, FLAGS }, { NULL }, }; static const AVClass trim_class = { .class_name = "trim", .item_name = av_default_item_name, .option = trim_options, .version = LIBAVUTIL_VERSION_INT, }; const FFBitStreamFilter ff_trim_bsf = { .p.name = "trim", .p.priv_class = &trim_class, .priv_data_size = sizeof(TrimContext), .init = trim_init, .close = trim_close, .flush = trim_flush, .filter = trim_filter, };