mirror of
https://github.com/FFmpeg/FFmpeg.git
synced 2026-11-06 05:17:42 +00:00
Drop packets outside a range given as a pts, dts, packet index, millisecond or duration bound, trimming a packet that straddles a boundary through skip samples side data for audio and shifted timestamps otherwise. Set preroll to export the packets a decoder needs to start on a cut made inside a group of pictures, flagged for discard so the frames it produces still begin at the bound.
799 lines
26 KiB
C
799 lines
26 KiB
C
/*
|
|
* 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,
|
|
};
|