add from/to parametrs support for transcoding function

from/to specified in ms from segment start
This commit is contained in:
Ivan Tivonenko
2021-11-22 15:35:39 +02:00
parent 46f7030c62
commit 5da83d2f46
10 changed files with 200 additions and 18 deletions

View File

@@ -18,8 +18,9 @@ func validRenditions() []string {
}
func main() {
var err error
if len(os.Args) <= 3 {
panic("Usage: <input file> <output renditions, comma separated> <sw/nv>")
panic("Usage: <input file> <output renditions, comma separated> <sw/nv> <from> <to>")
}
str2accel := func(inp string) (ffmpeg.Acceleration, string) {
if inp == "nv" {
@@ -42,17 +43,32 @@ func main() {
fname := os.Args[1]
profiles := str2profs(os.Args[2])
accel, lbl := str2accel(os.Args[3])
var from, to time.Duration
if len(os.Args) > 4 {
from, err = time.ParseDuration(os.Args[4])
if err != nil {
panic(err)
}
}
if len(os.Args) > 5 {
to, err = time.ParseDuration(os.Args[5])
if err != nil {
panic(err)
}
}
profs2opts := func(profs []ffmpeg.VideoProfile) []ffmpeg.TranscodeOptions {
opts := []ffmpeg.TranscodeOptions{}
for i := range profs {
o := ffmpeg.TranscodeOptions{
Oname: fmt.Sprintf("out_%s_%d_out.mkv", lbl, i),
Oname: fmt.Sprintf("out_%s_%d_out.mp4", lbl, i),
Profile: profs[i],
// Uncomment the following to test scene classifier
// Detector: &ffmpeg.DSceneAdultSoccer,
Accel: accel,
}
o.From = from
o.To = to
opts = append(opts, o)
}
return opts

View File

@@ -1288,6 +1288,40 @@ func TestTranscoder_OutputFPS(t *testing.T) {
outputFPS(t, Software)
}
func TestTranscoderAPI_ClipInvalidConfig(t *testing.T) {
tc := NewTranscoder()
defer tc.StopTranscoder()
in := &TranscodeOptionsIn{}
out := []TranscodeOptions{{
Oname: "-",
VideoEncoder: ComponentOptions{Name: "drop"},
From: time.Second,
}}
_, err := tc.Transcode(in, out)
if err == nil || err != ErrTranscoderClipConfig {
t.Errorf("Expected '%s', got %v", ErrTranscoderClipConfig, err)
}
out[0].VideoEncoder.Name = "copy"
_, err = tc.Transcode(in, out)
if err == nil || err != ErrTranscoderClipConfig {
t.Errorf("Expected '%s', got %v", ErrTranscoderClipConfig, err)
}
out[0].From = 0
out[0].To = time.Second
_, err = tc.Transcode(in, out)
if err == nil || err != ErrTranscoderClipConfig {
t.Errorf("Expected '%s', got %v", ErrTranscoderClipConfig, err)
}
out[0].VideoEncoder.Name = ""
out[0].From = 10 * time.Second
out[0].To = time.Second
_, err = tc.Transcode(in, out)
if err == nil || err != ErrTranscoderClipConfig {
t.Errorf("Expected '%s', got %v", ErrTranscoderClipConfig, err)
}
}
/*
func detectionFreq(t *testing.T, accel Acceleration) {
run, dir := setupTest(t)

View File

@@ -26,17 +26,23 @@ static int add_video_stream(struct output_ctx *octx, struct input_ctx *ictx)
} else if (octx->vc) {
st->time_base = octx->vc->time_base;
ret = avcodec_parameters_from_context(st->codecpar, octx->vc);
if (octx->gop_time) {
// Rescale the gop time to the expected timebase after filtering.
// Rescale the gop/clip time to the expected timebase after filtering.
// The FPS filter outputs pts incrementing by 1 at a rate of 1/framerate
// while non-fps will retain the input timebase.
AVRational gop_tb = {1, 1000};
AVRational ms_tb = {1, 1000};
AVRational dest_tb;
if (octx->fps.den) dest_tb = av_inv_q(octx->fps);
else dest_tb = ictx->ic->streams[ictx->vi]->time_base;
octx->gop_pts_len = av_rescale_q(octx->gop_time, gop_tb, dest_tb);
if (octx->gop_time) {
octx->gop_pts_len = av_rescale_q(octx->gop_time, ms_tb, dest_tb);
octx->next_kf_pts = 0; // force for first frame
}
if (octx->clip_from) {
octx->clip_from_pts = av_rescale_q(octx->clip_from, ms_tb, dest_tb);
}
if (octx->clip_to) {
octx->clip_to_pts = av_rescale_q(octx->clip_to, ms_tb, dest_tb);
}
if (ret < 0) LPMS_ERR(add_video_err, "Error setting video params from encoder");
} else LPMS_ERR(add_video_err, "No video encoder, not a copy; what is this?");
return 0;
@@ -444,6 +450,29 @@ int process_out(struct input_ctx *ictx, struct output_ctx *octx, AVCodecContext
frame = NULL;
} else if (ret < 0) goto proc_cleanup;
if (is_video && !octx->clip_start_pts_found && frame) {
octx->clip_start_pts = frame->pts;
octx->clip_start_pts_found = 1;
}
if (octx->clip_to && octx->clip_start_pts_found && frame && frame->pts > octx->clip_to_pts + octx->clip_start_pts) goto skip;
if (is_video) {
if (octx->clip_from && frame) {
if (frame->pts < octx->clip_from_pts + octx->clip_start_pts) goto skip;
if (!octx->clip_started) {
octx->clip_started = 1;
frame->pict_type = AV_PICTURE_TYPE_I;
if (octx->gop_pts_len) {
octx->next_kf_pts = frame->pts + octx->gop_pts_len;
}
}
}
} else if (octx->clip_from_pts && !octx->clip_started) {
// we want first frame to be video frame
goto skip;
}
// Set GOP interval if necessary
if (is_video && octx->gop_pts_len && frame && frame->pts >= octx->next_kf_pts) {
frame->pict_type = AV_PICTURE_TYPE_I;
@@ -458,6 +487,7 @@ int process_out(struct input_ctx *ictx, struct output_ctx *octx, AVCodecContext
}
ret = encode(encoder, frame, octx, ost);
}
skip:
av_frame_unref(frame);
// For HW we keep the encoder open so will only get EAGAIN.
// Return EOF in place of EAGAIN for to terminate the flush

View File

@@ -12,6 +12,7 @@ import (
"strconv"
"strings"
"sync"
"time"
"unsafe"
"github.com/golang/glog"
@@ -28,6 +29,7 @@ import "C"
var ErrTranscoderRes = errors.New("TranscoderInvalidResolution")
var ErrTranscoderHw = errors.New("TranscoderInvalidHardware")
var ErrTranscoderInp = errors.New("TranscoderInvalidInput")
var ErrTranscoderClipConfig = errors.New("TranscoderInvalidClipConfig")
var ErrTranscoderVid = errors.New("TranscoderInvalidVideo")
var ErrTranscoderStp = errors.New("TranscoderStopped")
var ErrTranscoderFmt = errors.New("TranscoderUnrecognizedFormat")
@@ -72,6 +74,8 @@ type TranscodeOptions struct {
Accel Acceleration
Device string
CalcSign bool
From time.Duration
To time.Duration
Muxer ComponentOptions
VideoEncoder ComponentOptions
@@ -269,6 +273,16 @@ func (t *Transcoder) Transcode(input *TranscodeOptionsIn, ps []TranscodeOptions)
if err != nil {
return nil, err
}
for _, p := range ps {
if (p.From > 0 || p.To > 0) && (p.VideoEncoder.Name == "drop" || p.VideoEncoder.Name == "copy") {
glog.Warning("Could clip only when transcoding video")
return nil, ErrTranscoderClipConfig
}
if p.From > 0 && p.To > 0 && p.To < p.From {
glog.Warning("'To' should be after 'From'")
return nil, ErrTranscoderClipConfig
}
}
fname := C.CString(input.Fname)
defer C.free(unsafe.Pointer(fname))
if input.Transmuxing {
@@ -438,6 +452,8 @@ func (t *Transcoder) Transcode(input *TranscodeOptionsIn, ps []TranscodeOptions)
name: C.CString(audioEncoder),
opts: newAVOpts(p.AudioEncoder.Opts),
}
fromMs := int(p.From.Milliseconds())
toMs := int(p.To.Milliseconds())
vfilt := C.CString(filters)
defer C.free(unsafe.Pointer(vidOpts.name))
defer C.free(unsafe.Pointer(audioOpts.name))
@@ -448,7 +464,7 @@ func (t *Transcoder) Transcode(input *TranscodeOptionsIn, ps []TranscodeOptions)
}
params[i] = C.output_params{fname: oname, fps: fps,
w: C.int(w), h: C.int(h), bitrate: C.int(bitrate),
gop_time: C.int(gopMs),
gop_time: C.int(gopMs), from: C.int(fromMs), to: C.int(toMs),
muxer: muxOpts, audio: audioOpts, video: vidOpts,
vfilters: vfilt, sfilters: nil, is_dnn: isDNN}
if p.CalcSign {

View File

@@ -8,6 +8,10 @@ import (
"os/exec"
"path"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func setupTest(t *testing.T) (func(cmd string), string) {
@@ -1602,3 +1606,83 @@ func TestTranscoder_ZeroFrameLongBadSegment(t *testing.T) {
t.Errorf("Expecting false, got %v", res)
}
}
func TestTranscoder_Clip(t *testing.T) {
run, dir := setupTest(t)
defer os.RemoveAll(dir)
// If no format and no mux opts specified, should be based on file extension
in := &TranscodeOptionsIn{Fname: "../transcoder/test.ts"}
P144p30fps16x9 := P144p30fps16x9
P144p30fps16x9.Framerate = 0
out := []TranscodeOptions{{
Profile: P144p30fps16x9,
Oname: dir + "/test_0.mp4",
// Oname: "./test_0.mp4",
From: time.Second,
To: 3 * time.Second,
}}
res, err := Transcode3(in, out)
require.NoError(t, err)
if err != nil {
t.Error(err)
}
assert.Equal(t, 480, res.Decoded.Frames)
assert.Equal(t, int64(442368000), res.Decoded.Pixels)
assert.Equal(t, 120, res.Encoded[0].Frames)
assert.Equal(t, int64(4423680), res.Encoded[0].Pixels)
cmd := `
# hardcode some checks for now. TODO make relative to source.
ffprobe -loglevel warning -select_streams v -show_streams -count_frames test_0.mp4 > test.out
grep start_pts=94410 test.out
grep start_time=1.049000 test.out
grep duration=2.000667 test.out
grep duration_ts=180060 test.out
grep nb_read_frames=120 test.out
`
run(cmd)
}
func TestTranscoder_Clip2(t *testing.T) {
run, dir := setupTest(t)
defer os.RemoveAll(dir)
// If no format and no mux opts specified, should be based on file extension
in := &TranscodeOptionsIn{Fname: "../transcoder/test.ts"}
P144p30fps16x9 := P144p30fps16x9
P144p30fps16x9.GOP = 5 * time.Second
P144p30fps16x9.Framerate = 120
out := []TranscodeOptions{{
Profile: P144p30fps16x9,
Oname: dir + "/test_0.mp4",
// Oname: "./test_1.mp4",
From: time.Second,
To: 6 * time.Second,
}}
res, err := Transcode3(in, out)
require.NoError(t, err)
if err != nil {
t.Error(err)
}
assert.Equal(t, 480, res.Decoded.Frames)
assert.Equal(t, int64(442368000), res.Decoded.Pixels)
assert.Equal(t, 601, res.Encoded[0].Frames)
assert.Equal(t, int64(22155264), res.Encoded[0].Pixels)
cmd := `
# hardcode some checks for now. TODO make relative to source.
ffprobe -loglevel warning -select_streams v -show_streams -count_frames test_0.mp4 > test.out
grep start_pts=15867 test.out
grep start_time=1.033008 test.out
grep duration=5.008333 test.out
grep duration_ts=76928 test.out
grep nb_read_frames=601 test.out
# check that we have two keyframes
ffprobe -loglevel warning -hide_banner -show_frames test_0.mp4 | grep pict_type=I -c | grep 2
# check indexes of keyframes
ffprobe -loglevel warning -hide_banner -show_frames -show_entries frame=pict_type -of csv test_0.mp4 | grep -n "frame,I" | cut -d ':' -f 1 | awk 'BEGIN{ORS=":"} {print}' | grep '1:602:'
`
run(cmd)
}

View File

@@ -61,6 +61,8 @@ struct output_ctx {
int64_t gop_time, gop_pts_len, next_kf_pts; // for gop reset
int64_t clip_from, clip_to, clip_from_pts, clip_to_pts, clip_started, clip_start_pts, clip_start_pts_found; // for clipping
AVFilterGraph **dnn_filtergraph;
int is_dnn_profile; //if not dnn profile: 0

View File

@@ -170,6 +170,8 @@ int transcode(struct transcode_thread *h,
if (params[i].bitrate) octx->bitrate = params[i].bitrate;
if (params[i].fps.den) octx->fps = params[i].fps;
if (params[i].gop_time) octx->gop_time = params[i].gop_time;
if (params[i].from) octx->clip_from = params[i].from;
if (params[i].to) octx->clip_to = params[i].to;
octx->dv = ictx->vi < 0 || is_drop(octx->video->name);
octx->da = ictx->ai < 0 || is_drop(octx->audio->name);
octx->res = &results[i];

View File

@@ -29,7 +29,7 @@ typedef struct {
char *fname;
char *vfilters;
char *sfilters;
int w, h, bitrate, gop_time;
int w, h, bitrate, gop_time, from, to;
AVRational fps;
int is_dnn;

2
go.mod
View File

@@ -7,6 +7,6 @@ require (
github.com/golang/protobuf v1.5.2
github.com/livepeer/joy4 v0.1.2-0.20191121080656-b2fea45cbded
github.com/livepeer/m3u8 v0.11.1
github.com/stretchr/testify v1.3.0
google.golang.org/protobuf v1.26.0
github.com/olekukonko/tablewriter v0.0.5 // indirect
)

6
go.sum
View File

@@ -5,20 +5,18 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/livepeer/joy4 v0.1.2-0.20191121080656-b2fea45cbded h1:ZQlvR5RB4nfT+cOQee+WqmaDOgGtP2oDMhcVvR4L0yA=
github.com/livepeer/joy4 v0.1.2-0.20191121080656-b2fea45cbded/go.mod h1:xkDdm+akniYxVT9KW1Y2Y7Hso6aW+rZObz3nrA9yTHw=
github.com/livepeer/m3u8 v0.11.1 h1:VkUJzfNTyjy9mqsgp5JPvouwna8wGZMvd/gAfT5FinU=
github.com/livepeer/m3u8 v0.11.1/go.mod h1:IUqAtwWPAG2CblfQa4SVzTQoDcEMPyfNOaBSxqHMS04=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=